mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add alchemy and improve cli tooling and structure (#520)
This commit is contained in:
@@ -66,7 +66,8 @@ Options:
|
||||
--install Install dependencies
|
||||
--no-install Skip installing dependencies
|
||||
--db-setup <setup> Database setup (turso, d1, neon, supabase, prisma-postgres, mongodb-atlas, docker, none)
|
||||
--web-deploy <setup> Web deployment (workers, none)
|
||||
--web-deploy <setup> Web deployment (workers, alchemy, none)
|
||||
--server-deploy <setup> Server deployment (workers, alchemy, none)
|
||||
--backend <framework> Backend framework (hono, express, elysia, next, convex, fastify, none)
|
||||
--runtime <runtime> Runtime (bun, node, workers, none)
|
||||
--api <type> API type (trpc, orpc, none)
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
"dev": "tsdown --watch",
|
||||
"check-types": "tsc --noEmit",
|
||||
"check": "biome check --write .",
|
||||
"test": "bun run build && vitest --ui",
|
||||
"test": "bun run build && vitest run",
|
||||
"test:ui": "bun run build && vitest --ui",
|
||||
"test:with-build": "bun run build && WITH_BUILD=1 vitest --ui",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
@@ -63,22 +64,24 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@biomejs/js-api": "^3.0.0",
|
||||
"@biomejs/wasm-nodejs": "^2.2.0",
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"consola": "^3.4.2",
|
||||
"execa": "^9.6.0",
|
||||
"fs-extra": "^11.3.1",
|
||||
"globby": "^14.1.0",
|
||||
"gradient-string": "^3.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"picocolors": "^1.1.1",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"trpc-cli": "^0.10.2",
|
||||
"ts-morph": "^26.0.0",
|
||||
"zod": "^4.0.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"tsdown": "^0.14.1",
|
||||
"typescript": "^5.9.2",
|
||||
|
||||
@@ -7,25 +7,37 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const distPath = path.dirname(__filename);
|
||||
export const PKG_ROOT = path.join(distPath, "../");
|
||||
|
||||
export const DEFAULT_CONFIG: ProjectConfig = {
|
||||
export const DEFAULT_CONFIG_BASE = {
|
||||
projectName: "my-better-t-app",
|
||||
projectDir: path.resolve(process.cwd(), "my-better-t-app"),
|
||||
relativePath: "my-better-t-app",
|
||||
frontend: ["tanstack-router"],
|
||||
database: "sqlite",
|
||||
orm: "drizzle",
|
||||
frontend: ["tanstack-router"] as const,
|
||||
database: "sqlite" as const,
|
||||
orm: "drizzle" as const,
|
||||
auth: true,
|
||||
addons: ["turborepo"],
|
||||
examples: [],
|
||||
addons: ["turborepo"] as const,
|
||||
examples: [] as const,
|
||||
git: true,
|
||||
packageManager: getUserPkgManager(),
|
||||
install: true,
|
||||
dbSetup: "none",
|
||||
backend: "hono",
|
||||
runtime: "bun",
|
||||
api: "trpc",
|
||||
webDeploy: "none",
|
||||
};
|
||||
dbSetup: "none" as const,
|
||||
backend: "hono" as const,
|
||||
runtime: "bun" as const,
|
||||
api: "trpc" as const,
|
||||
webDeploy: "none" as const,
|
||||
serverDeploy: "none" as const,
|
||||
} as const;
|
||||
|
||||
export function getDefaultConfig(): ProjectConfig {
|
||||
return {
|
||||
...DEFAULT_CONFIG_BASE,
|
||||
projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
|
||||
packageManager: getUserPkgManager(),
|
||||
frontend: [...DEFAULT_CONFIG_BASE.frontend],
|
||||
addons: [...DEFAULT_CONFIG_BASE.addons],
|
||||
examples: [...DEFAULT_CONFIG_BASE.examples],
|
||||
};
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG = getDefaultConfig();
|
||||
|
||||
export const dependencyVersionMap = {
|
||||
"better-auth": "^1.3.4",
|
||||
@@ -106,7 +118,8 @@ export const dependencyVersionMap = {
|
||||
"convex-nuxt": "0.1.5",
|
||||
"convex-vue": "^0.1.5",
|
||||
|
||||
"@tanstack/svelte-query": "^5.74.4",
|
||||
"@tanstack/svelte-query": "^5.85.3",
|
||||
"@tanstack/svelte-query-devtools": "^5.85.3",
|
||||
|
||||
"@tanstack/vue-query-devtools": "^5.83.0",
|
||||
"@tanstack/vue-query": "^5.83.0",
|
||||
@@ -116,19 +129,27 @@ export const dependencyVersionMap = {
|
||||
|
||||
"@tanstack/solid-query": "^5.75.0",
|
||||
"@tanstack/solid-query-devtools": "^5.75.0",
|
||||
"@tanstack/solid-router-devtools": "^1.131.25",
|
||||
|
||||
wrangler: "^4.23.0",
|
||||
"@cloudflare/vite-plugin": "^1.9.0",
|
||||
"@opennextjs/cloudflare": "^1.3.0",
|
||||
"nitro-cloudflare-dev": "^0.2.2",
|
||||
"@sveltejs/adapter-cloudflare": "^7.0.4",
|
||||
"@sveltejs/adapter-cloudflare": "^7.2.1",
|
||||
"@cloudflare/workers-types": "^4.20250813.0",
|
||||
|
||||
alchemy: "^0.62.1",
|
||||
// temporary workaround for alchemy + tanstack start
|
||||
nitropack: "^2.12.4",
|
||||
|
||||
dotenv: "^17.2.1",
|
||||
} as const;
|
||||
|
||||
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
||||
|
||||
export const ADDON_COMPATIBILITY: Record<Addons, readonly Frontend[]> = {
|
||||
pwa: ["tanstack-router", "react-router", "solid", "next"],
|
||||
tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid"],
|
||||
tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid", "next"],
|
||||
biome: [],
|
||||
husky: [],
|
||||
turborepo: [],
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PKG_ROOT } from "../../constants";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { exitCancelled } from "../../utils/errors";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
import { processAndCopyFiles } from "../project-generation/template-manager";
|
||||
import { processAndCopyFiles } from "../core/template-manager";
|
||||
|
||||
export async function setupVibeRules(config: ProjectConfig) {
|
||||
const { packageManager, projectDir } = config;
|
||||
@@ -69,8 +69,8 @@ export async function setupTauri(config: ProjectConfig) {
|
||||
`--window-title=${path.basename(projectDir)}`,
|
||||
`--frontend-dist=${frontendDist}`,
|
||||
`--dev-url=${devUrl}`,
|
||||
`--before-dev-command=\"${packageManager} run dev\"`,
|
||||
`--before-build-command=\"${packageManager} run build\"`,
|
||||
`--before-dev-command="${packageManager} run dev"`,
|
||||
`--before-build-command="${packageManager} run build"`,
|
||||
];
|
||||
const tauriArgsString = tauriArgs.join(" ");
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { AddInput, Addons, ProjectConfig } from "../../types";
|
||||
import { validateAddonCompatibility } from "../../utils/addon-compatibility";
|
||||
import { updateBtsConfig } from "../../utils/bts-config";
|
||||
import { exitWithError } from "../../utils/errors";
|
||||
import { setupAddons } from "../setup/addons-setup";
|
||||
import { setupAddons } from "../addons/addons-setup";
|
||||
import {
|
||||
detectProjectConfig,
|
||||
isBetterTStackProject,
|
||||
@@ -52,6 +52,7 @@ export async function addAddonsToProject(
|
||||
dbSetup: detectedConfig.dbSetup || "none",
|
||||
api: detectedConfig.api || "none",
|
||||
webDeploy: detectedConfig.webDeploy || "none",
|
||||
serverDeploy: detectedConfig.serverDeploy || "none",
|
||||
};
|
||||
|
||||
for (const addon of input.addons) {
|
||||
@@ -1,10 +1,16 @@
|
||||
import path from "node:path";
|
||||
import { log } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { AddInput, ProjectConfig, WebDeploy } from "../../types";
|
||||
import type {
|
||||
AddInput,
|
||||
ProjectConfig,
|
||||
ServerDeploy,
|
||||
WebDeploy,
|
||||
} from "../../types";
|
||||
import { updateBtsConfig } from "../../utils/bts-config";
|
||||
import { exitWithError } from "../../utils/errors";
|
||||
import { setupWebDeploy } from "../setup/web-deploy-setup";
|
||||
import { setupServerDeploy } from "../deployment/server-deploy-setup";
|
||||
import { setupWebDeploy } from "../deployment/web-deploy-setup";
|
||||
import {
|
||||
detectProjectConfig,
|
||||
isBetterTStackProject,
|
||||
@@ -13,7 +19,11 @@ import { installDependencies } from "./install-dependencies";
|
||||
import { setupDeploymentTemplates } from "./template-manager";
|
||||
|
||||
export async function addDeploymentToProject(
|
||||
input: AddInput & { webDeploy: WebDeploy; suppressInstallMessage?: boolean },
|
||||
input: AddInput & {
|
||||
webDeploy?: WebDeploy;
|
||||
serverDeploy?: ServerDeploy;
|
||||
suppressInstallMessage?: boolean;
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const projectDir = input.projectDir || process.cwd();
|
||||
@@ -32,9 +42,18 @@ export async function addDeploymentToProject(
|
||||
);
|
||||
}
|
||||
|
||||
if (detectedConfig.webDeploy === input.webDeploy) {
|
||||
if (input.webDeploy && detectedConfig.webDeploy === input.webDeploy) {
|
||||
exitWithError(
|
||||
`${input.webDeploy} deployment is already configured for this project.`,
|
||||
`${input.webDeploy} web deployment is already configured for this project.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
input.serverDeploy &&
|
||||
detectedConfig.serverDeploy === input.serverDeploy
|
||||
) {
|
||||
exitWithError(
|
||||
`${input.serverDeploy} server deployment is already configured for this project.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,19 +75,30 @@ export async function addDeploymentToProject(
|
||||
install: input.install || false,
|
||||
dbSetup: detectedConfig.dbSetup || "none",
|
||||
api: detectedConfig.api || "none",
|
||||
webDeploy: input.webDeploy,
|
||||
webDeploy: input.webDeploy || detectedConfig.webDeploy || "none",
|
||||
serverDeploy: input.serverDeploy || detectedConfig.serverDeploy || "none",
|
||||
};
|
||||
|
||||
log.info(
|
||||
pc.green(
|
||||
`Adding ${input.webDeploy} deployment to ${config.frontend.join("/")}`,
|
||||
),
|
||||
);
|
||||
if (input.webDeploy && input.webDeploy !== "none") {
|
||||
log.info(
|
||||
pc.green(
|
||||
`Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (input.serverDeploy && input.serverDeploy !== "none") {
|
||||
log.info(pc.green(`Adding ${input.serverDeploy} server deployment`));
|
||||
}
|
||||
|
||||
await setupDeploymentTemplates(projectDir, config);
|
||||
await setupWebDeploy(config);
|
||||
await setupServerDeploy(config);
|
||||
|
||||
await updateBtsConfig(projectDir, { webDeploy: input.webDeploy });
|
||||
await updateBtsConfig(projectDir, {
|
||||
webDeploy: input.webDeploy || config.webDeploy,
|
||||
serverDeploy: input.serverDeploy || config.serverDeploy,
|
||||
});
|
||||
|
||||
if (config.install) {
|
||||
await installDependencies({
|
||||
303
apps/cli/src/helpers/core/api-setup.ts
Normal file
303
apps/cli/src/helpers/core/api-setup.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { AvailableDependencies } from "../../constants";
|
||||
import type { Frontend, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
async function addBackendWorkspaceDependency(
|
||||
projectDir: string,
|
||||
backendPackageName: string,
|
||||
workspaceVersion: string,
|
||||
) {
|
||||
const pkgJsonPath = path.join(projectDir, "package.json");
|
||||
try {
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
if (!pkgJson.dependencies) {
|
||||
pkgJson.dependencies = {};
|
||||
}
|
||||
pkgJson.dependencies[backendPackageName] = workspaceVersion;
|
||||
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
function getFrontendType(frontend: Frontend[]): {
|
||||
hasReactWeb: boolean;
|
||||
hasNuxtWeb: boolean;
|
||||
hasSvelteWeb: boolean;
|
||||
hasSolidWeb: boolean;
|
||||
hasNative: boolean;
|
||||
} {
|
||||
const reactBasedFrontends = [
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
];
|
||||
const nativeFrontends = ["native-nativewind", "native-unistyles"];
|
||||
|
||||
return {
|
||||
hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)),
|
||||
hasNuxtWeb: frontend.includes("nuxt"),
|
||||
hasSvelteWeb: frontend.includes("svelte"),
|
||||
hasSolidWeb: frontend.includes("solid"),
|
||||
hasNative: frontend.some((f) => nativeFrontends.includes(f)),
|
||||
};
|
||||
}
|
||||
|
||||
function getApiDependencies(
|
||||
api: string,
|
||||
frontendType: ReturnType<typeof getFrontendType>,
|
||||
) {
|
||||
const deps: Record<
|
||||
string,
|
||||
{ dependencies: string[]; devDependencies?: string[] }
|
||||
> = {};
|
||||
|
||||
if (api === "orpc") {
|
||||
deps.server = { dependencies: ["@orpc/server", "@orpc/client"] };
|
||||
} else if (api === "trpc") {
|
||||
deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
|
||||
}
|
||||
|
||||
if (frontendType.hasReactWeb) {
|
||||
if (api === "orpc") {
|
||||
deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
||||
} else if (api === "trpc") {
|
||||
deps.web = {
|
||||
dependencies: [
|
||||
"@trpc/tanstack-react-query",
|
||||
"@trpc/client",
|
||||
"@trpc/server",
|
||||
],
|
||||
};
|
||||
}
|
||||
} else if (frontendType.hasNuxtWeb && api === "orpc") {
|
||||
deps.web = {
|
||||
dependencies: [
|
||||
"@tanstack/vue-query",
|
||||
"@orpc/tanstack-query",
|
||||
"@orpc/client",
|
||||
],
|
||||
devDependencies: ["@tanstack/vue-query-devtools"],
|
||||
};
|
||||
} else if (frontendType.hasSvelteWeb && api === "orpc") {
|
||||
deps.web = {
|
||||
dependencies: [
|
||||
"@orpc/tanstack-query",
|
||||
"@orpc/client",
|
||||
"@tanstack/svelte-query",
|
||||
],
|
||||
devDependencies: ["@tanstack/svelte-query-devtools"],
|
||||
};
|
||||
} else if (frontendType.hasSolidWeb && api === "orpc") {
|
||||
deps.web = {
|
||||
dependencies: [
|
||||
"@orpc/tanstack-query",
|
||||
"@orpc/client",
|
||||
"@tanstack/solid-query",
|
||||
],
|
||||
devDependencies: [
|
||||
"@tanstack/solid-query-devtools",
|
||||
"@tanstack/solid-router-devtools",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (api === "trpc") {
|
||||
deps.native = {
|
||||
dependencies: [
|
||||
"@trpc/tanstack-react-query",
|
||||
"@trpc/client",
|
||||
"@trpc/server",
|
||||
],
|
||||
};
|
||||
} else if (api === "orpc") {
|
||||
deps.native = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
function getQueryDependencies(frontend: Frontend[]) {
|
||||
const reactBasedFrontends: Frontend[] = [
|
||||
"react-router",
|
||||
"tanstack-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"native-nativewind",
|
||||
"native-unistyles",
|
||||
];
|
||||
|
||||
const deps: Record<
|
||||
string,
|
||||
{ dependencies: string[]; devDependencies?: string[] }
|
||||
> = {};
|
||||
|
||||
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
|
||||
if (needsReactQuery) {
|
||||
const hasReactWeb = frontend.some(
|
||||
(f) =>
|
||||
f !== "native-nativewind" &&
|
||||
f !== "native-unistyles" &&
|
||||
reactBasedFrontends.includes(f),
|
||||
);
|
||||
const hasNative =
|
||||
frontend.includes("native-nativewind") ||
|
||||
frontend.includes("native-unistyles");
|
||||
|
||||
if (hasReactWeb) {
|
||||
deps.web = {
|
||||
dependencies: ["@tanstack/react-query"],
|
||||
devDependencies: ["@tanstack/react-query-devtools"],
|
||||
};
|
||||
}
|
||||
if (hasNative) {
|
||||
deps.native = { dependencies: ["@tanstack/react-query"] };
|
||||
}
|
||||
}
|
||||
|
||||
if (frontend.includes("solid")) {
|
||||
deps.web = {
|
||||
dependencies: ["@tanstack/solid-query"],
|
||||
devDependencies: [
|
||||
"@tanstack/solid-query-devtools",
|
||||
"@tanstack/solid-router-devtools",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
function getConvexDependencies(frontend: Frontend[]) {
|
||||
const deps: Record<string, { dependencies: string[] }> = {
|
||||
web: { dependencies: ["convex"] },
|
||||
native: { dependencies: ["convex"] },
|
||||
};
|
||||
|
||||
if (frontend.includes("tanstack-start")) {
|
||||
deps.web.dependencies.push("@convex-dev/react-query");
|
||||
}
|
||||
if (frontend.includes("svelte")) {
|
||||
deps.web.dependencies.push("convex-svelte");
|
||||
}
|
||||
if (frontend.includes("nuxt")) {
|
||||
deps.web.dependencies.push("convex-nuxt", "convex-vue");
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
export async function setupApi(config: ProjectConfig) {
|
||||
const { api, projectName, frontend, backend, packageManager, projectDir } =
|
||||
config;
|
||||
const isConvex = backend === "convex";
|
||||
|
||||
const webDir = path.join(projectDir, "apps/web");
|
||||
const nativeDir = path.join(projectDir, "apps/native");
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
|
||||
const webDirExists = await fs.pathExists(webDir);
|
||||
const nativeDirExists = await fs.pathExists(nativeDir);
|
||||
const serverDirExists = await fs.pathExists(serverDir);
|
||||
|
||||
const frontendType = getFrontendType(frontend);
|
||||
|
||||
if (!isConvex && api !== "none") {
|
||||
const apiDeps = getApiDependencies(api, frontendType);
|
||||
|
||||
if (serverDirExists && apiDeps.server) {
|
||||
await addPackageDependency({
|
||||
dependencies: apiDeps.server.dependencies as AvailableDependencies[],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
|
||||
if (api === "trpc") {
|
||||
if (backend === "hono") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@hono/trpc-server"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
} else if (backend === "elysia") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@elysiajs/trpc"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (webDirExists && apiDeps.web) {
|
||||
await addPackageDependency({
|
||||
dependencies: apiDeps.web.dependencies as AvailableDependencies[],
|
||||
devDependencies: apiDeps.web.devDependencies as AvailableDependencies[],
|
||||
projectDir: webDir,
|
||||
});
|
||||
}
|
||||
|
||||
if (nativeDirExists && apiDeps.native) {
|
||||
await addPackageDependency({
|
||||
dependencies: apiDeps.native.dependencies as AvailableDependencies[],
|
||||
projectDir: nativeDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!isConvex) {
|
||||
const queryDeps = getQueryDependencies(frontend);
|
||||
|
||||
if (webDirExists && queryDeps.web) {
|
||||
await addPackageDependency({
|
||||
dependencies: queryDeps.web.dependencies as AvailableDependencies[],
|
||||
devDependencies: queryDeps.web
|
||||
.devDependencies as AvailableDependencies[],
|
||||
projectDir: webDir,
|
||||
});
|
||||
}
|
||||
|
||||
if (nativeDirExists && queryDeps.native) {
|
||||
await addPackageDependency({
|
||||
dependencies: queryDeps.native.dependencies as AvailableDependencies[],
|
||||
projectDir: nativeDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isConvex) {
|
||||
const convexDeps = getConvexDependencies(frontend);
|
||||
|
||||
if (webDirExists) {
|
||||
await addPackageDependency({
|
||||
dependencies: convexDeps.web.dependencies as AvailableDependencies[],
|
||||
projectDir: webDir,
|
||||
});
|
||||
}
|
||||
|
||||
if (nativeDirExists) {
|
||||
await addPackageDependency({
|
||||
dependencies: convexDeps.native.dependencies as AvailableDependencies[],
|
||||
projectDir: nativeDir,
|
||||
});
|
||||
}
|
||||
|
||||
const backendPackageName = `@${projectName}/backend`;
|
||||
const backendWorkspaceVersion =
|
||||
packageManager === "npm" ? "*" : "workspace:*";
|
||||
|
||||
if (webDirExists) {
|
||||
await addBackendWorkspaceDependency(
|
||||
webDir,
|
||||
backendPackageName,
|
||||
backendWorkspaceVersion,
|
||||
);
|
||||
}
|
||||
|
||||
if (nativeDirExists) {
|
||||
await addBackendWorkspaceDependency(
|
||||
nativeDir,
|
||||
backendPackageName,
|
||||
backendWorkspaceVersion,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,11 @@ import { intro, log, outro } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../../constants";
|
||||
import { getDefaultConfig } from "../../constants";
|
||||
import { getAddonsToAdd } from "../../prompts/addons";
|
||||
import { gatherConfig } from "../../prompts/config-prompts";
|
||||
import { getProjectName } from "../../prompts/project-name";
|
||||
import { getServerDeploymentToAdd } from "../../prompts/server-deploy";
|
||||
import { getDeploymentToAdd } from "../../prompts/web-deploy";
|
||||
import type {
|
||||
AddInput,
|
||||
@@ -16,6 +17,7 @@ import type {
|
||||
ProjectConfig,
|
||||
} from "../../types";
|
||||
import { trackProjectCreation } from "../../utils/analytics";
|
||||
import { coerceBackendPresets } from "../../utils/compatibility-rules";
|
||||
import { displayConfig } from "../../utils/display-config";
|
||||
import { exitWithError, handleError } from "../../utils/errors";
|
||||
import { generateReproducibleCommand } from "../../utils/generate-reproducible-command";
|
||||
@@ -24,7 +26,12 @@ import {
|
||||
setupProjectDirectory,
|
||||
} from "../../utils/project-directory";
|
||||
import { renderTitle } from "../../utils/render-title";
|
||||
import { getProvidedFlags, processAndValidateFlags } from "../../validation";
|
||||
import {
|
||||
getProvidedFlags,
|
||||
processAndValidateFlags,
|
||||
processProvidedFlagsWithoutValidation,
|
||||
validateConfigCompatibility,
|
||||
} from "../../validation";
|
||||
import { addAddonsToProject } from "./add-addons";
|
||||
import { addDeploymentToProject } from "./add-deployment";
|
||||
import { createProject } from "./create-project";
|
||||
@@ -50,13 +57,14 @@ export async function createProjectHandler(
|
||||
if (input.yes && input.projectName) {
|
||||
currentPathInput = input.projectName;
|
||||
} else if (input.yes) {
|
||||
let defaultName = DEFAULT_CONFIG.relativePath;
|
||||
const defaultConfig = getDefaultConfig();
|
||||
let defaultName = defaultConfig.relativePath;
|
||||
let counter = 1;
|
||||
while (
|
||||
fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) &&
|
||||
fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0
|
||||
(await fs.pathExists(path.resolve(process.cwd(), defaultName))) &&
|
||||
(await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0
|
||||
) {
|
||||
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
||||
defaultName = `${defaultConfig.projectName}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
currentPathInput = defaultName;
|
||||
@@ -102,6 +110,7 @@ export async function createProjectHandler(
|
||||
dbSetup: "none",
|
||||
api: "none",
|
||||
webDeploy: "none",
|
||||
serverDeploy: "none",
|
||||
} satisfies ProjectConfig,
|
||||
reproducibleCommand: "",
|
||||
timeScaffolded,
|
||||
@@ -124,29 +133,25 @@ export async function createProjectHandler(
|
||||
|
||||
const providedFlags = getProvidedFlags(cliInput);
|
||||
|
||||
const flagConfig = processAndValidateFlags(
|
||||
cliInput,
|
||||
providedFlags,
|
||||
finalBaseName,
|
||||
);
|
||||
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
||||
|
||||
if (!input.yes && Object.keys(otherFlags).length > 0) {
|
||||
log.info(pc.yellow("Using these pre-selected options:"));
|
||||
log.message(displayConfig(otherFlags));
|
||||
log.message("");
|
||||
}
|
||||
|
||||
let config: ProjectConfig;
|
||||
if (input.yes) {
|
||||
const flagConfig = processProvidedFlagsWithoutValidation(
|
||||
cliInput,
|
||||
finalBaseName,
|
||||
);
|
||||
|
||||
config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...getDefaultConfig(),
|
||||
...flagConfig,
|
||||
projectName: finalBaseName,
|
||||
projectDir: finalResolvedPath,
|
||||
relativePath: finalPathInput,
|
||||
};
|
||||
|
||||
coerceBackendPresets(config);
|
||||
|
||||
validateConfigCompatibility(config, providedFlags, cliInput);
|
||||
|
||||
if (config.backend === "convex") {
|
||||
log.info(
|
||||
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
|
||||
@@ -161,6 +166,19 @@ export async function createProjectHandler(
|
||||
log.message(displayConfig(config));
|
||||
log.message("");
|
||||
} else {
|
||||
const flagConfig = processAndValidateFlags(
|
||||
cliInput,
|
||||
providedFlags,
|
||||
finalBaseName,
|
||||
);
|
||||
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
||||
|
||||
if (Object.keys(otherFlags).length > 0) {
|
||||
log.info(pc.yellow("Using these pre-selected options:"));
|
||||
log.message(displayConfig(otherFlags));
|
||||
log.message("");
|
||||
}
|
||||
|
||||
config = await gatherConfig(
|
||||
flagConfig,
|
||||
finalBaseName,
|
||||
@@ -207,11 +225,11 @@ async function handleDirectoryConflictProgrammatically(
|
||||
): Promise<{ finalPathInput: string; shouldClearDirectory: boolean }> {
|
||||
const currentPath = path.resolve(process.cwd(), currentPathInput);
|
||||
|
||||
if (!fs.pathExistsSync(currentPath)) {
|
||||
if (!(await fs.pathExists(currentPath))) {
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
}
|
||||
|
||||
const dirContents = fs.readdirSync(currentPath);
|
||||
const dirContents = await fs.readdir(currentPath);
|
||||
const isNotEmpty = dirContents.length > 0;
|
||||
|
||||
if (!isNotEmpty) {
|
||||
@@ -231,8 +249,9 @@ async function handleDirectoryConflictProgrammatically(
|
||||
let finalPathInput = `${baseName}-${counter}`;
|
||||
|
||||
while (
|
||||
fs.pathExistsSync(path.resolve(process.cwd(), finalPathInput)) &&
|
||||
fs.readdirSync(path.resolve(process.cwd(), finalPathInput)).length > 0
|
||||
(await fs.pathExists(path.resolve(process.cwd(), finalPathInput))) &&
|
||||
(await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length >
|
||||
0
|
||||
) {
|
||||
counter++;
|
||||
finalPathInput = `${baseName}-${counter}`;
|
||||
@@ -284,6 +303,17 @@ export async function addAddonsHandler(input: AddInput) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!input.serverDeploy) {
|
||||
const serverDeploymentPrompt = await getServerDeploymentToAdd(
|
||||
detectedConfig.runtime,
|
||||
detectedConfig.serverDeploy,
|
||||
);
|
||||
|
||||
if (serverDeploymentPrompt !== "none") {
|
||||
input.serverDeploy = serverDeploymentPrompt;
|
||||
}
|
||||
}
|
||||
|
||||
const packageManager =
|
||||
input.packageManager || detectedConfig.packageManager || "npm";
|
||||
|
||||
@@ -309,6 +339,16 @@ export async function addAddonsHandler(input: AddInput) {
|
||||
somethingAdded = true;
|
||||
}
|
||||
|
||||
if (input.serverDeploy && input.serverDeploy !== "none") {
|
||||
await addDeploymentToProject({
|
||||
...input,
|
||||
install: false,
|
||||
suppressInstallMessage: true,
|
||||
serverDeploy: input.serverDeploy,
|
||||
});
|
||||
somethingAdded = true;
|
||||
}
|
||||
|
||||
if (!somethingAdded) {
|
||||
outro(pc.yellow("No addons or deployment configurations to add."));
|
||||
return;
|
||||
13
apps/cli/src/helpers/core/convex-codegen.ts
Normal file
13
apps/cli/src/helpers/core/convex-codegen.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import path from "node:path";
|
||||
import { execa } from "execa";
|
||||
import type { PackageManager } from "../../types";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
|
||||
export async function runConvexCodegen(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager | null | undefined,
|
||||
) {
|
||||
const backendDir = path.join(projectDir, "packages/backend");
|
||||
const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
|
||||
await execa(cmd, { cwd: backendDir, shell: true });
|
||||
}
|
||||
@@ -3,17 +3,17 @@ import fs from "fs-extra";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { writeBtsConfig } from "../../utils/bts-config";
|
||||
import { exitWithError } from "../../utils/errors";
|
||||
import { setupAddons } from "../setup/addons-setup";
|
||||
import { setupApi } from "../setup/api-setup";
|
||||
import { setupAuth } from "../setup/auth-setup";
|
||||
import { setupBackendDependencies } from "../setup/backend-setup";
|
||||
import { setupDatabase } from "../setup/db-setup";
|
||||
import { setupExamples } from "../setup/examples-setup";
|
||||
import {
|
||||
generateCloudflareWorkerTypes,
|
||||
setupRuntime,
|
||||
} from "../setup/runtime-setup";
|
||||
import { setupWebDeploy } from "../setup/web-deploy-setup";
|
||||
import { formatProjectWithBiome } from "../../utils/format-with-biome";
|
||||
import { setupAddons } from "../addons/addons-setup";
|
||||
import { setupAuth } from "../addons/auth-setup";
|
||||
import { setupExamples } from "../addons/examples-setup";
|
||||
import { setupApi } from "../core/api-setup";
|
||||
import { setupBackendDependencies } from "../core/backend-setup";
|
||||
import { setupDatabase } from "../core/db-setup";
|
||||
import { setupRuntime } from "../core/runtime-setup";
|
||||
import { setupServerDeploy } from "../deployment/server-deploy-setup";
|
||||
import { setupWebDeploy } from "../deployment/web-deploy-setup";
|
||||
import { runConvexCodegen } from "./convex-codegen";
|
||||
import { createReadme } from "./create-readme";
|
||||
import { setupEnvironmentVariables } from "./env-setup";
|
||||
import { initializeGit } from "./git";
|
||||
@@ -77,6 +77,7 @@ export async function createProject(options: ProjectConfig) {
|
||||
await handleExtras(projectDir, options);
|
||||
|
||||
await setupWebDeploy(options);
|
||||
await setupServerDeploy(options);
|
||||
|
||||
await setupEnvironmentVariables(options);
|
||||
await updatePackageConfigurations(projectDir, options);
|
||||
@@ -84,6 +85,12 @@ export async function createProject(options: ProjectConfig) {
|
||||
|
||||
await writeBtsConfig(options);
|
||||
|
||||
await formatProjectWithBiome(projectDir);
|
||||
|
||||
if (isConvex) {
|
||||
await runConvexCodegen(projectDir, options.packageManager);
|
||||
}
|
||||
|
||||
log.success("Project template successfully scaffolded!");
|
||||
|
||||
if (options.install) {
|
||||
@@ -91,7 +98,6 @@ export async function createProject(options: ProjectConfig) {
|
||||
projectDir,
|
||||
packageManager: options.packageManager,
|
||||
});
|
||||
await generateCloudflareWorkerTypes(options);
|
||||
}
|
||||
|
||||
await initializeGit(projectDir, options.git);
|
||||
@@ -24,6 +24,7 @@ export async function detectProjectConfig(
|
||||
dbSetup: btsConfig.dbSetup,
|
||||
api: btsConfig.api,
|
||||
webDeploy: btsConfig.webDeploy,
|
||||
serverDeploy: btsConfig.serverDeploy,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { generateAuthSecret } from "../setup/auth-setup";
|
||||
import { generateAuthSecret } from "../addons/auth-setup";
|
||||
|
||||
export interface EnvVariable {
|
||||
key: string;
|
||||
@@ -85,8 +85,17 @@ export async function addEnvVariablesToFile(
|
||||
}
|
||||
|
||||
export async function setupEnvironmentVariables(config: ProjectConfig) {
|
||||
const { backend, frontend, database, auth, examples, dbSetup, projectDir } =
|
||||
config;
|
||||
const {
|
||||
backend,
|
||||
frontend,
|
||||
database,
|
||||
auth,
|
||||
examples,
|
||||
dbSetup,
|
||||
projectDir,
|
||||
webDeploy,
|
||||
serverDeploy,
|
||||
} = config;
|
||||
|
||||
const hasReactRouter = frontend.includes("react-router");
|
||||
const hasTanStackRouter = frontend.includes("tanstack-router");
|
||||
@@ -239,10 +248,51 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
|
||||
|
||||
await addEnvVariablesToFile(envPath, serverVars);
|
||||
|
||||
if (config.runtime === "workers") {
|
||||
const devVarsPath = path.join(serverDir, ".dev.vars");
|
||||
try {
|
||||
await fs.copy(envPath, devVarsPath);
|
||||
} catch (_err) {}
|
||||
const isUnifiedAlchemy =
|
||||
webDeploy === "alchemy" && serverDeploy === "alchemy";
|
||||
const isIndividualAlchemy =
|
||||
webDeploy === "alchemy" || serverDeploy === "alchemy";
|
||||
|
||||
if (isUnifiedAlchemy) {
|
||||
const rootEnvPath = path.join(projectDir, ".env");
|
||||
const rootAlchemyVars: EnvVariable[] = [
|
||||
{
|
||||
key: "ALCHEMY_PASSWORD",
|
||||
value: "please-change-this",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
|
||||
} else if (isIndividualAlchemy) {
|
||||
if (webDeploy === "alchemy") {
|
||||
const webDir = path.join(projectDir, "apps/web");
|
||||
if (await fs.pathExists(webDir)) {
|
||||
const webAlchemyVars: EnvVariable[] = [
|
||||
{
|
||||
key: "ALCHEMY_PASSWORD",
|
||||
value: "please-change-this",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
|
||||
}
|
||||
}
|
||||
|
||||
if (serverDeploy === "alchemy") {
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
if (await fs.pathExists(serverDir)) {
|
||||
const serverAlchemyVars: EnvVariable[] = [
|
||||
{
|
||||
key: "ALCHEMY_PASSWORD",
|
||||
value: "please-change-this",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(
|
||||
path.join(serverDir, ".env"),
|
||||
serverAlchemyVars,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export async function displayPostInstallInstructions(
|
||||
backend,
|
||||
dbSetup,
|
||||
webDeploy,
|
||||
serverDeploy,
|
||||
} = config;
|
||||
|
||||
const isConvex = backend === "convex";
|
||||
@@ -35,7 +36,14 @@ export async function displayPostInstallInstructions(
|
||||
|
||||
const databaseInstructions =
|
||||
!isConvex && database !== "none"
|
||||
? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
||||
? await getDatabaseInstructions(
|
||||
database,
|
||||
orm,
|
||||
runCmd,
|
||||
runtime,
|
||||
dbSetup,
|
||||
serverDeploy,
|
||||
)
|
||||
: "";
|
||||
|
||||
const tauriInstructions = addons?.includes("tauri")
|
||||
@@ -56,8 +64,16 @@ export async function displayPostInstallInstructions(
|
||||
const starlightInstructions = addons?.includes("starlight")
|
||||
? getStarlightInstructions(runCmd)
|
||||
: "";
|
||||
const workersDeployInstructions =
|
||||
webDeploy === "workers" ? getWorkersDeployInstructions(runCmd) : "";
|
||||
const wranglerDeployInstructions = getWranglerDeployInstructions(
|
||||
runCmd,
|
||||
webDeploy,
|
||||
serverDeploy,
|
||||
);
|
||||
const alchemyDeployInstructions = getAlchemyDeployInstructions(
|
||||
runCmd,
|
||||
webDeploy,
|
||||
serverDeploy,
|
||||
);
|
||||
|
||||
const hasWeb = frontend?.some((f) =>
|
||||
[
|
||||
@@ -116,11 +132,9 @@ export async function displayPostInstallInstructions(
|
||||
)} Complete D1 database setup first\n (see Database commands below)\n`;
|
||||
}
|
||||
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
||||
output += `${pc.cyan(
|
||||
`${stepCounter++}.`,
|
||||
)} cd apps/server && ${runCmd} run cf-typegen\n\n`;
|
||||
} else {
|
||||
output += "\n";
|
||||
if (serverDeploy === "wrangler") {
|
||||
output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +165,10 @@ export async function displayPostInstallInstructions(
|
||||
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
||||
if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
|
||||
if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
|
||||
if (workersDeployInstructions)
|
||||
output += `\n${workersDeployInstructions.trim()}\n`;
|
||||
if (wranglerDeployInstructions)
|
||||
output += `\n${wranglerDeployInstructions.trim()}\n`;
|
||||
if (alchemyDeployInstructions)
|
||||
output += `\n${alchemyDeployInstructions.trim()}\n`;
|
||||
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
||||
|
||||
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
||||
@@ -202,10 +218,11 @@ async function getDatabaseInstructions(
|
||||
database: Database,
|
||||
orm?: ORM,
|
||||
runCmd?: string,
|
||||
runtime?: Runtime,
|
||||
_runtime?: Runtime,
|
||||
dbSetup?: DatabaseSetup,
|
||||
serverDeploy?: string,
|
||||
): Promise<string> {
|
||||
const instructions = [];
|
||||
const instructions: string[] = [];
|
||||
|
||||
if (dbSetup === "docker") {
|
||||
const dockerStatus = await getDockerStatus(database);
|
||||
@@ -216,7 +233,7 @@ async function getDatabaseInstructions(
|
||||
}
|
||||
}
|
||||
|
||||
if (runtime === "workers" && dbSetup === "d1") {
|
||||
if (serverDeploy === "wrangler" && dbSetup === "d1") {
|
||||
const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
|
||||
|
||||
instructions.push(
|
||||
@@ -249,7 +266,9 @@ async function getDatabaseInstructions(
|
||||
`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`,
|
||||
)}`,
|
||||
);
|
||||
instructions.push("");
|
||||
}
|
||||
|
||||
if (dbSetup === "d1" && serverDeploy === "alchemy") {
|
||||
}
|
||||
|
||||
if (orm === "prisma") {
|
||||
@@ -281,7 +300,9 @@ async function getDatabaseInstructions(
|
||||
`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`,
|
||||
);
|
||||
}
|
||||
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
||||
if (dbSetup !== "d1") {
|
||||
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
||||
}
|
||||
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
||||
if (database === "sqlite" && dbSetup !== "d1") {
|
||||
instructions.push(
|
||||
@@ -343,6 +364,47 @@ function getBunWebNativeWarning(): string {
|
||||
)} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
|
||||
}
|
||||
|
||||
function getWorkersDeployInstructions(runCmd?: string): string {
|
||||
return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`;
|
||||
function getWranglerDeployInstructions(
|
||||
runCmd?: string,
|
||||
webDeploy?: string,
|
||||
serverDeploy?: string,
|
||||
): string {
|
||||
const instructions: string[] = [];
|
||||
|
||||
if (webDeploy === "wrangler") {
|
||||
instructions.push(
|
||||
`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`,
|
||||
);
|
||||
}
|
||||
if (serverDeploy === "wrangler") {
|
||||
instructions.push(
|
||||
`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`,
|
||||
);
|
||||
}
|
||||
|
||||
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
||||
}
|
||||
|
||||
function getAlchemyDeployInstructions(
|
||||
runCmd?: string,
|
||||
webDeploy?: string,
|
||||
serverDeploy?: string,
|
||||
): string {
|
||||
const instructions: string[] = [];
|
||||
|
||||
if (webDeploy === "alchemy" && serverDeploy !== "alchemy") {
|
||||
instructions.push(
|
||||
`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`,
|
||||
);
|
||||
} else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") {
|
||||
instructions.push(
|
||||
`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`,
|
||||
);
|
||||
} else if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
|
||||
instructions.push(
|
||||
`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`,
|
||||
);
|
||||
}
|
||||
|
||||
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { spinner } from "@clack/prompts";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { Backend, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
@@ -23,43 +20,6 @@ export async function setupRuntime(config: ProjectConfig) {
|
||||
await setupBunRuntime(serverDir, backend);
|
||||
} else if (runtime === "node") {
|
||||
await setupNodeRuntime(serverDir, backend);
|
||||
} else if (runtime === "workers") {
|
||||
await setupWorkersRuntime(serverDir);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateCloudflareWorkerTypes(config: ProjectConfig) {
|
||||
if (config.runtime !== "workers") {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverDir = path.join(config.projectDir, "apps/server");
|
||||
|
||||
if (!(await fs.pathExists(serverDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const s = spinner();
|
||||
|
||||
try {
|
||||
s.start("Generating Cloudflare Workers types...");
|
||||
|
||||
const runCmd =
|
||||
config.packageManager === "npm" ? "npm" : config.packageManager;
|
||||
await execa(runCmd, ["run", "cf-typegen"], {
|
||||
cwd: serverDir,
|
||||
});
|
||||
|
||||
s.stop("Cloudflare Workers types generated successfully!");
|
||||
} catch {
|
||||
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
|
||||
const managerCmd =
|
||||
config.packageManager === "npm"
|
||||
? "npm run"
|
||||
: `${config.packageManager} run`;
|
||||
console.warn(
|
||||
`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,26 +74,3 @@ async function setupNodeRuntime(serverDir: string, backend: Backend) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function setupWorkersRuntime(serverDir: string) {
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
if (!(await fs.pathExists(packageJsonPath))) return;
|
||||
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
dev: "wrangler dev --port=3000",
|
||||
start: "wrangler dev",
|
||||
deploy: "wrangler deploy",
|
||||
build: "wrangler deploy --dry-run",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareBindings",
|
||||
};
|
||||
|
||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["wrangler", "@types/node"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { globby } from "globby";
|
||||
import { glob } from "tinyglobby";
|
||||
import { PKG_ROOT } from "../../constants";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { processTemplate } from "../../utils/template-processor";
|
||||
@@ -13,7 +13,7 @@ export async function processAndCopyFiles(
|
||||
overwrite = true,
|
||||
ignorePatterns?: string[],
|
||||
) {
|
||||
const sourceFiles = await globby(sourcePattern, {
|
||||
const sourceFiles = await glob(sourcePattern, {
|
||||
cwd: baseSourceDir,
|
||||
dot: true,
|
||||
onlyFiles: true,
|
||||
@@ -788,19 +788,6 @@ export async function handleExtras(projectDir: string, context: ProjectConfig) {
|
||||
await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.runtime === "workers") {
|
||||
const runtimeWorkersDir = path.join(PKG_ROOT, "templates/runtime/workers");
|
||||
if (await fs.pathExists(runtimeWorkersDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
runtimeWorkersDir,
|
||||
projectDir,
|
||||
context,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupDockerComposeTemplates(
|
||||
@@ -827,43 +814,137 @@ export async function setupDeploymentTemplates(
|
||||
projectDir: string,
|
||||
context: ProjectConfig,
|
||||
) {
|
||||
if (context.webDeploy === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.webDeploy === "workers") {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frontends = context.frontend;
|
||||
|
||||
const templateMap: Record<string, string> = {
|
||||
"tanstack-router": "react/tanstack-router",
|
||||
"tanstack-start": "react/tanstack-start",
|
||||
"react-router": "react/react-router",
|
||||
solid: "solid",
|
||||
next: "react/next",
|
||||
nuxt: "nuxt",
|
||||
svelte: "svelte",
|
||||
};
|
||||
|
||||
for (const f of frontends) {
|
||||
if (templateMap[f]) {
|
||||
const deployTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/deploy/web/${templateMap[f]}`,
|
||||
if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") {
|
||||
if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
|
||||
const alchemyTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/deploy/alchemy",
|
||||
);
|
||||
if (await fs.pathExists(alchemyTemplateSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"alchemy.run.ts.hbs",
|
||||
alchemyTemplateSrc,
|
||||
projectDir,
|
||||
context,
|
||||
);
|
||||
if (await fs.pathExists(deployTemplateSrc)) {
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
if (await fs.pathExists(serverAppDir)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
deployTemplateSrc,
|
||||
"env.d.ts.hbs",
|
||||
alchemyTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
await processAndCopyFiles(
|
||||
"wrangler.jsonc.hbs",
|
||||
alchemyTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (context.webDeploy === "alchemy") {
|
||||
const alchemyTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/deploy/alchemy",
|
||||
);
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (
|
||||
(await fs.pathExists(alchemyTemplateSrc)) &&
|
||||
(await fs.pathExists(webAppDir))
|
||||
) {
|
||||
await processAndCopyFiles(
|
||||
"alchemy.run.ts.hbs",
|
||||
alchemyTemplateSrc,
|
||||
webAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.serverDeploy === "alchemy") {
|
||||
const alchemyTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
"templates/deploy/alchemy",
|
||||
);
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
if (
|
||||
(await fs.pathExists(alchemyTemplateSrc)) &&
|
||||
(await fs.pathExists(serverAppDir))
|
||||
) {
|
||||
await processAndCopyFiles(
|
||||
"alchemy.run.ts.hbs",
|
||||
alchemyTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
await processAndCopyFiles(
|
||||
"env.d.ts.hbs",
|
||||
alchemyTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
await processAndCopyFiles(
|
||||
"wrangler.jsonc.hbs",
|
||||
alchemyTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (await fs.pathExists(webAppDir)) {
|
||||
const frontends = context.frontend;
|
||||
|
||||
const templateMap: Record<string, string> = {
|
||||
"tanstack-router": "react/tanstack-router",
|
||||
"tanstack-start": "react/tanstack-start",
|
||||
"react-router": "react/react-router",
|
||||
solid: "solid",
|
||||
next: "react/next",
|
||||
nuxt: "nuxt",
|
||||
svelte: "svelte",
|
||||
};
|
||||
|
||||
for (const f of frontends) {
|
||||
if (templateMap[f]) {
|
||||
const deployTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/deploy/${context.webDeploy}/web/${templateMap[f]}`,
|
||||
);
|
||||
if (await fs.pathExists(deployTemplateSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
deployTemplateSrc,
|
||||
webAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
|
||||
const serverAppDir = path.join(projectDir, "apps/server");
|
||||
if (await fs.pathExists(serverAppDir)) {
|
||||
const deployTemplateSrc = path.join(
|
||||
PKG_ROOT,
|
||||
`templates/deploy/${context.serverDeploy}/server`,
|
||||
);
|
||||
if (await fs.pathExists(deployTemplateSrc)) {
|
||||
await processAndCopyFiles(
|
||||
"**/*",
|
||||
deployTemplateSrc,
|
||||
serverAppDir,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,33 @@
|
||||
import path from "node:path";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
type EnvVariable,
|
||||
} from "../project-generation/env-setup";
|
||||
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||
|
||||
export async function setupCloudflareD1(config: ProjectConfig) {
|
||||
const { projectDir } = config;
|
||||
const { projectDir, serverDeploy } = config;
|
||||
|
||||
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||
if (serverDeploy === "wrangler") {
|
||||
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||
|
||||
const variables: EnvVariable[] = [
|
||||
{
|
||||
key: "CLOUDFLARE_ACCOUNT_ID",
|
||||
value: "",
|
||||
condition: true,
|
||||
},
|
||||
{
|
||||
key: "CLOUDFLARE_DATABASE_ID",
|
||||
value: "",
|
||||
condition: true,
|
||||
},
|
||||
{
|
||||
key: "CLOUDFLARE_D1_TOKEN",
|
||||
value: "",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
const variables: EnvVariable[] = [
|
||||
{
|
||||
key: "CLOUDFLARE_ACCOUNT_ID",
|
||||
value: "",
|
||||
condition: true,
|
||||
},
|
||||
{
|
||||
key: "CLOUDFLARE_DATABASE_ID",
|
||||
value: "",
|
||||
condition: true,
|
||||
},
|
||||
{
|
||||
key: "CLOUDFLARE_D1_TOKEN",
|
||||
value: "",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
await addEnvVariablesToFile(envPath, variables);
|
||||
} catch (_err) {}
|
||||
try {
|
||||
await addEnvVariablesToFile(envPath, variables);
|
||||
} catch (_err) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import path from "node:path";
|
||||
import type { Database, ProjectConfig } from "../../types";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
type EnvVariable,
|
||||
} from "../project-generation/env-setup";
|
||||
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||
|
||||
export async function setupDockerCompose(config: ProjectConfig) {
|
||||
const { database, projectDir, projectName } = config;
|
||||
|
||||
@@ -6,10 +6,7 @@ import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { commandExists } from "../../utils/command-exists";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
type EnvVariable,
|
||||
} from "../project-generation/env-setup";
|
||||
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||
|
||||
type MongoDBConfig = {
|
||||
connectionString: string;
|
||||
|
||||
@@ -7,10 +7,7 @@ import pc from "picocolors";
|
||||
import type { PackageManager, ProjectConfig } from "../../types";
|
||||
import { exitCancelled } from "../../utils/errors";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
type EnvVariable,
|
||||
} from "../project-generation/env-setup";
|
||||
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||
|
||||
type NeonConfig = {
|
||||
connectionString: string;
|
||||
|
||||
@@ -8,10 +8,7 @@ import type { ORM, PackageManager, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { exitCancelled } from "../../utils/errors";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
type EnvVariable,
|
||||
} from "../project-generation/env-setup";
|
||||
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||
|
||||
type PrismaConfig = {
|
||||
databaseUrl: string;
|
||||
@@ -253,9 +250,8 @@ export async function setupPrismaPostgres(config: ProjectConfig) {
|
||||
if (prismaConfig) {
|
||||
await writeEnvFile(projectDir, prismaConfig);
|
||||
|
||||
await addDotenvImportToPrismaConfig(projectDir);
|
||||
|
||||
if (orm === "prisma") {
|
||||
await addDotenvImportToPrismaConfig(projectDir);
|
||||
await addPrismaAccelerateExtension(serverDir);
|
||||
}
|
||||
log.success(
|
||||
|
||||
@@ -6,10 +6,7 @@ import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager, ProjectConfig } from "../../types";
|
||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
type EnvVariable,
|
||||
} from "../project-generation/env-setup";
|
||||
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||
|
||||
async function writeSupabaseEnvFile(projectDir: string, databaseUrl: string) {
|
||||
try {
|
||||
|
||||
@@ -7,10 +7,7 @@ import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { commandExists } from "../../utils/command-exists";
|
||||
import { exitCancelled } from "../../utils/errors";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
type EnvVariable,
|
||||
} from "../project-generation/env-setup";
|
||||
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
|
||||
|
||||
type TursoConfig = {
|
||||
dbUrl: string;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { PackageManager, ProjectConfig } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
import { setupAlchemyServerDeploy } from "../server-deploy-setup";
|
||||
import { setupNextAlchemyDeploy } from "./alchemy-next-setup";
|
||||
import { setupNuxtAlchemyDeploy } from "./alchemy-nuxt-setup";
|
||||
import { setupReactRouterAlchemyDeploy } from "./alchemy-react-router-setup";
|
||||
import { setupSolidAlchemyDeploy } from "./alchemy-solid-setup";
|
||||
import { setupSvelteAlchemyDeploy } from "./alchemy-svelte-setup";
|
||||
import { setupTanStackRouterAlchemyDeploy } from "./alchemy-tanstack-router-setup";
|
||||
import { setupTanStackStartAlchemyDeploy } from "./alchemy-tanstack-start-setup";
|
||||
|
||||
export async function setupCombinedAlchemyDeploy(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager,
|
||||
config: ProjectConfig,
|
||||
) {
|
||||
await addPackageDependency({
|
||||
devDependencies: ["alchemy", "dotenv"],
|
||||
projectDir,
|
||||
});
|
||||
|
||||
const rootPkgPath = path.join(projectDir, "package.json");
|
||||
if (await fs.pathExists(rootPkgPath)) {
|
||||
const pkg = await fs.readJson(rootPkgPath);
|
||||
|
||||
pkg.scripts = {
|
||||
...pkg.scripts,
|
||||
deploy: "alchemy deploy",
|
||||
destroy: "alchemy destroy",
|
||||
"alchemy:dev": "alchemy dev",
|
||||
};
|
||||
await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
|
||||
}
|
||||
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
if (await fs.pathExists(serverDir)) {
|
||||
await setupAlchemyServerDeploy(serverDir, packageManager);
|
||||
}
|
||||
|
||||
const frontend = config.frontend;
|
||||
const isNext = frontend.includes("next");
|
||||
const isNuxt = frontend.includes("nuxt");
|
||||
const isSvelte = frontend.includes("svelte");
|
||||
const isTanstackRouter = frontend.includes("tanstack-router");
|
||||
const isTanstackStart = frontend.includes("tanstack-start");
|
||||
const isReactRouter = frontend.includes("react-router");
|
||||
const isSolid = frontend.includes("solid");
|
||||
|
||||
if (isNext) {
|
||||
await setupNextAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isNuxt) {
|
||||
await setupNuxtAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isSvelte) {
|
||||
await setupSvelteAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isTanstackStart) {
|
||||
await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isTanstackRouter) {
|
||||
await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isReactRouter) {
|
||||
await setupReactRouterAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isSolid) {
|
||||
await setupSolidAlchemyDeploy(projectDir, packageManager);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
|
||||
export async function setupNextAlchemyDeploy(
|
||||
projectDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) return;
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["alchemy", "dotenv"],
|
||||
projectDir: webAppDir,
|
||||
});
|
||||
|
||||
const pkgPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(pkgPath)) {
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
|
||||
pkg.scripts = {
|
||||
...pkg.scripts,
|
||||
deploy: "alchemy deploy",
|
||||
destroy: "alchemy destroy",
|
||||
"alchemy:dev": "alchemy dev",
|
||||
};
|
||||
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
104
apps/cli/src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
Normal file
104
apps/cli/src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { IndentationText, Node, Project, QuoteKind } from "ts-morph";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
|
||||
export async function setupNuxtAlchemyDeploy(
|
||||
projectDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) return;
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["alchemy", "nitro-cloudflare-dev", "dotenv"],
|
||||
projectDir: webAppDir,
|
||||
});
|
||||
|
||||
const pkgPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(pkgPath)) {
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
|
||||
pkg.scripts = {
|
||||
...pkg.scripts,
|
||||
deploy: "alchemy deploy",
|
||||
destroy: "alchemy destroy",
|
||||
"alchemy:dev": "alchemy dev",
|
||||
};
|
||||
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
||||
}
|
||||
|
||||
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
||||
if (!(await fs.pathExists(nuxtConfigPath))) return;
|
||||
|
||||
try {
|
||||
const project = new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
quoteKind: QuoteKind.Double,
|
||||
},
|
||||
});
|
||||
|
||||
project.addSourceFileAtPath(nuxtConfigPath);
|
||||
const sourceFile = project.getSourceFileOrThrow(nuxtConfigPath);
|
||||
|
||||
const exportAssignment = sourceFile.getExportAssignment(
|
||||
(d) => !d.isExportEquals(),
|
||||
);
|
||||
if (!exportAssignment) return;
|
||||
|
||||
const defineConfigCall = exportAssignment.getExpression();
|
||||
if (
|
||||
!Node.isCallExpression(defineConfigCall) ||
|
||||
defineConfigCall.getExpression().getText() !== "defineNuxtConfig"
|
||||
)
|
||||
return;
|
||||
|
||||
let configObject = defineConfigCall.getArguments()[0];
|
||||
if (!configObject) {
|
||||
configObject = defineConfigCall.addArgument("{}");
|
||||
}
|
||||
|
||||
if (Node.isObjectLiteralExpression(configObject)) {
|
||||
if (!configObject.getProperty("nitro")) {
|
||||
configObject.addPropertyAssignment({
|
||||
name: "nitro",
|
||||
initializer: `{
|
||||
preset: "cloudflare_module",
|
||||
cloudflare: {
|
||||
deployConfig: true,
|
||||
nodeCompat: true
|
||||
}
|
||||
}`,
|
||||
});
|
||||
}
|
||||
|
||||
const modulesProperty = configObject.getProperty("modules");
|
||||
if (modulesProperty && Node.isPropertyAssignment(modulesProperty)) {
|
||||
const initializer = modulesProperty.getInitializer();
|
||||
if (Node.isArrayLiteralExpression(initializer)) {
|
||||
const hasModule = initializer
|
||||
.getElements()
|
||||
.some(
|
||||
(el) =>
|
||||
el.getText() === '"nitro-cloudflare-dev"' ||
|
||||
el.getText() === "'nitro-cloudflare-dev'",
|
||||
);
|
||||
if (!hasModule) {
|
||||
initializer.addElement('"nitro-cloudflare-dev"');
|
||||
}
|
||||
}
|
||||
} else if (!modulesProperty) {
|
||||
configObject.addPropertyAssignment({
|
||||
name: "modules",
|
||||
initializer: '["nitro-cloudflare-dev"]',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await project.save();
|
||||
} catch (error) {
|
||||
console.warn("Failed to update nuxt.config.ts:", error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { IndentationText, Node, Project, QuoteKind } from "ts-morph";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
|
||||
export async function setupReactRouterAlchemyDeploy(
|
||||
projectDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) return;
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["alchemy", "@cloudflare/vite-plugin", "dotenv"],
|
||||
projectDir: webAppDir,
|
||||
});
|
||||
|
||||
const pkgPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(pkgPath)) {
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
|
||||
pkg.scripts = {
|
||||
...pkg.scripts,
|
||||
deploy: "alchemy deploy",
|
||||
destroy: "alchemy destroy",
|
||||
"alchemy:dev": "alchemy dev",
|
||||
};
|
||||
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
||||
}
|
||||
|
||||
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
||||
if (await fs.pathExists(viteConfigPath)) {
|
||||
try {
|
||||
const project = new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
quoteKind: QuoteKind.Double,
|
||||
},
|
||||
});
|
||||
|
||||
project.addSourceFileAtPath(viteConfigPath);
|
||||
const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
|
||||
|
||||
const alchemyImport = sourceFile.getImportDeclaration(
|
||||
"alchemy/cloudflare/react-router",
|
||||
);
|
||||
if (!alchemyImport) {
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: "alchemy/cloudflare/react-router",
|
||||
defaultImport: "alchemy",
|
||||
});
|
||||
}
|
||||
|
||||
const exportAssignment = sourceFile.getExportAssignment(
|
||||
(d) => !d.isExportEquals(),
|
||||
);
|
||||
if (!exportAssignment) return;
|
||||
|
||||
const defineConfigCall = exportAssignment.getExpression();
|
||||
if (
|
||||
!Node.isCallExpression(defineConfigCall) ||
|
||||
defineConfigCall.getExpression().getText() !== "defineConfig"
|
||||
)
|
||||
return;
|
||||
|
||||
let configObject = defineConfigCall.getArguments()[0];
|
||||
if (!configObject) {
|
||||
configObject = defineConfigCall.addArgument("{}");
|
||||
}
|
||||
|
||||
if (Node.isObjectLiteralExpression(configObject)) {
|
||||
const pluginsProperty = configObject.getProperty("plugins");
|
||||
if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
|
||||
const initializer = pluginsProperty.getInitializer();
|
||||
if (Node.isArrayLiteralExpression(initializer)) {
|
||||
const hasCloudflarePlugin = initializer
|
||||
.getElements()
|
||||
.some((el) => el.getText().includes("cloudflare("));
|
||||
|
||||
if (!hasCloudflarePlugin) {
|
||||
initializer.addElement("alchemy()");
|
||||
}
|
||||
}
|
||||
} else if (!pluginsProperty) {
|
||||
configObject.addPropertyAssignment({
|
||||
name: "plugins",
|
||||
initializer: "[alchemy()]",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await project.save();
|
||||
} catch (error) {
|
||||
console.warn("Failed to update vite.config.ts:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const reactRouterConfigPath = path.join(webAppDir, "react-router.config.ts");
|
||||
if (await fs.pathExists(reactRouterConfigPath)) {
|
||||
try {
|
||||
const project = new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
quoteKind: QuoteKind.Double,
|
||||
},
|
||||
});
|
||||
|
||||
project.addSourceFileAtPath(reactRouterConfigPath);
|
||||
const sourceFile = project.getSourceFileOrThrow(reactRouterConfigPath);
|
||||
|
||||
const exportAssignment = sourceFile.getExportAssignment(
|
||||
(d) => !d.isExportEquals(),
|
||||
);
|
||||
if (!exportAssignment) return;
|
||||
|
||||
const configExpression = exportAssignment.getExpression();
|
||||
let configObject: Node | undefined;
|
||||
|
||||
if (Node.isObjectLiteralExpression(configExpression)) {
|
||||
configObject = configExpression;
|
||||
} else if (Node.isSatisfiesExpression(configExpression)) {
|
||||
const expression = configExpression.getExpression();
|
||||
if (Node.isObjectLiteralExpression(expression)) {
|
||||
configObject = expression;
|
||||
}
|
||||
}
|
||||
|
||||
if (!configObject || !Node.isObjectLiteralExpression(configObject))
|
||||
return;
|
||||
|
||||
const futureProperty = configObject.getProperty("future");
|
||||
|
||||
if (!futureProperty) {
|
||||
configObject.addPropertyAssignment({
|
||||
name: "future",
|
||||
initializer: `{
|
||||
unstable_viteEnvironmentApi: true,
|
||||
}`,
|
||||
});
|
||||
} else if (Node.isPropertyAssignment(futureProperty)) {
|
||||
const futureInitializer = futureProperty.getInitializer();
|
||||
|
||||
if (Node.isObjectLiteralExpression(futureInitializer)) {
|
||||
const viteEnvApiProp = futureInitializer.getProperty(
|
||||
"unstable_viteEnvironmentApi",
|
||||
);
|
||||
|
||||
if (!viteEnvApiProp) {
|
||||
futureInitializer.addPropertyAssignment({
|
||||
name: "unstable_viteEnvironmentApi",
|
||||
initializer: "true",
|
||||
});
|
||||
} else if (Node.isPropertyAssignment(viteEnvApiProp)) {
|
||||
const value = viteEnvApiProp.getInitializer()?.getText();
|
||||
if (value === "false") {
|
||||
viteEnvApiProp.setInitializer("true");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await project.save();
|
||||
} catch (error) {
|
||||
console.warn("Failed to update react-router.config.ts:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
|
||||
export async function setupSolidAlchemyDeploy(
|
||||
projectDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) return;
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["alchemy", "dotenv"],
|
||||
projectDir: webAppDir,
|
||||
});
|
||||
|
||||
const pkgPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(pkgPath)) {
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
|
||||
pkg.scripts = {
|
||||
...pkg.scripts,
|
||||
deploy: "alchemy deploy",
|
||||
destroy: "alchemy destroy",
|
||||
"alchemy:dev": "alchemy dev",
|
||||
};
|
||||
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { IndentationText, Node, Project, QuoteKind } from "ts-morph";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
|
||||
export async function setupSvelteAlchemyDeploy(
|
||||
projectDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) return;
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["alchemy", "@sveltejs/adapter-cloudflare", "dotenv"],
|
||||
projectDir: webAppDir,
|
||||
});
|
||||
|
||||
const pkgPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(pkgPath)) {
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
|
||||
pkg.scripts = {
|
||||
...pkg.scripts,
|
||||
deploy: "alchemy deploy",
|
||||
destroy: "alchemy destroy",
|
||||
"alchemy:dev": "alchemy dev",
|
||||
};
|
||||
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
||||
}
|
||||
|
||||
const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
|
||||
if (!(await fs.pathExists(svelteConfigPath))) return;
|
||||
|
||||
try {
|
||||
const project = new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
quoteKind: QuoteKind.Single,
|
||||
},
|
||||
});
|
||||
|
||||
project.addSourceFileAtPath(svelteConfigPath);
|
||||
const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
|
||||
|
||||
const importDeclarations = sourceFile.getImportDeclarations();
|
||||
const adapterImport = importDeclarations.find((imp) =>
|
||||
imp.getModuleSpecifierValue().includes("@sveltejs/adapter"),
|
||||
);
|
||||
|
||||
if (adapterImport) {
|
||||
adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
|
||||
adapterImport.removeDefaultImport();
|
||||
adapterImport.setDefaultImport("alchemy");
|
||||
} else {
|
||||
sourceFile.insertImportDeclaration(0, {
|
||||
moduleSpecifier: "alchemy/cloudflare/sveltekit",
|
||||
defaultImport: "alchemy",
|
||||
});
|
||||
}
|
||||
|
||||
const configVariable = sourceFile.getVariableDeclaration("config");
|
||||
if (configVariable) {
|
||||
const initializer = configVariable.getInitializer();
|
||||
if (Node.isObjectLiteralExpression(initializer)) {
|
||||
updateAdapterInConfig(initializer);
|
||||
}
|
||||
}
|
||||
|
||||
await project.save();
|
||||
} catch (error) {
|
||||
console.warn("Failed to update svelte.config.js:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAdapterInConfig(configObject: Node): void {
|
||||
if (!Node.isObjectLiteralExpression(configObject)) return;
|
||||
|
||||
const kitProperty = configObject.getProperty("kit");
|
||||
if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
|
||||
const kitInitializer = kitProperty.getInitializer();
|
||||
if (Node.isObjectLiteralExpression(kitInitializer)) {
|
||||
const adapterProperty = kitInitializer.getProperty("adapter");
|
||||
if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
|
||||
const initializer = adapterProperty.getInitializer();
|
||||
if (Node.isCallExpression(initializer)) {
|
||||
const expression = initializer.getExpression();
|
||||
if (
|
||||
Node.isIdentifier(expression) &&
|
||||
expression.getText() === "adapter"
|
||||
) {
|
||||
expression.replaceWithText("alchemy");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
|
||||
export async function setupTanStackRouterAlchemyDeploy(
|
||||
projectDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) return;
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["alchemy", "dotenv"],
|
||||
projectDir: webAppDir,
|
||||
});
|
||||
|
||||
const pkgPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(pkgPath)) {
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
|
||||
pkg.scripts = {
|
||||
...pkg.scripts,
|
||||
deploy: "alchemy deploy",
|
||||
destroy: "alchemy destroy",
|
||||
"alchemy:dev": "alchemy dev",
|
||||
};
|
||||
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { IndentationText, Node, Project, QuoteKind } from "ts-morph";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
|
||||
export async function setupTanStackStartAlchemyDeploy(
|
||||
projectDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) return;
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["alchemy", "nitropack", "dotenv"],
|
||||
projectDir: webAppDir,
|
||||
});
|
||||
|
||||
const pkgPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(pkgPath)) {
|
||||
const pkg = await fs.readJson(pkgPath);
|
||||
|
||||
pkg.scripts = {
|
||||
...pkg.scripts,
|
||||
deploy: "alchemy deploy",
|
||||
destroy: "alchemy destroy",
|
||||
"alchemy:dev": "alchemy dev",
|
||||
};
|
||||
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
||||
}
|
||||
|
||||
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
||||
if (await fs.pathExists(viteConfigPath)) {
|
||||
try {
|
||||
const project = new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
quoteKind: QuoteKind.Double,
|
||||
},
|
||||
});
|
||||
|
||||
project.addSourceFileAtPath(viteConfigPath);
|
||||
const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
|
||||
|
||||
const alchemyImport = sourceFile.getImportDeclaration(
|
||||
"alchemy/cloudflare/tanstack-start",
|
||||
);
|
||||
if (!alchemyImport) {
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: "alchemy/cloudflare/tanstack-start",
|
||||
defaultImport: "alchemy",
|
||||
});
|
||||
} else {
|
||||
alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
|
||||
}
|
||||
|
||||
const exportAssignment = sourceFile.getExportAssignment(
|
||||
(d) => !d.isExportEquals(),
|
||||
);
|
||||
if (!exportAssignment) return;
|
||||
|
||||
const defineConfigCall = exportAssignment.getExpression();
|
||||
if (
|
||||
!Node.isCallExpression(defineConfigCall) ||
|
||||
defineConfigCall.getExpression().getText() !== "defineConfig"
|
||||
)
|
||||
return;
|
||||
|
||||
let configObject = defineConfigCall.getArguments()[0];
|
||||
if (!configObject) {
|
||||
configObject = defineConfigCall.addArgument("{}");
|
||||
}
|
||||
|
||||
if (Node.isObjectLiteralExpression(configObject)) {
|
||||
if (!configObject.getProperty("build")) {
|
||||
configObject.addPropertyAssignment({
|
||||
name: "build",
|
||||
initializer: `{
|
||||
target: "esnext",
|
||||
rollupOptions: {
|
||||
external: ["node:async_hooks", "cloudflare:workers"],
|
||||
},
|
||||
}`,
|
||||
});
|
||||
}
|
||||
|
||||
const pluginsProperty = configObject.getProperty("plugins");
|
||||
if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
|
||||
const initializer = pluginsProperty.getInitializer();
|
||||
if (Node.isArrayLiteralExpression(initializer)) {
|
||||
const hasShim = initializer
|
||||
.getElements()
|
||||
.some((el) => el.getText().includes("alchemy"));
|
||||
if (!hasShim) {
|
||||
initializer.addElement("alchemy()");
|
||||
}
|
||||
|
||||
const tanstackElements = initializer
|
||||
.getElements()
|
||||
.filter((el) => el.getText().includes("tanstackStart"));
|
||||
|
||||
tanstackElements.forEach((element) => {
|
||||
if (Node.isCallExpression(element)) {
|
||||
const args = element.getArguments();
|
||||
if (args.length === 0) {
|
||||
element.addArgument(`{
|
||||
target: "cloudflare-module",
|
||||
customViteReactPlugin: true,
|
||||
}`);
|
||||
} else if (
|
||||
args.length === 1 &&
|
||||
Node.isObjectLiteralExpression(args[0])
|
||||
) {
|
||||
const configObj = args[0];
|
||||
if (!configObj.getProperty("target")) {
|
||||
configObj.addPropertyAssignment({
|
||||
name: "target",
|
||||
initializer: '"cloudflare-module"',
|
||||
});
|
||||
}
|
||||
if (!configObj.getProperty("customViteReactPlugin")) {
|
||||
configObj.addPropertyAssignment({
|
||||
name: "customViteReactPlugin",
|
||||
initializer: "true",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
configObject.addPropertyAssignment({
|
||||
name: "plugins",
|
||||
initializer: "[alchemy()]",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await project.save();
|
||||
} catch (error) {
|
||||
console.warn("Failed to update vite.config.ts:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// workaround for tanstack start + workers
|
||||
const nitroConfigPath = path.join(webAppDir, "nitro.config.ts");
|
||||
const nitroConfigContent = `import { defineNitroConfig } from "nitropack/config";
|
||||
|
||||
export default defineNitroConfig({
|
||||
preset: "cloudflare-module",
|
||||
cloudflare: {
|
||||
nodeCompat: true,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.writeFile(nitroConfigPath, nitroConfigContent, "utf-8");
|
||||
}
|
||||
7
apps/cli/src/helpers/deployment/alchemy/index.ts
Normal file
7
apps/cli/src/helpers/deployment/alchemy/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { setupNextAlchemyDeploy } from "./alchemy-next-setup";
|
||||
export { setupNuxtAlchemyDeploy } from "./alchemy-nuxt-setup";
|
||||
export { setupReactRouterAlchemyDeploy } from "./alchemy-react-router-setup";
|
||||
export { setupSolidAlchemyDeploy } from "./alchemy-solid-setup";
|
||||
export { setupSvelteAlchemyDeploy } from "./alchemy-svelte-setup";
|
||||
export { setupTanStackRouterAlchemyDeploy } from "./alchemy-tanstack-router-setup";
|
||||
export { setupTanStackStartAlchemyDeploy } from "./alchemy-tanstack-start-setup";
|
||||
111
apps/cli/src/helpers/deployment/server-deploy-setup.ts
Normal file
111
apps/cli/src/helpers/deployment/server-deploy-setup.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import path from "node:path";
|
||||
import { log, spinner } from "@clack/prompts";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
export async function setupServerDeploy(config: ProjectConfig) {
|
||||
const { serverDeploy, webDeploy, projectDir } = config;
|
||||
const { packageManager } = config;
|
||||
|
||||
if (serverDeploy === "none") return;
|
||||
|
||||
if (serverDeploy === "alchemy" && webDeploy === "alchemy") {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
if (!(await fs.pathExists(serverDir))) return;
|
||||
|
||||
if (serverDeploy === "wrangler") {
|
||||
await setupWorkersServerDeploy(serverDir, packageManager);
|
||||
await generateCloudflareWorkerTypes({ serverDir, packageManager });
|
||||
} else if (serverDeploy === "alchemy") {
|
||||
await setupAlchemyServerDeploy(serverDir, packageManager);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupWorkersServerDeploy(
|
||||
serverDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
if (!(await fs.pathExists(packageJsonPath))) return;
|
||||
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
dev: "wrangler dev --port=3000",
|
||||
start: "wrangler dev",
|
||||
deploy: "wrangler deploy",
|
||||
build: "wrangler deploy --dry-run",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareBindings",
|
||||
};
|
||||
|
||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: ["wrangler", "@types/node", "@cloudflare/workers-types"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
}
|
||||
|
||||
async function generateCloudflareWorkerTypes({
|
||||
serverDir,
|
||||
packageManager,
|
||||
}: {
|
||||
serverDir: string;
|
||||
packageManager: ProjectConfig["packageManager"];
|
||||
}) {
|
||||
if (!(await fs.pathExists(serverDir))) return;
|
||||
const s = spinner();
|
||||
try {
|
||||
s.start("Generating Cloudflare Workers types...");
|
||||
const runCmd = packageManager === "npm" ? "npm" : packageManager;
|
||||
await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
|
||||
s.stop("Cloudflare Workers types generated successfully!");
|
||||
} catch {
|
||||
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
|
||||
const managerCmd = `${packageManager} run`;
|
||||
log.warn(
|
||||
`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupAlchemyServerDeploy(
|
||||
serverDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
if (!(await fs.pathExists(serverDir))) return;
|
||||
|
||||
await addPackageDependency({
|
||||
devDependencies: [
|
||||
"alchemy",
|
||||
"wrangler",
|
||||
"@types/node",
|
||||
"@cloudflare/workers-types",
|
||||
"dotenv",
|
||||
],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
dev: "wrangler dev --port=3000",
|
||||
build: "wrangler deploy --dry-run",
|
||||
deploy: "alchemy deploy",
|
||||
destroy: "alchemy destroy",
|
||||
"alchemy:dev": "alchemy dev",
|
||||
};
|
||||
|
||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
94
apps/cli/src/helpers/deployment/web-deploy-setup.ts
Normal file
94
apps/cli/src/helpers/deployment/web-deploy-setup.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { PackageManager, ProjectConfig } from "../../types";
|
||||
import { setupCombinedAlchemyDeploy } from "./alchemy/alchemy-combined-setup";
|
||||
import { setupNextAlchemyDeploy } from "./alchemy/alchemy-next-setup";
|
||||
import { setupNuxtAlchemyDeploy } from "./alchemy/alchemy-nuxt-setup";
|
||||
import { setupReactRouterAlchemyDeploy } from "./alchemy/alchemy-react-router-setup";
|
||||
import { setupSolidAlchemyDeploy } from "./alchemy/alchemy-solid-setup";
|
||||
import { setupSvelteAlchemyDeploy } from "./alchemy/alchemy-svelte-setup";
|
||||
import { setupTanStackRouterAlchemyDeploy } from "./alchemy/alchemy-tanstack-router-setup";
|
||||
import { setupTanStackStartAlchemyDeploy } from "./alchemy/alchemy-tanstack-start-setup";
|
||||
import { setupNextWorkersDeploy } from "./workers/workers-next-setup";
|
||||
import { setupNuxtWorkersDeploy } from "./workers/workers-nuxt-setup";
|
||||
import { setupSvelteWorkersDeploy } from "./workers/workers-svelte-setup";
|
||||
import { setupTanstackStartWorkersDeploy } from "./workers/workers-tanstack-start-setup";
|
||||
import { setupWorkersVitePlugin } from "./workers/workers-vite-setup";
|
||||
|
||||
export async function setupWebDeploy(config: ProjectConfig) {
|
||||
const { webDeploy, serverDeploy, frontend, projectDir } = config;
|
||||
const { packageManager } = config;
|
||||
|
||||
if (webDeploy === "none") return;
|
||||
|
||||
if (webDeploy !== "wrangler" && webDeploy !== "alchemy") return;
|
||||
|
||||
if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
|
||||
await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
|
||||
return;
|
||||
}
|
||||
|
||||
const isNext = frontend.includes("next");
|
||||
const isNuxt = frontend.includes("nuxt");
|
||||
const isSvelte = frontend.includes("svelte");
|
||||
const isTanstackRouter = frontend.includes("tanstack-router");
|
||||
const isTanstackStart = frontend.includes("tanstack-start");
|
||||
const isReactRouter = frontend.includes("react-router");
|
||||
const isSolid = frontend.includes("solid");
|
||||
|
||||
if (webDeploy === "wrangler") {
|
||||
if (isNext) {
|
||||
await setupNextWorkersDeploy(projectDir, packageManager);
|
||||
} else if (isNuxt) {
|
||||
await setupNuxtWorkersDeploy(projectDir, packageManager);
|
||||
} else if (isSvelte) {
|
||||
await setupSvelteWorkersDeploy(projectDir, packageManager);
|
||||
} else if (isTanstackStart) {
|
||||
await setupTanstackStartWorkersDeploy(projectDir, packageManager);
|
||||
} else if (isTanstackRouter || isReactRouter || isSolid) {
|
||||
await setupWorkersWebDeploy(projectDir, packageManager);
|
||||
}
|
||||
} else if (webDeploy === "alchemy") {
|
||||
if (isNext) {
|
||||
await setupNextAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isNuxt) {
|
||||
await setupNuxtAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isSvelte) {
|
||||
await setupSvelteAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isTanstackStart) {
|
||||
await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isTanstackRouter) {
|
||||
await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isReactRouter) {
|
||||
await setupReactRouterAlchemyDeploy(projectDir, packageManager);
|
||||
} else if (isSolid) {
|
||||
await setupSolidAlchemyDeploy(projectDir, packageManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupWorkersWebDeploy(
|
||||
projectDir: string,
|
||||
pkgManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
|
||||
if (!(await fs.pathExists(webAppDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
"wrangler:dev": "wrangler dev --port=3001",
|
||||
deploy: `${pkgManager} run build && wrangler deploy`,
|
||||
};
|
||||
|
||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||
}
|
||||
|
||||
await setupWorkersVitePlugin(projectDir);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
|
||||
export async function setupNextWorkersDeploy(
|
||||
projectDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) return;
|
||||
|
||||
await addPackageDependency({
|
||||
dependencies: ["@opennextjs/cloudflare"],
|
||||
devDependencies: ["wrangler"],
|
||||
projectDir: webAppDir,
|
||||
});
|
||||
|
||||
const packageJsonPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
const pkg = await fs.readJson(packageJsonPath);
|
||||
|
||||
pkg.scripts = {
|
||||
...pkg.scripts,
|
||||
preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||
"cf-typegen":
|
||||
"wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
||||
};
|
||||
|
||||
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
type PropertyAssignment,
|
||||
SyntaxKind,
|
||||
} from "ts-morph";
|
||||
import type { PackageManager } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { tsProject } from "../../utils/ts-morph";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
import { tsProject } from "../../../utils/ts-morph";
|
||||
|
||||
export async function setupNuxtWorkersDeploy(
|
||||
projectDir: string,
|
||||
@@ -1,9 +1,9 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { ImportDeclaration } from "ts-morph";
|
||||
import type { PackageManager } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { tsProject } from "../../utils/ts-morph";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
import { tsProject } from "../../../utils/ts-morph";
|
||||
|
||||
export async function setupSvelteWorkersDeploy(
|
||||
projectDir: string,
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
type ObjectLiteralExpression,
|
||||
SyntaxKind,
|
||||
} from "ts-morph";
|
||||
import type { PackageManager } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { ensureArrayProperty, tsProject } from "../../utils/ts-morph";
|
||||
import type { PackageManager } from "../../../types";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
import { ensureArrayProperty, tsProject } from "../../../utils/ts-morph";
|
||||
|
||||
export async function setupTanstackStartWorkersDeploy(
|
||||
projectDir: string,
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
type ObjectLiteralExpression,
|
||||
SyntaxKind,
|
||||
} from "ts-morph";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { ensureArrayProperty, tsProject } from "../../utils/ts-morph";
|
||||
import { addPackageDependency } from "../../../utils/add-package-deps";
|
||||
import { ensureArrayProperty, tsProject } from "../../../utils/ts-morph";
|
||||
|
||||
export async function setupWorkersVitePlugin(projectDir: string) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
@@ -1,284 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { AvailableDependencies } from "../../constants";
|
||||
import type { Frontend, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
export async function setupApi(config: ProjectConfig) {
|
||||
const { api, projectName, frontend, backend, packageManager, projectDir } =
|
||||
config;
|
||||
const isConvex = backend === "convex";
|
||||
const webDir = path.join(projectDir, "apps/web");
|
||||
const nativeDir = path.join(projectDir, "apps/native");
|
||||
const webDirExists = await fs.pathExists(webDir);
|
||||
const nativeDirExists = await fs.pathExists(nativeDir);
|
||||
|
||||
const hasReactWeb = frontend.some((f) =>
|
||||
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||
);
|
||||
const hasNuxtWeb = frontend.includes("nuxt");
|
||||
const hasSvelteWeb = frontend.includes("svelte");
|
||||
const hasSolidWeb = frontend.includes("solid");
|
||||
|
||||
if (!isConvex && api !== "none") {
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
const serverDirExists = await fs.pathExists(serverDir);
|
||||
|
||||
if (serverDirExists) {
|
||||
if (api === "orpc") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@orpc/server", "@orpc/client"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
} else if (api === "trpc") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@trpc/server", "@trpc/client"],
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if (webDirExists) {
|
||||
if (hasReactWeb) {
|
||||
if (api === "orpc") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@orpc/tanstack-query", "@orpc/client"],
|
||||
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: [
|
||||
"@tanstack/vue-query",
|
||||
"@tanstack/vue-query-devtools",
|
||||
"@orpc/tanstack-query",
|
||||
"@orpc/client",
|
||||
],
|
||||
projectDir: webDir,
|
||||
});
|
||||
}
|
||||
} else if (hasSvelteWeb) {
|
||||
if (api === "orpc") {
|
||||
await addPackageDependency({
|
||||
dependencies: [
|
||||
"@orpc/tanstack-query",
|
||||
"@orpc/client",
|
||||
"@tanstack/svelte-query",
|
||||
],
|
||||
projectDir: webDir,
|
||||
});
|
||||
}
|
||||
} else if (hasSolidWeb) {
|
||||
if (api === "orpc") {
|
||||
await addPackageDependency({
|
||||
dependencies: [
|
||||
"@orpc/tanstack-query",
|
||||
"@orpc/client",
|
||||
"@tanstack/solid-query",
|
||||
],
|
||||
projectDir: webDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeDirExists) {
|
||||
if (api === "trpc") {
|
||||
await addPackageDependency({
|
||||
dependencies: [
|
||||
"@trpc/tanstack-react-query",
|
||||
"@trpc/client",
|
||||
"@trpc/server",
|
||||
],
|
||||
projectDir: nativeDir,
|
||||
});
|
||||
} else if (api === "orpc") {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@orpc/tanstack-query", "@orpc/client"],
|
||||
projectDir: nativeDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reactBasedFrontends: Frontend[] = [
|
||||
"react-router",
|
||||
"tanstack-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"native-nativewind",
|
||||
"native-unistyles",
|
||||
];
|
||||
const needsSolidQuery = frontend.includes("solid");
|
||||
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
|
||||
|
||||
if (needsReactQuery && !isConvex) {
|
||||
const reactQueryDeps: AvailableDependencies[] = ["@tanstack/react-query"];
|
||||
const reactQueryDevDeps: AvailableDependencies[] = [
|
||||
"@tanstack/react-query-devtools",
|
||||
];
|
||||
|
||||
const hasReactWeb = frontend.some(
|
||||
(f) =>
|
||||
f !== "native-nativewind" &&
|
||||
f !== "native-unistyles" &&
|
||||
reactBasedFrontends.includes(f),
|
||||
);
|
||||
const hasNative =
|
||||
frontend.includes("native-nativewind") ||
|
||||
frontend.includes("native-unistyles");
|
||||
|
||||
if (hasReactWeb && webDirExists) {
|
||||
const webPkgJsonPath = path.join(webDir, "package.json");
|
||||
if (await fs.pathExists(webPkgJsonPath)) {
|
||||
try {
|
||||
await addPackageDependency({
|
||||
dependencies: reactQueryDeps,
|
||||
devDependencies: reactQueryDevDeps,
|
||||
projectDir: webDir,
|
||||
});
|
||||
} catch (_error) {}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNative && nativeDirExists) {
|
||||
const nativePkgJsonPath = path.join(nativeDir, "package.json");
|
||||
if (await fs.pathExists(nativePkgJsonPath)) {
|
||||
try {
|
||||
await addPackageDependency({
|
||||
dependencies: reactQueryDeps,
|
||||
projectDir: nativeDir,
|
||||
});
|
||||
} catch (_error) {}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsSolidQuery && !isConvex) {
|
||||
const solidQueryDeps: AvailableDependencies[] = ["@tanstack/solid-query"];
|
||||
const solidQueryDevDeps: AvailableDependencies[] = [
|
||||
"@tanstack/solid-query-devtools",
|
||||
];
|
||||
|
||||
if (webDirExists) {
|
||||
const webPkgJsonPath = path.join(webDir, "package.json");
|
||||
if (await fs.pathExists(webPkgJsonPath)) {
|
||||
try {
|
||||
await addPackageDependency({
|
||||
dependencies: solidQueryDeps,
|
||||
devDependencies: solidQueryDevDeps,
|
||||
projectDir: webDir,
|
||||
});
|
||||
} catch (_error) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isConvex) {
|
||||
if (webDirExists) {
|
||||
const webPkgJsonPath = path.join(webDir, "package.json");
|
||||
if (await fs.pathExists(webPkgJsonPath)) {
|
||||
try {
|
||||
const webDepsToAdd: AvailableDependencies[] = ["convex"];
|
||||
if (frontend.includes("tanstack-start")) {
|
||||
webDepsToAdd.push("@convex-dev/react-query");
|
||||
}
|
||||
if (hasSvelteWeb) {
|
||||
webDepsToAdd.push("convex-svelte");
|
||||
}
|
||||
if (hasNuxtWeb) {
|
||||
webDepsToAdd.push("convex-nuxt");
|
||||
webDepsToAdd.push("convex-vue");
|
||||
}
|
||||
await addPackageDependency({
|
||||
dependencies: webDepsToAdd,
|
||||
projectDir: webDir,
|
||||
});
|
||||
} catch (_error) {}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeDirExists) {
|
||||
const nativePkgJsonPath = path.join(nativeDir, "package.json");
|
||||
if (await fs.pathExists(nativePkgJsonPath)) {
|
||||
try {
|
||||
await addPackageDependency({
|
||||
dependencies: ["convex"],
|
||||
projectDir: nativeDir,
|
||||
});
|
||||
} catch (_error) {}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
const backendPackageName = `@${projectName}/backend`;
|
||||
const backendWorkspaceVersion =
|
||||
packageManager === "npm" ? "*" : "workspace:*";
|
||||
const addWorkspaceDepManually = async (
|
||||
pkgJsonPath: string,
|
||||
depName: string,
|
||||
depVersion: string,
|
||||
) => {
|
||||
try {
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
if (!pkgJson.dependencies) {
|
||||
pkgJson.dependencies = {};
|
||||
}
|
||||
if (pkgJson.dependencies[depName] !== depVersion) {
|
||||
pkgJson.dependencies[depName] = depVersion;
|
||||
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
||||
} else {
|
||||
}
|
||||
} catch (_error) {}
|
||||
};
|
||||
|
||||
if (webDirExists) {
|
||||
const webPkgJsonPath = path.join(webDir, "package.json");
|
||||
if (await fs.pathExists(webPkgJsonPath)) {
|
||||
await addWorkspaceDepManually(
|
||||
webPkgJsonPath,
|
||||
backendPackageName,
|
||||
backendWorkspaceVersion,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeDirExists) {
|
||||
const nativePkgJsonPath = path.join(nativeDir, "package.json");
|
||||
if (await fs.pathExists(nativePkgJsonPath)) {
|
||||
await addWorkspaceDepManually(
|
||||
nativePkgJsonPath,
|
||||
backendPackageName,
|
||||
backendWorkspaceVersion,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { PackageManager, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { setupNuxtWorkersDeploy } from "./workers-nuxt-setup";
|
||||
import { setupSvelteWorkersDeploy } from "./workers-svelte-setup";
|
||||
import { setupTanstackStartWorkersDeploy } from "./workers-tanstack-start-setup";
|
||||
import { setupWorkersVitePlugin } from "./workers-vite-setup";
|
||||
|
||||
export async function setupWebDeploy(config: ProjectConfig) {
|
||||
const { webDeploy, frontend, projectDir } = config;
|
||||
const { packageManager } = config;
|
||||
|
||||
if (webDeploy === "none") return;
|
||||
|
||||
if (webDeploy !== "workers") return;
|
||||
|
||||
const isNext = frontend.includes("next");
|
||||
const isNuxt = frontend.includes("nuxt");
|
||||
const isSvelte = frontend.includes("svelte");
|
||||
const isTanstackRouter = frontend.includes("tanstack-router");
|
||||
const isTanstackStart = frontend.includes("tanstack-start");
|
||||
const isReactRouter = frontend.includes("react-router");
|
||||
const isSolid = frontend.includes("solid");
|
||||
|
||||
if (isNext) {
|
||||
await setupNextWorkersDeploy(projectDir, packageManager);
|
||||
} else if (isNuxt) {
|
||||
await setupNuxtWorkersDeploy(projectDir, packageManager);
|
||||
} else if (isSvelte) {
|
||||
await setupSvelteWorkersDeploy(projectDir, packageManager);
|
||||
} else if (isTanstackStart) {
|
||||
await setupTanstackStartWorkersDeploy(projectDir, packageManager);
|
||||
} else if (isTanstackRouter || isReactRouter || isSolid) {
|
||||
await setupWorkersWebDeploy(projectDir, packageManager);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupWorkersWebDeploy(
|
||||
projectDir: string,
|
||||
pkgManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
|
||||
if (!(await fs.pathExists(webAppDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
packageJson.scripts = {
|
||||
...packageJson.scripts,
|
||||
"wrangler:dev": "wrangler dev --port=3001",
|
||||
deploy: `${pkgManager} run build && wrangler deploy`,
|
||||
};
|
||||
|
||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||
}
|
||||
|
||||
await setupWorkersVitePlugin(projectDir);
|
||||
}
|
||||
|
||||
async function setupNextWorkersDeploy(
|
||||
projectDir: string,
|
||||
_packageManager: PackageManager,
|
||||
) {
|
||||
const webAppDir = path.join(projectDir, "apps/web");
|
||||
if (!(await fs.pathExists(webAppDir))) return;
|
||||
|
||||
await addPackageDependency({
|
||||
dependencies: ["@opennextjs/cloudflare"],
|
||||
devDependencies: ["wrangler"],
|
||||
projectDir: webAppDir,
|
||||
});
|
||||
|
||||
const packageJsonPath = path.join(webAppDir, "package.json");
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
const pkg = await fs.readJson(packageJsonPath);
|
||||
|
||||
pkg.scripts = {
|
||||
...pkg.scripts,
|
||||
preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||
"cf-typegen":
|
||||
"wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
||||
};
|
||||
|
||||
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import z from "zod";
|
||||
import {
|
||||
addAddonsHandler,
|
||||
createProjectHandler,
|
||||
} from "./helpers/project-generation/command-handlers";
|
||||
} from "./helpers/core/command-handlers";
|
||||
import {
|
||||
type AddInput,
|
||||
type Addons,
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
ProjectNameSchema,
|
||||
type Runtime,
|
||||
RuntimeSchema,
|
||||
type ServerDeploy,
|
||||
ServerDeploySchema,
|
||||
type WebDeploy,
|
||||
WebDeploySchema,
|
||||
} from "./types";
|
||||
@@ -88,6 +90,7 @@ export const router = t.router({
|
||||
runtime: RuntimeSchema.optional(),
|
||||
api: APISchema.optional(),
|
||||
webDeploy: WebDeploySchema.optional(),
|
||||
serverDeploy: ServerDeploySchema.optional(),
|
||||
directoryConflict: DirectoryConflictSchema.optional(),
|
||||
renderTitle: z.boolean().optional(),
|
||||
disableAnalytics: z
|
||||
@@ -120,6 +123,7 @@ export const router = t.router({
|
||||
z.object({
|
||||
addons: z.array(AddonsSchema).optional().default([]),
|
||||
webDeploy: WebDeploySchema.optional(),
|
||||
serverDeploy: ServerDeploySchema.optional(),
|
||||
projectDir: z.string().optional(),
|
||||
install: z
|
||||
.boolean()
|
||||
@@ -251,6 +255,7 @@ export type {
|
||||
DatabaseSetup,
|
||||
API,
|
||||
WebDeploy,
|
||||
ServerDeploy,
|
||||
DirectoryConflict,
|
||||
CreateInput,
|
||||
AddInput,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
PackageManager,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
ServerDeploy,
|
||||
WebDeploy,
|
||||
} from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
@@ -27,6 +28,7 @@ import { getinstallChoice } from "./install";
|
||||
import { getORMChoice } from "./orm";
|
||||
import { getPackageManagerChoice } from "./package-manager";
|
||||
import { getRuntimeChoice } from "./runtime";
|
||||
import { getServerDeploymentChoice } from "./server-deploy";
|
||||
import { getDeploymentChoice } from "./web-deploy";
|
||||
|
||||
type PromptGroupResults = {
|
||||
@@ -44,6 +46,7 @@ type PromptGroupResults = {
|
||||
packageManager: PackageManager;
|
||||
install: boolean;
|
||||
webDeploy: WebDeploy;
|
||||
serverDeploy: ServerDeploy;
|
||||
};
|
||||
|
||||
export async function gatherConfig(
|
||||
@@ -97,6 +100,13 @@ export async function gatherConfig(
|
||||
results.backend,
|
||||
results.frontend,
|
||||
),
|
||||
serverDeploy: ({ results }) =>
|
||||
getServerDeploymentChoice(
|
||||
flags.serverDeploy,
|
||||
results.runtime,
|
||||
results.backend,
|
||||
results.webDeploy,
|
||||
),
|
||||
git: () => getGitChoice(flags.git),
|
||||
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
||||
install: () => getinstallChoice(flags.install),
|
||||
@@ -144,5 +154,6 @@ export async function gatherConfig(
|
||||
dbSetup: result.dbSetup,
|
||||
api: result.api,
|
||||
webDeploy: result.webDeploy,
|
||||
serverDeploy: result.serverDeploy,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ export async function getProjectName(initialName?: string): Promise<string> {
|
||||
let counter = 1;
|
||||
|
||||
while (
|
||||
fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) &&
|
||||
fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0
|
||||
(await fs.pathExists(path.resolve(process.cwd(), defaultName))) &&
|
||||
(await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0
|
||||
) {
|
||||
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
||||
counter++;
|
||||
|
||||
129
apps/cli/src/prompts/server-deploy.ts
Normal file
129
apps/cli/src/prompts/server-deploy.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { isCancel, select } from "@clack/prompts";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Backend, Runtime, ServerDeploy, WebDeploy } from "../types";
|
||||
import { exitCancelled } from "../utils/errors";
|
||||
|
||||
type DeploymentOption = {
|
||||
value: ServerDeploy;
|
||||
label: string;
|
||||
hint: string;
|
||||
};
|
||||
|
||||
function getDeploymentDisplay(deployment: ServerDeploy): {
|
||||
label: string;
|
||||
hint: string;
|
||||
} {
|
||||
if (deployment === "wrangler") {
|
||||
return {
|
||||
label: "Wrangler",
|
||||
hint: "Deploy to Cloudflare Workers using Wrangler",
|
||||
};
|
||||
}
|
||||
if (deployment === "alchemy") {
|
||||
return {
|
||||
label: "Alchemy",
|
||||
hint: "Deploy to Cloudflare Workers using Alchemy",
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: deployment,
|
||||
hint: `Add ${deployment} deployment`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getServerDeploymentChoice(
|
||||
deployment?: ServerDeploy,
|
||||
runtime?: Runtime,
|
||||
backend?: Backend,
|
||||
webDeploy?: WebDeploy,
|
||||
): Promise<ServerDeploy> {
|
||||
if (deployment !== undefined) return deployment;
|
||||
|
||||
if (backend === "none" || backend === "convex") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
const options: DeploymentOption[] = [];
|
||||
|
||||
if (runtime === "workers") {
|
||||
["alchemy", "wrangler"].forEach((deploy) => {
|
||||
const { label, hint } = getDeploymentDisplay(deploy as ServerDeploy);
|
||||
options.unshift({
|
||||
value: deploy as ServerDeploy,
|
||||
label,
|
||||
hint,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
options.push({ value: "none", label: "None", hint: "Manual setup" });
|
||||
}
|
||||
|
||||
const response = await select<ServerDeploy>({
|
||||
message: "Select server deployment",
|
||||
options,
|
||||
initialValue:
|
||||
webDeploy === "alchemy"
|
||||
? "alchemy"
|
||||
: runtime === "workers"
|
||||
? "wrangler"
|
||||
: DEFAULT_CONFIG.serverDeploy,
|
||||
});
|
||||
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function getServerDeploymentToAdd(
|
||||
runtime?: Runtime,
|
||||
existingDeployment?: ServerDeploy,
|
||||
): Promise<ServerDeploy> {
|
||||
const options: DeploymentOption[] = [];
|
||||
|
||||
if (runtime === "workers") {
|
||||
if (existingDeployment !== "wrangler") {
|
||||
const { label, hint } = getDeploymentDisplay("wrangler");
|
||||
options.push({
|
||||
value: "wrangler",
|
||||
label,
|
||||
hint,
|
||||
});
|
||||
}
|
||||
|
||||
if (existingDeployment !== "alchemy") {
|
||||
const { label, hint } = getDeploymentDisplay("alchemy");
|
||||
options.push({
|
||||
value: "alchemy",
|
||||
label,
|
||||
hint,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (existingDeployment && existingDeployment !== "none") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (options.length > 0) {
|
||||
options.push({
|
||||
value: "none",
|
||||
label: "None",
|
||||
hint: "Skip deployment setup",
|
||||
});
|
||||
}
|
||||
|
||||
if (options.length === 0) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
const response = await select<ServerDeploy>({
|
||||
message: "Select server deployment",
|
||||
options,
|
||||
initialValue:
|
||||
runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy,
|
||||
});
|
||||
|
||||
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -18,12 +18,18 @@ function getDeploymentDisplay(deployment: WebDeploy): {
|
||||
label: string;
|
||||
hint: string;
|
||||
} {
|
||||
if (deployment === "workers") {
|
||||
if (deployment === "wrangler") {
|
||||
return {
|
||||
label: "Cloudflare Workers",
|
||||
label: "Wrangler",
|
||||
hint: "Deploy to Cloudflare Workers using Wrangler",
|
||||
};
|
||||
}
|
||||
if (deployment === "alchemy") {
|
||||
return {
|
||||
label: "Alchemy",
|
||||
hint: "Deploy to Cloudflare Workers using Alchemy",
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: deployment,
|
||||
hint: `Add ${deployment} deployment`,
|
||||
@@ -41,14 +47,16 @@ export async function getDeploymentChoice(
|
||||
return "none";
|
||||
}
|
||||
|
||||
const options: DeploymentOption[] = [
|
||||
{
|
||||
value: "workers",
|
||||
label: "Cloudflare Workers",
|
||||
hint: "Deploy to Cloudflare Workers using Wrangler",
|
||||
const options: DeploymentOption[] = ["wrangler", "alchemy", "none"].map(
|
||||
(deploy) => {
|
||||
const { label, hint } = getDeploymentDisplay(deploy as WebDeploy);
|
||||
return {
|
||||
value: deploy as WebDeploy,
|
||||
label,
|
||||
hint,
|
||||
};
|
||||
},
|
||||
{ value: "none", label: "None", hint: "Manual setup" },
|
||||
];
|
||||
);
|
||||
|
||||
const response = await select<WebDeploy>({
|
||||
message: "Select web deployment",
|
||||
@@ -71,10 +79,19 @@ export async function getDeploymentToAdd(
|
||||
|
||||
const options: DeploymentOption[] = [];
|
||||
|
||||
if (existingDeployment !== "workers") {
|
||||
const { label, hint } = getDeploymentDisplay("workers");
|
||||
if (existingDeployment !== "wrangler") {
|
||||
const { label, hint } = getDeploymentDisplay("wrangler");
|
||||
options.push({
|
||||
value: "workers",
|
||||
value: "wrangler",
|
||||
label,
|
||||
hint,
|
||||
});
|
||||
}
|
||||
|
||||
if (existingDeployment !== "alchemy") {
|
||||
const { label, hint } = getDeploymentDisplay("alchemy");
|
||||
options.push({
|
||||
value: "alchemy",
|
||||
label,
|
||||
hint,
|
||||
});
|
||||
|
||||
@@ -104,10 +104,15 @@ export const ProjectNameSchema = z
|
||||
export type ProjectName = z.infer<typeof ProjectNameSchema>;
|
||||
|
||||
export const WebDeploySchema = z
|
||||
.enum(["workers", "none"])
|
||||
.enum(["wrangler", "alchemy", "none"])
|
||||
.describe("Web deployment");
|
||||
export type WebDeploy = z.infer<typeof WebDeploySchema>;
|
||||
|
||||
export const ServerDeploySchema = z
|
||||
.enum(["wrangler", "alchemy", "none"])
|
||||
.describe("Server deployment");
|
||||
export type ServerDeploy = z.infer<typeof ServerDeploySchema>;
|
||||
|
||||
export const DirectoryConflictSchema = z
|
||||
.enum(["merge", "overwrite", "increment", "error"])
|
||||
.describe("How to handle existing directory conflicts");
|
||||
@@ -132,6 +137,7 @@ export type CreateInput = {
|
||||
runtime?: Runtime;
|
||||
api?: API;
|
||||
webDeploy?: WebDeploy;
|
||||
serverDeploy?: ServerDeploy;
|
||||
directoryConflict?: DirectoryConflict;
|
||||
renderTitle?: boolean;
|
||||
disableAnalytics?: boolean;
|
||||
@@ -140,6 +146,7 @@ export type CreateInput = {
|
||||
export type AddInput = {
|
||||
addons?: Addons[];
|
||||
webDeploy?: WebDeploy;
|
||||
serverDeploy?: ServerDeploy;
|
||||
projectDir?: string;
|
||||
install?: boolean;
|
||||
packageManager?: PackageManager;
|
||||
@@ -167,6 +174,7 @@ export interface ProjectConfig {
|
||||
dbSetup: DatabaseSetup;
|
||||
api: API;
|
||||
webDeploy: WebDeploy;
|
||||
serverDeploy: ServerDeploy;
|
||||
}
|
||||
|
||||
export interface BetterTStackConfig {
|
||||
@@ -184,6 +192,7 @@ export interface BetterTStackConfig {
|
||||
dbSetup: DatabaseSetup;
|
||||
api: API;
|
||||
webDeploy: WebDeploy;
|
||||
serverDeploy: ServerDeploy;
|
||||
}
|
||||
|
||||
export interface InitResult {
|
||||
|
||||
@@ -5,13 +5,19 @@ import { isTelemetryEnabled } from "./telemetry";
|
||||
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || "";
|
||||
const POSTHOG_HOST = process.env.POSTHOG_HOST;
|
||||
|
||||
function generateSessionId() {
|
||||
const rand = Math.random().toString(36).slice(2);
|
||||
const now = Date.now().toString(36);
|
||||
return `cli_${now}${rand}`;
|
||||
}
|
||||
|
||||
export async function trackProjectCreation(
|
||||
config: ProjectConfig,
|
||||
disableAnalytics = false,
|
||||
) {
|
||||
if (!isTelemetryEnabled() || disableAnalytics) return;
|
||||
|
||||
const sessionId = `cli_${crypto.randomUUID().replace(/-/g, "")}`;
|
||||
const sessionId = generateSessionId();
|
||||
// biome-ignore lint/correctness/noUnusedVariables: `projectName`, `projectDir`, and `relativePath` are not used in the event properties
|
||||
const { projectName, projectDir, relativePath, ...safeConfig } = config;
|
||||
|
||||
@@ -21,8 +27,8 @@ export async function trackProjectCreation(
|
||||
properties: {
|
||||
...safeConfig,
|
||||
cli_version: getLatestCLIVersion(),
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
node_version: typeof process !== "undefined" ? process.version : "",
|
||||
platform: typeof process !== "undefined" ? process.platform : "",
|
||||
$ip: null,
|
||||
},
|
||||
distinct_id: sessionId,
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) {
|
||||
dbSetup: projectConfig.dbSetup,
|
||||
api: projectConfig.api,
|
||||
webDeploy: projectConfig.webDeploy,
|
||||
serverDeploy: projectConfig.serverDeploy,
|
||||
};
|
||||
|
||||
const baseContent = {
|
||||
@@ -40,6 +41,7 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) {
|
||||
dbSetup: btsConfig.dbSetup,
|
||||
api: btsConfig.api,
|
||||
webDeploy: btsConfig.webDeploy,
|
||||
serverDeploy: btsConfig.serverDeploy,
|
||||
};
|
||||
|
||||
let configContent = JSON.stringify(baseContent);
|
||||
@@ -91,7 +93,9 @@ export async function readBtsConfig(
|
||||
|
||||
export async function updateBtsConfig(
|
||||
projectDir: string,
|
||||
updates: Partial<Pick<BetterTStackConfig, "addons" | "webDeploy">>,
|
||||
updates: Partial<
|
||||
Pick<BetterTStackConfig, "addons" | "webDeploy" | "serverDeploy">
|
||||
>,
|
||||
) {
|
||||
try {
|
||||
const configPath = path.join(projectDir, BTS_CONFIG_FILE);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type {
|
||||
Addons,
|
||||
API,
|
||||
Backend,
|
||||
CLIInput,
|
||||
Frontend,
|
||||
ProjectConfig,
|
||||
ServerDeploy,
|
||||
WebDeploy,
|
||||
} from "../types";
|
||||
import { validateAddonCompatibility } from "./addon-compatibility";
|
||||
@@ -252,6 +254,21 @@ export function validateWebDeployRequiresWebFrontend(
|
||||
}
|
||||
}
|
||||
|
||||
export function validateServerDeployRequiresBackend(
|
||||
serverDeploy: ServerDeploy | undefined,
|
||||
backend: Backend | undefined,
|
||||
) {
|
||||
if (
|
||||
serverDeploy &&
|
||||
serverDeploy !== "none" &&
|
||||
(!backend || backend === "none")
|
||||
) {
|
||||
exitWithError(
|
||||
"'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAddonsAgainstFrontends(
|
||||
addons: Addons[] = [],
|
||||
frontends: Frontend[] = [],
|
||||
@@ -297,3 +314,31 @@ export function validateExamplesCompatibility(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAlchemyCompatibility(
|
||||
webDeploy: WebDeploy | undefined,
|
||||
serverDeploy: ServerDeploy | undefined,
|
||||
frontends: Frontend[] = [],
|
||||
) {
|
||||
const isAlchemyWebDeploy = webDeploy === "alchemy";
|
||||
const isAlchemyServerDeploy = serverDeploy === "alchemy";
|
||||
|
||||
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
|
||||
const incompatibleFrontends = frontends.filter(
|
||||
(f) => f === "next" || f === "react-router",
|
||||
);
|
||||
|
||||
if (incompatibleFrontends.length > 0) {
|
||||
const deployType =
|
||||
isAlchemyWebDeploy && isAlchemyServerDeploy
|
||||
? "web and server deployment"
|
||||
: isAlchemyWebDeploy
|
||||
? "web deployment"
|
||||
: "server deployment";
|
||||
|
||||
exitWithError(
|
||||
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")} frontend(s). Please choose a different frontend or deployment option.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
134
apps/cli/src/utils/config-processing.ts
Normal file
134
apps/cli/src/utils/config-processing.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import path from "node:path";
|
||||
import type {
|
||||
API,
|
||||
Backend,
|
||||
CLIInput,
|
||||
Database,
|
||||
DatabaseSetup,
|
||||
ORM,
|
||||
PackageManager,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
ServerDeploy,
|
||||
WebDeploy,
|
||||
} from "../types";
|
||||
|
||||
export function processArrayOption<T>(
|
||||
options: (T | "none")[] | undefined,
|
||||
): T[] {
|
||||
if (!options || options.length === 0) return [];
|
||||
if (options.includes("none" as T | "none")) return [];
|
||||
return options.filter((item): item is T => item !== "none");
|
||||
}
|
||||
|
||||
export function deriveProjectName(
|
||||
projectName?: string,
|
||||
projectDirectory?: string,
|
||||
): string {
|
||||
if (projectName) {
|
||||
return projectName;
|
||||
}
|
||||
if (projectDirectory) {
|
||||
return path.basename(path.resolve(process.cwd(), projectDirectory));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function processFlags(
|
||||
options: CLIInput,
|
||||
projectName?: string,
|
||||
): Partial<ProjectConfig> {
|
||||
const config: Partial<ProjectConfig> = {};
|
||||
|
||||
if (options.api) {
|
||||
config.api = options.api as API;
|
||||
}
|
||||
|
||||
if (options.backend) {
|
||||
config.backend = options.backend as Backend;
|
||||
}
|
||||
|
||||
if (options.database) {
|
||||
config.database = options.database as Database;
|
||||
}
|
||||
|
||||
if (options.orm) {
|
||||
config.orm = options.orm as ORM;
|
||||
}
|
||||
|
||||
if (options.auth !== undefined) {
|
||||
config.auth = options.auth;
|
||||
}
|
||||
|
||||
if (options.git !== undefined) {
|
||||
config.git = options.git;
|
||||
}
|
||||
|
||||
if (options.install !== undefined) {
|
||||
config.install = options.install;
|
||||
}
|
||||
|
||||
if (options.runtime) {
|
||||
config.runtime = options.runtime as Runtime;
|
||||
}
|
||||
|
||||
if (options.dbSetup) {
|
||||
config.dbSetup = options.dbSetup as DatabaseSetup;
|
||||
}
|
||||
|
||||
if (options.packageManager) {
|
||||
config.packageManager = options.packageManager as PackageManager;
|
||||
}
|
||||
|
||||
if (options.webDeploy) {
|
||||
config.webDeploy = options.webDeploy as WebDeploy;
|
||||
}
|
||||
|
||||
if (options.serverDeploy) {
|
||||
config.serverDeploy = options.serverDeploy as ServerDeploy;
|
||||
}
|
||||
|
||||
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
||||
if (derivedName) {
|
||||
config.projectName = projectName || derivedName;
|
||||
}
|
||||
|
||||
if (options.frontend && options.frontend.length > 0) {
|
||||
config.frontend = processArrayOption(options.frontend);
|
||||
}
|
||||
|
||||
if (options.addons && options.addons.length > 0) {
|
||||
config.addons = processArrayOption(options.addons);
|
||||
}
|
||||
|
||||
if (options.examples && options.examples.length > 0) {
|
||||
config.examples = processArrayOption(options.examples);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function getProvidedFlags(options: CLIInput): Set<string> {
|
||||
return new Set(
|
||||
Object.keys(options).filter(
|
||||
(key) => options[key as keyof CLIInput] !== undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function validateNoneExclusivity<T>(
|
||||
options: (T | "none")[] | undefined,
|
||||
optionName: string,
|
||||
): void {
|
||||
if (!options || options.length === 0) return;
|
||||
|
||||
if (options.includes("none" as T | "none") && options.length > 1) {
|
||||
throw new Error(`Cannot combine 'none' with other ${optionName}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateArrayOptions(options: CLIInput): void {
|
||||
validateNoneExclusivity(options.frontend, "frontend options");
|
||||
validateNoneExclusivity(options.addons, "addons");
|
||||
validateNoneExclusivity(options.examples, "examples");
|
||||
}
|
||||
333
apps/cli/src/utils/config-validation.ts
Normal file
333
apps/cli/src/utils/config-validation.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import type {
|
||||
CLIInput,
|
||||
Database,
|
||||
DatabaseSetup,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
} from "../types";
|
||||
import {
|
||||
coerceBackendPresets,
|
||||
ensureSingleWebAndNative,
|
||||
incompatibleFlagsForBackend,
|
||||
isWebFrontend,
|
||||
validateAddonsAgainstFrontends,
|
||||
validateApiFrontendCompatibility,
|
||||
validateExamplesCompatibility,
|
||||
validateServerDeployRequiresBackend,
|
||||
validateWebDeployRequiresWebFrontend,
|
||||
validateWorkersCompatibility,
|
||||
validateAlchemyCompatibility,
|
||||
} from "./compatibility-rules";
|
||||
import { exitWithError } from "./errors";
|
||||
|
||||
export function validateDatabaseOrmAuth(
|
||||
cfg: Partial<ProjectConfig>,
|
||||
flags?: Set<string>,
|
||||
): void {
|
||||
const db = cfg.database;
|
||||
const orm = cfg.orm;
|
||||
const has = (k: string) => (flags ? flags.has(k) : true);
|
||||
|
||||
if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") {
|
||||
exitWithError(
|
||||
"Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.",
|
||||
);
|
||||
}
|
||||
|
||||
if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") {
|
||||
exitWithError(
|
||||
"Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
has("database") &&
|
||||
has("orm") &&
|
||||
db === "mongodb" &&
|
||||
orm &&
|
||||
orm !== "mongoose" &&
|
||||
orm !== "prisma" &&
|
||||
orm !== "none"
|
||||
) {
|
||||
exitWithError(
|
||||
"MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
|
||||
);
|
||||
}
|
||||
|
||||
if (has("database") && has("orm") && db && db !== "none" && orm === "none") {
|
||||
exitWithError(
|
||||
"Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (has("orm") && has("database") && orm && orm !== "none" && db === "none") {
|
||||
exitWithError(
|
||||
"ORM selection requires a database. Please choose a database or set '--orm none'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (has("auth") && has("database") && cfg.auth && db === "none") {
|
||||
exitWithError(
|
||||
"Authentication requires a database. Please choose a database or set '--no-auth'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (cfg.auth && db === "none") {
|
||||
exitWithError(
|
||||
"Authentication requires a database. Please choose a database or set '--no-auth'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (orm && orm !== "none" && db === "none") {
|
||||
exitWithError(
|
||||
"ORM selection requires a database. Please choose a database or set '--orm none'.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateDatabaseSetup(
|
||||
config: Partial<ProjectConfig>,
|
||||
providedFlags: Set<string>,
|
||||
): void {
|
||||
const { dbSetup, database, runtime } = config;
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
providedFlags.has("database") &&
|
||||
dbSetup &&
|
||||
dbSetup !== "none" &&
|
||||
database === "none"
|
||||
) {
|
||||
exitWithError(
|
||||
"Database setup requires a database. Please choose a database or set '--db-setup none'.",
|
||||
);
|
||||
}
|
||||
|
||||
const setupValidations: Record<
|
||||
DatabaseSetup,
|
||||
{ database?: Database; runtime?: Runtime; errorMessage: string }
|
||||
> = {
|
||||
turso: {
|
||||
database: "sqlite",
|
||||
errorMessage:
|
||||
"Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
|
||||
},
|
||||
neon: {
|
||||
database: "postgres",
|
||||
errorMessage:
|
||||
"Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
},
|
||||
"prisma-postgres": {
|
||||
database: "postgres",
|
||||
errorMessage:
|
||||
"Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
},
|
||||
"mongodb-atlas": {
|
||||
database: "mongodb",
|
||||
errorMessage:
|
||||
"MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.",
|
||||
},
|
||||
supabase: {
|
||||
database: "postgres",
|
||||
errorMessage:
|
||||
"Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
},
|
||||
d1: {
|
||||
database: "sqlite",
|
||||
runtime: "workers",
|
||||
errorMessage:
|
||||
"Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime.",
|
||||
},
|
||||
docker: {
|
||||
errorMessage:
|
||||
"Docker setup is not compatible with SQLite database or Cloudflare Workers runtime.",
|
||||
},
|
||||
none: { errorMessage: "" },
|
||||
};
|
||||
|
||||
if (dbSetup && dbSetup !== "none") {
|
||||
const validation = setupValidations[dbSetup];
|
||||
|
||||
if (validation.database && database !== validation.database) {
|
||||
exitWithError(validation.errorMessage);
|
||||
}
|
||||
|
||||
if (validation.runtime && runtime !== validation.runtime) {
|
||||
exitWithError(validation.errorMessage);
|
||||
}
|
||||
|
||||
if (dbSetup === "docker") {
|
||||
if (database === "sqlite") {
|
||||
exitWithError(
|
||||
"Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.",
|
||||
);
|
||||
}
|
||||
if (runtime === "workers") {
|
||||
exitWithError(
|
||||
"Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateBackendConstraints(
|
||||
config: Partial<ProjectConfig>,
|
||||
providedFlags: Set<string>,
|
||||
options: CLIInput,
|
||||
): void {
|
||||
const { backend } = config;
|
||||
|
||||
if (
|
||||
providedFlags.has("backend") &&
|
||||
backend &&
|
||||
backend !== "convex" &&
|
||||
backend !== "none"
|
||||
) {
|
||||
if (providedFlags.has("runtime") && options.runtime === "none") {
|
||||
exitWithError(
|
||||
"'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (backend === "convex" || backend === "none") {
|
||||
const incompatibleFlags = incompatibleFlagsForBackend(
|
||||
backend,
|
||||
providedFlags,
|
||||
options,
|
||||
);
|
||||
if (incompatibleFlags.length > 0) {
|
||||
exitWithError(
|
||||
`The following flags are incompatible with '--backend ${backend}': ${incompatibleFlags.join(
|
||||
", ",
|
||||
)}. Please remove them.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
backend === "convex" &&
|
||||
providedFlags.has("frontend") &&
|
||||
options.frontend
|
||||
) {
|
||||
const incompatibleFrontends = options.frontend.filter(
|
||||
(f) => f === "solid",
|
||||
);
|
||||
if (incompatibleFrontends.length > 0) {
|
||||
exitWithError(
|
||||
`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(
|
||||
", ",
|
||||
)}. Please choose a different frontend or backend.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coerceBackendPresets(config);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateFrontendConstraints(
|
||||
config: Partial<ProjectConfig>,
|
||||
providedFlags: Set<string>,
|
||||
): void {
|
||||
const { frontend } = config;
|
||||
|
||||
if (frontend && frontend.length > 0) {
|
||||
ensureSingleWebAndNative(frontend);
|
||||
|
||||
if (
|
||||
providedFlags.has("api") &&
|
||||
providedFlags.has("frontend") &&
|
||||
config.api
|
||||
) {
|
||||
validateApiFrontendCompatibility(config.api, frontend);
|
||||
}
|
||||
}
|
||||
|
||||
const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend(f));
|
||||
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
||||
}
|
||||
|
||||
export function validateApiConstraints(
|
||||
config: Partial<ProjectConfig>,
|
||||
options: CLIInput,
|
||||
): void {
|
||||
if (config.api === "none") {
|
||||
if (
|
||||
options.examples &&
|
||||
!(options.examples.length === 1 && options.examples[0] === "none") &&
|
||||
options.backend !== "convex"
|
||||
) {
|
||||
exitWithError(
|
||||
"Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateFullConfig(
|
||||
config: Partial<ProjectConfig>,
|
||||
providedFlags: Set<string>,
|
||||
options: CLIInput,
|
||||
): void {
|
||||
validateDatabaseOrmAuth(config, providedFlags);
|
||||
validateDatabaseSetup(config, providedFlags);
|
||||
|
||||
validateBackendConstraints(config, providedFlags, options);
|
||||
|
||||
validateFrontendConstraints(config, providedFlags);
|
||||
|
||||
validateApiConstraints(config, options);
|
||||
|
||||
validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
|
||||
|
||||
validateWorkersCompatibility(providedFlags, options, config);
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
||||
config.addons = [...new Set(config.addons)];
|
||||
}
|
||||
|
||||
validateExamplesCompatibility(
|
||||
config.examples ?? [],
|
||||
config.backend,
|
||||
config.database,
|
||||
config.frontend ?? [],
|
||||
);
|
||||
|
||||
validateAlchemyCompatibility(
|
||||
config.webDeploy,
|
||||
config.serverDeploy,
|
||||
config.frontend ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
export function validateConfigForProgrammaticUse(
|
||||
config: Partial<ProjectConfig>,
|
||||
): void {
|
||||
try {
|
||||
validateDatabaseOrmAuth(config);
|
||||
|
||||
if (config.frontend && config.frontend.length > 0) {
|
||||
ensureSingleWebAndNative(config.frontend);
|
||||
}
|
||||
|
||||
validateApiFrontendCompatibility(config.api, config.frontend);
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
||||
}
|
||||
|
||||
validateExamplesCompatibility(
|
||||
config.examples ?? [],
|
||||
config.backend,
|
||||
config.database,
|
||||
config.frontend ?? [],
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(String(error));
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,12 @@ export function displayConfig(config: Partial<ProjectConfig>) {
|
||||
);
|
||||
}
|
||||
|
||||
if (config.serverDeploy !== undefined) {
|
||||
configDisplay.push(
|
||||
`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (configDisplay.length === 0) {
|
||||
return pc.yellow("No configuration selected.");
|
||||
}
|
||||
|
||||
61
apps/cli/src/utils/format-with-biome.ts
Normal file
61
apps/cli/src/utils/format-with-biome.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import path from "node:path";
|
||||
import { Biome } from "@biomejs/js-api/nodejs";
|
||||
import fs from "fs-extra";
|
||||
import { glob } from "tinyglobby";
|
||||
|
||||
export async function formatProjectWithBiome(projectDir: string) {
|
||||
const biome = new Biome();
|
||||
const { projectKey } = biome.openProject(projectDir);
|
||||
|
||||
biome.applyConfiguration(projectKey, {
|
||||
formatter: {
|
||||
enabled: true,
|
||||
indentStyle: "tab",
|
||||
},
|
||||
javascript: {
|
||||
formatter: {
|
||||
quoteStyle: "double",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const files = await glob("**/*", {
|
||||
cwd: projectDir,
|
||||
dot: true,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
});
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const supported = new Set([
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".cjs",
|
||||
".mjs",
|
||||
".cts",
|
||||
".mts",
|
||||
".json",
|
||||
".jsonc",
|
||||
".md",
|
||||
".mdx",
|
||||
".css",
|
||||
".scss",
|
||||
".html",
|
||||
]);
|
||||
if (!supported.has(ext)) continue;
|
||||
|
||||
const original = await fs.readFile(filePath, "utf8");
|
||||
const result = biome.formatContent(projectKey, original, { filePath });
|
||||
const content = result?.content;
|
||||
if (typeof content !== "string") continue;
|
||||
if (content.length === 0 && original.length > 0) continue;
|
||||
if (content !== original) {
|
||||
await fs.writeFile(filePath, content);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
||||
|
||||
flags.push(`--db-setup ${config.dbSetup}`);
|
||||
flags.push(`--web-deploy ${config.webDeploy}`);
|
||||
flags.push(`--server-deploy ${config.serverDeploy}`);
|
||||
flags.push(config.git ? "--git" : "--no-git");
|
||||
flags.push(`--package-manager ${config.packageManager}`);
|
||||
flags.push(config.install ? "--install" : "--no-install");
|
||||
|
||||
@@ -14,8 +14,9 @@ export async function handleDirectoryConflict(
|
||||
}> {
|
||||
while (true) {
|
||||
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
||||
const dirExists = fs.pathExistsSync(resolvedPath);
|
||||
const dirIsNotEmpty = dirExists && fs.readdirSync(resolvedPath).length > 0;
|
||||
const dirExists = await fs.pathExists(resolvedPath);
|
||||
const dirIsNotEmpty =
|
||||
dirExists && (await fs.readdir(resolvedPath)).length > 0;
|
||||
|
||||
if (!dirIsNotEmpty) {
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
|
||||
47
apps/cli/src/utils/project-name-validation.ts
Normal file
47
apps/cli/src/utils/project-name-validation.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import path from "node:path";
|
||||
import { ProjectNameSchema } from "../types";
|
||||
import { exitWithError } from "./errors";
|
||||
|
||||
export function validateProjectName(name: string): void {
|
||||
const result = ProjectNameSchema.safeParse(name);
|
||||
if (!result.success) {
|
||||
exitWithError(
|
||||
`Invalid project name: ${
|
||||
result.error.issues[0]?.message || "Invalid project name"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateProjectNameThrow(name: string): void {
|
||||
const result = ProjectNameSchema.safeParse(name);
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function extractAndValidateProjectName(
|
||||
projectName?: string,
|
||||
projectDirectory?: string,
|
||||
throwOnError = false,
|
||||
): string {
|
||||
const derivedName =
|
||||
projectName ||
|
||||
(projectDirectory
|
||||
? path.basename(path.resolve(process.cwd(), projectDirectory))
|
||||
: "");
|
||||
|
||||
if (!derivedName) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const nameToValidate = projectName ? path.basename(projectName) : derivedName;
|
||||
|
||||
if (throwOnError) {
|
||||
validateProjectNameThrow(nameToValidate);
|
||||
} else {
|
||||
validateProjectName(nameToValidate);
|
||||
}
|
||||
|
||||
return projectName || derivedName;
|
||||
}
|
||||
@@ -1,514 +1,85 @@
|
||||
import path from "node:path";
|
||||
import type { CLIInput, ProjectConfig } from "./types";
|
||||
import {
|
||||
type API,
|
||||
type Backend,
|
||||
type CLIInput,
|
||||
type Database,
|
||||
type DatabaseSetup,
|
||||
type ORM,
|
||||
type PackageManager,
|
||||
type ProjectConfig,
|
||||
ProjectNameSchema,
|
||||
type Runtime,
|
||||
type WebDeploy,
|
||||
} from "./types";
|
||||
getProvidedFlags,
|
||||
processFlags,
|
||||
validateArrayOptions,
|
||||
} from "./utils/config-processing";
|
||||
import {
|
||||
coerceBackendPresets,
|
||||
ensureSingleWebAndNative,
|
||||
incompatibleFlagsForBackend,
|
||||
isWebFrontend,
|
||||
validateAddonsAgainstFrontends,
|
||||
validateApiFrontendCompatibility,
|
||||
validateExamplesCompatibility,
|
||||
validateWebDeployRequiresWebFrontend,
|
||||
validateWorkersCompatibility,
|
||||
} from "./utils/compatibility-rules";
|
||||
validateConfigForProgrammaticUse,
|
||||
validateFullConfig,
|
||||
} from "./utils/config-validation";
|
||||
import { exitWithError } from "./utils/errors";
|
||||
|
||||
function processArrayOption<T>(options: (T | "none")[] | undefined): T[] {
|
||||
if (!options || options.length === 0) return [];
|
||||
if (options.includes("none" as T | "none")) return [];
|
||||
return options.filter((item): item is T => item !== "none");
|
||||
}
|
||||
|
||||
function deriveProjectName(
|
||||
projectName?: string,
|
||||
projectDirectory?: string,
|
||||
): string {
|
||||
if (projectName) {
|
||||
return projectName;
|
||||
}
|
||||
if (projectDirectory) {
|
||||
return path.basename(path.resolve(process.cwd(), projectDirectory));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function validateProjectName(name: string): void {
|
||||
const result = ProjectNameSchema.safeParse(name);
|
||||
if (!result.success) {
|
||||
exitWithError(
|
||||
`Invalid project name: ${
|
||||
result.error.issues[0]?.message || "Invalid project name"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
import { extractAndValidateProjectName } from "./utils/project-name-validation";
|
||||
|
||||
export function processAndValidateFlags(
|
||||
options: CLIInput,
|
||||
providedFlags: Set<string>,
|
||||
projectName?: string,
|
||||
): Partial<ProjectConfig> {
|
||||
const config: Partial<ProjectConfig> = {};
|
||||
|
||||
if (options.api) {
|
||||
config.api = options.api as API;
|
||||
if (options.api === "none") {
|
||||
if (
|
||||
options.examples &&
|
||||
!(options.examples.length === 1 && options.examples[0] === "none") &&
|
||||
options.backend !== "convex"
|
||||
) {
|
||||
exitWithError(
|
||||
"Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.",
|
||||
);
|
||||
}
|
||||
if (options.yolo) {
|
||||
const cfg = processFlags(options, projectName);
|
||||
const validatedProjectName = extractAndValidateProjectName(
|
||||
projectName,
|
||||
options.projectDirectory,
|
||||
true,
|
||||
);
|
||||
if (validatedProjectName) {
|
||||
cfg.projectName = validatedProjectName;
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
if (options.backend) {
|
||||
config.backend = options.backend as Backend;
|
||||
try {
|
||||
validateArrayOptions(options);
|
||||
} catch (error) {
|
||||
exitWithError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("backend") &&
|
||||
config.backend &&
|
||||
config.backend !== "convex" &&
|
||||
config.backend !== "none"
|
||||
) {
|
||||
if (providedFlags.has("runtime") && options.runtime === "none") {
|
||||
exitWithError(
|
||||
`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const config = processFlags(options, projectName);
|
||||
|
||||
if (options.database) {
|
||||
config.database = options.database as Database;
|
||||
}
|
||||
if (options.orm) {
|
||||
config.orm = options.orm as ORM;
|
||||
}
|
||||
if (options.auth !== undefined) {
|
||||
config.auth = options.auth;
|
||||
}
|
||||
if (options.git !== undefined) {
|
||||
config.git = options.git;
|
||||
}
|
||||
if (options.install !== undefined) {
|
||||
config.install = options.install;
|
||||
}
|
||||
if (options.runtime) {
|
||||
config.runtime = options.runtime as Runtime;
|
||||
}
|
||||
if (options.dbSetup) {
|
||||
config.dbSetup = options.dbSetup as DatabaseSetup;
|
||||
}
|
||||
if (options.packageManager) {
|
||||
config.packageManager = options.packageManager as PackageManager;
|
||||
}
|
||||
|
||||
if (options.webDeploy) {
|
||||
config.webDeploy = options.webDeploy as WebDeploy;
|
||||
}
|
||||
|
||||
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
||||
if (derivedName) {
|
||||
const nameToValidate = projectName
|
||||
? path.basename(projectName)
|
||||
: derivedName;
|
||||
validateProjectName(nameToValidate);
|
||||
config.projectName = projectName || derivedName;
|
||||
}
|
||||
|
||||
if (options.frontend && options.frontend.length > 0) {
|
||||
if (options.frontend.includes("none")) {
|
||||
if (options.frontend.length > 1) {
|
||||
exitWithError(`Cannot combine 'none' with other frontend options.`);
|
||||
}
|
||||
config.frontend = [];
|
||||
} else {
|
||||
const validOptions = processArrayOption(options.frontend);
|
||||
ensureSingleWebAndNative(validOptions);
|
||||
config.frontend = validOptions;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("api") &&
|
||||
providedFlags.has("frontend") &&
|
||||
config.api &&
|
||||
config.frontend &&
|
||||
config.frontend.length > 0
|
||||
) {
|
||||
validateApiFrontendCompatibility(config.api, config.frontend);
|
||||
}
|
||||
if (options.addons && options.addons.length > 0) {
|
||||
if (options.addons.includes("none")) {
|
||||
if (options.addons.length > 1) {
|
||||
exitWithError(`Cannot combine 'none' with other addons.`);
|
||||
}
|
||||
config.addons = [];
|
||||
} else {
|
||||
config.addons = processArrayOption(options.addons);
|
||||
}
|
||||
}
|
||||
if (options.examples && options.examples.length > 0) {
|
||||
if (options.examples.includes("none")) {
|
||||
if (options.examples.length > 1) {
|
||||
exitWithError("Cannot combine 'none' with other examples.");
|
||||
}
|
||||
config.examples = [];
|
||||
} else {
|
||||
config.examples = processArrayOption(options.examples);
|
||||
if (options.examples.includes("none") && config.backend !== "convex") {
|
||||
config.examples = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.backend === "convex" || config.backend === "none") {
|
||||
const incompatibleFlags = incompatibleFlagsForBackend(
|
||||
config.backend,
|
||||
providedFlags,
|
||||
options,
|
||||
);
|
||||
if (incompatibleFlags.length > 0) {
|
||||
exitWithError(
|
||||
`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(
|
||||
", ",
|
||||
)}. Please remove them.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
config.backend === "convex" &&
|
||||
providedFlags.has("frontend") &&
|
||||
options.frontend
|
||||
) {
|
||||
const incompatibleFrontends = options.frontend.filter(
|
||||
(f) => f === "solid",
|
||||
);
|
||||
if (incompatibleFrontends.length > 0) {
|
||||
exitWithError(
|
||||
`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(
|
||||
", ",
|
||||
)}. Please choose a different frontend or backend.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coerceBackendPresets(config);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("orm") &&
|
||||
providedFlags.has("database") &&
|
||||
config.orm === "mongoose" &&
|
||||
config.database !== "mongodb"
|
||||
) {
|
||||
exitWithError(
|
||||
"Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("database") &&
|
||||
providedFlags.has("orm") &&
|
||||
config.database === "mongodb" &&
|
||||
config.orm &&
|
||||
config.orm !== "mongoose" &&
|
||||
config.orm !== "prisma"
|
||||
) {
|
||||
exitWithError(
|
||||
"MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("orm") &&
|
||||
providedFlags.has("database") &&
|
||||
config.orm === "drizzle" &&
|
||||
config.database === "mongodb"
|
||||
) {
|
||||
exitWithError(
|
||||
"Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("database") &&
|
||||
providedFlags.has("orm") &&
|
||||
config.database &&
|
||||
config.database !== "none" &&
|
||||
config.orm === "none"
|
||||
) {
|
||||
exitWithError(
|
||||
"Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("orm") &&
|
||||
providedFlags.has("database") &&
|
||||
config.orm &&
|
||||
config.orm !== "none" &&
|
||||
config.database === "none"
|
||||
) {
|
||||
exitWithError(
|
||||
"ORM selection requires a database. Please choose a database or set '--orm none'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("auth") &&
|
||||
providedFlags.has("database") &&
|
||||
config.auth &&
|
||||
config.database === "none"
|
||||
) {
|
||||
exitWithError(
|
||||
"Authentication requires a database. Please choose a database or set '--no-auth'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
providedFlags.has("database") &&
|
||||
config.dbSetup &&
|
||||
config.dbSetup !== "none" &&
|
||||
config.database === "none"
|
||||
) {
|
||||
exitWithError(
|
||||
"Database setup requires a database. Please choose a database or set '--db-setup none'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
(config.database ? providedFlags.has("database") : true) &&
|
||||
config.dbSetup === "turso" &&
|
||||
config.database !== "sqlite"
|
||||
) {
|
||||
exitWithError(
|
||||
"Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
(config.database ? providedFlags.has("database") : true) &&
|
||||
config.dbSetup === "neon" &&
|
||||
config.database !== "postgres"
|
||||
) {
|
||||
exitWithError(
|
||||
"Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
(config.database ? providedFlags.has("database") : true) &&
|
||||
config.dbSetup === "prisma-postgres" &&
|
||||
config.database !== "postgres"
|
||||
) {
|
||||
exitWithError(
|
||||
"Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
(config.database ? providedFlags.has("database") : true) &&
|
||||
config.dbSetup === "mongodb-atlas" &&
|
||||
config.database !== "mongodb"
|
||||
) {
|
||||
exitWithError(
|
||||
"MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
(config.database ? providedFlags.has("database") : true) &&
|
||||
config.dbSetup === "supabase" &&
|
||||
config.database !== "postgres"
|
||||
) {
|
||||
exitWithError(
|
||||
"Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
);
|
||||
}
|
||||
|
||||
if (config.dbSetup === "d1") {
|
||||
if (
|
||||
(providedFlags.has("dbSetup") && providedFlags.has("database")) ||
|
||||
(providedFlags.has("dbSetup") && !config.database)
|
||||
) {
|
||||
if (config.database !== "sqlite") {
|
||||
exitWithError(
|
||||
"Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(providedFlags.has("dbSetup") && providedFlags.has("runtime")) ||
|
||||
(providedFlags.has("dbSetup") && !config.runtime)
|
||||
) {
|
||||
if (config.runtime !== "workers") {
|
||||
exitWithError(
|
||||
"Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
providedFlags.has("database") &&
|
||||
config.dbSetup === "docker" &&
|
||||
config.database === "sqlite"
|
||||
) {
|
||||
exitWithError(
|
||||
"Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
providedFlags.has("runtime") &&
|
||||
config.dbSetup === "docker" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
exitWithError(
|
||||
"Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
}
|
||||
|
||||
validateWorkersCompatibility(providedFlags, options, config);
|
||||
|
||||
const hasWebFrontendFlag = (config.frontend ?? []).some((f) =>
|
||||
isWebFrontend(f),
|
||||
const validatedProjectName = extractAndValidateProjectName(
|
||||
projectName,
|
||||
options.projectDirectory,
|
||||
false,
|
||||
);
|
||||
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
||||
if (validatedProjectName) {
|
||||
config.projectName = validatedProjectName;
|
||||
}
|
||||
|
||||
validateFullConfig(config, providedFlags, options);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function validateConfigCompatibility(config: Partial<ProjectConfig>) {
|
||||
const effectiveDatabase = config.database;
|
||||
const effectiveBackend = config.backend;
|
||||
const effectiveFrontend = config.frontend;
|
||||
const effectiveApi = config.api;
|
||||
|
||||
validateApiFrontendCompatibility(effectiveApi, effectiveFrontend);
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
validateAddonsAgainstFrontends(config.addons, effectiveFrontend);
|
||||
config.addons = [...new Set(config.addons)];
|
||||
}
|
||||
|
||||
validateExamplesCompatibility(
|
||||
config.examples ?? [],
|
||||
effectiveBackend,
|
||||
effectiveDatabase,
|
||||
effectiveFrontend ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
export function processProvidedFlagsWithoutValidation(
|
||||
options: CLIInput,
|
||||
projectName?: string,
|
||||
): Partial<ProjectConfig> {
|
||||
const config: Partial<ProjectConfig> = {};
|
||||
const config = processFlags(options, projectName);
|
||||
|
||||
if (options.api) {
|
||||
config.api = options.api as API;
|
||||
}
|
||||
|
||||
if (options.backend) {
|
||||
config.backend = options.backend as Backend;
|
||||
}
|
||||
|
||||
if (options.database) {
|
||||
config.database = options.database as Database;
|
||||
}
|
||||
|
||||
if (options.orm) {
|
||||
config.orm = options.orm as ORM;
|
||||
}
|
||||
|
||||
if (options.auth !== undefined) {
|
||||
config.auth = options.auth;
|
||||
}
|
||||
|
||||
if (options.git !== undefined) {
|
||||
config.git = options.git;
|
||||
}
|
||||
|
||||
if (options.install !== undefined) {
|
||||
config.install = options.install;
|
||||
}
|
||||
|
||||
if (options.runtime) {
|
||||
config.runtime = options.runtime as Runtime;
|
||||
}
|
||||
|
||||
if (options.dbSetup) {
|
||||
config.dbSetup = options.dbSetup as DatabaseSetup;
|
||||
}
|
||||
|
||||
if (options.packageManager) {
|
||||
config.packageManager = options.packageManager as PackageManager;
|
||||
}
|
||||
|
||||
if (options.webDeploy) {
|
||||
config.webDeploy = options.webDeploy as WebDeploy;
|
||||
}
|
||||
|
||||
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
||||
if (derivedName) {
|
||||
const nameToValidate = projectName
|
||||
? path.basename(projectName)
|
||||
: derivedName;
|
||||
const result = ProjectNameSchema.safeParse(nameToValidate);
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Invalid project name: ${result.error.issues[0]?.message}`,
|
||||
);
|
||||
}
|
||||
config.projectName = projectName || derivedName;
|
||||
}
|
||||
|
||||
if (options.frontend && options.frontend.length > 0) {
|
||||
config.frontend = processArrayOption(options.frontend);
|
||||
}
|
||||
|
||||
if (options.addons && options.addons.length > 0) {
|
||||
config.addons = processArrayOption(options.addons);
|
||||
}
|
||||
|
||||
if (options.examples && options.examples.length > 0) {
|
||||
config.examples = processArrayOption(options.examples);
|
||||
const validatedProjectName = extractAndValidateProjectName(
|
||||
projectName,
|
||||
options.projectDirectory,
|
||||
true,
|
||||
);
|
||||
if (validatedProjectName) {
|
||||
config.projectName = validatedProjectName;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function getProvidedFlags(options: CLIInput): Set<string> {
|
||||
return new Set(
|
||||
Object.keys(options).filter(
|
||||
(key) => options[key as keyof CLIInput] !== undefined,
|
||||
),
|
||||
);
|
||||
export function validateConfigCompatibility(
|
||||
config: Partial<ProjectConfig>,
|
||||
providedFlags?: Set<string>,
|
||||
options?: CLIInput,
|
||||
) {
|
||||
if (options?.yolo) return;
|
||||
if (options && providedFlags) {
|
||||
validateFullConfig(config, providedFlags, options);
|
||||
} else {
|
||||
validateConfigForProgrammaticUse(config);
|
||||
}
|
||||
}
|
||||
|
||||
export { getProvidedFlags };
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"!bts.jsonc",
|
||||
"!**/.expo",
|
||||
"!**/.wrangler",
|
||||
"!**/.alchemy",
|
||||
"!**/wrangler.jsonc",
|
||||
"!**/.source"
|
||||
]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@upstash/context7-mcp"]
|
||||
}{{#if (or (eq runtime "workers") (eq webDeploy "workers"))}},
|
||||
}{{#if (or (eq runtime "workers") (eq webDeploy "wrangler"))}},
|
||||
"cloudflare": {
|
||||
"command": "npx",
|
||||
"args": ["mcp-remote", "https://docs.mcp.cloudflare.com/sse"]
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"!bts.jsonc",
|
||||
"!**/.expo",
|
||||
"!**/.wrangler",
|
||||
"!**/.alchemy",
|
||||
"!**/wrangler.jsonc",
|
||||
"!**/.source"
|
||||
]
|
||||
|
||||
@@ -21,6 +21,13 @@ export const auth = betterAuth({
|
||||
],
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
advanced: {
|
||||
defaultCookieAttributes: {
|
||||
sameSite: "none",
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
},
|
||||
}
|
||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||
, plugins: [expo()]
|
||||
@@ -54,8 +61,13 @@ export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
secret: process.env.BETTER_AUTH_SECRET,
|
||||
baseURL: process.env.BETTER_AUTH_URL,
|
||||
advanced: {
|
||||
defaultCookieAttributes: {
|
||||
sameSite: "none",
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
},
|
||||
},
|
||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||
plugins: [expo()],
|
||||
{{/if}}
|
||||
@@ -73,7 +85,7 @@ import * as schema from "../db/schema/auth";
|
||||
import { env } from "cloudflare:workers";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
database: drizzleAdapter(db, {
|
||||
{{#if (eq database "postgres")}}provider: "pg",{{/if}}
|
||||
{{#if (eq database "sqlite")}}provider: "sqlite",{{/if}}
|
||||
{{#if (eq database "mysql")}}provider: "mysql",{{/if}}
|
||||
@@ -85,6 +97,13 @@ export const auth = betterAuth({
|
||||
},
|
||||
secret: env.BETTER_AUTH_SECRET,
|
||||
baseURL: env.BETTER_AUTH_URL,
|
||||
advanced: {
|
||||
defaultCookieAttributes: {
|
||||
sameSite: "none",
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
},
|
||||
},
|
||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||
plugins: [expo()],
|
||||
{{/if}}
|
||||
@@ -110,6 +129,13 @@ export const auth = betterAuth({
|
||||
],
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
advanced: {
|
||||
defaultCookieAttributes: {
|
||||
sameSite: "none",
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
},
|
||||
}
|
||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||
, plugins: [expo()]
|
||||
@@ -133,9 +159,16 @@ export const auth = betterAuth({
|
||||
],
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
advanced: {
|
||||
defaultCookieAttributes: {
|
||||
sameSite: "none",
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
},
|
||||
}
|
||||
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
||||
, plugins: [expo()]
|
||||
{{/if}}
|
||||
});
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
@@ -16,6 +16,7 @@ dist/
|
||||
.idea/usage.statistics.xml
|
||||
.idea/shelf
|
||||
.wrangler
|
||||
.alchemy
|
||||
/.next/
|
||||
.vercel
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{else if (eq runtime "bun")}}
|
||||
"bun"
|
||||
{{else if (eq runtime "workers")}}
|
||||
"./worker-configuration",
|
||||
"@cloudflare/workers-types",
|
||||
"node"
|
||||
{{else}}
|
||||
"node",
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
node_modules
|
||||
.turbo
|
||||
.alchemy
|
||||
.env
|
||||
@@ -7,11 +7,13 @@ export default defineConfig({
|
||||
// DOCS: https://orm.drizzle.team/docs/guides/d1-http-with-drizzle-kit
|
||||
dialect: "sqlite",
|
||||
driver: "d1-http",
|
||||
{{#if (eq serverDeploy "wrangler")}}
|
||||
dbCredentials: {
|
||||
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
|
||||
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
|
||||
token: process.env.CLOUDFLARE_D1_TOKEN!,
|
||||
},
|
||||
{{/if}}
|
||||
{{else}}
|
||||
dialect: "turso",
|
||||
dbCredentials: {
|
||||
|
||||
208
apps/cli/templates/deploy/alchemy/alchemy.run.ts.hbs
Normal file
208
apps/cli/templates/deploy/alchemy/alchemy.run.ts.hbs
Normal file
@@ -0,0 +1,208 @@
|
||||
import alchemy from "alchemy";
|
||||
{{#if (eq webDeploy "alchemy")}}
|
||||
{{#if (includes frontend "next")}}
|
||||
import { Next } from "alchemy/cloudflare";
|
||||
{{else if (includes frontend "nuxt")}}
|
||||
import { Nuxt } from "alchemy/cloudflare";
|
||||
{{else if (includes frontend "svelte")}}
|
||||
import { SvelteKit } from "alchemy/cloudflare";
|
||||
{{else if (includes frontend "tanstack-start")}}
|
||||
import { TanStackStart } from "alchemy/cloudflare";
|
||||
{{else if (includes frontend "tanstack-router")}}
|
||||
import { Vite } from "alchemy/cloudflare";
|
||||
{{else if (includes frontend "react-router")}}
|
||||
import { ReactRouter } from "alchemy/cloudflare";
|
||||
{{else if (includes frontend "solid")}}
|
||||
import { Vite } from "alchemy/cloudflare";
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (eq serverDeploy "alchemy")}}
|
||||
import { Worker, WranglerJson } from "alchemy/cloudflare";
|
||||
{{#if (eq dbSetup "d1")}}
|
||||
import { D1Database } from "alchemy/cloudflare";
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (and (eq serverDeploy "alchemy") (eq dbSetup "d1"))}}
|
||||
import { Exec } from "alchemy/os";
|
||||
{{/if}}
|
||||
import { config } from "dotenv";
|
||||
|
||||
{{#if (and (eq webDeploy "alchemy") (eq serverDeploy "alchemy"))}}
|
||||
config({ path: "./.env" });
|
||||
config({ path: "./apps/web/.env" });
|
||||
config({ path: "./apps/server/.env" });
|
||||
{{else if (or (eq webDeploy "alchemy") (eq serverDeploy "alchemy"))}}
|
||||
config({ path: "./.env" });
|
||||
{{/if}}
|
||||
|
||||
const app = await alchemy("{{projectName}}");
|
||||
|
||||
{{#if (and (eq serverDeploy "alchemy") (eq dbSetup "d1"))}}
|
||||
await Exec("db-generate", {
|
||||
{{#if (and (eq webDeploy "alchemy") (eq serverDeploy "alchemy"))}}cwd: "apps/server",{{/if}}
|
||||
command: "{{packageManager}} run db:generate",
|
||||
});
|
||||
|
||||
const db = await D1Database("database", {
|
||||
name: `${app.name}-${app.stage}-db`,
|
||||
migrationsDir: "apps/server/src/db/migrations",
|
||||
});
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq webDeploy "alchemy")}}
|
||||
{{#if (includes frontend "next")}}
|
||||
export const web = await Next("web", {
|
||||
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
|
||||
name: `${app.name}-${app.stage}-web`,
|
||||
bindings: {
|
||||
{{#if (eq backend "convex")}}
|
||||
NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL || "",
|
||||
{{else}}
|
||||
NEXT_PUBLIC_SERVER_URL: process.env.NEXT_PUBLIC_SERVER_URL || "",
|
||||
{{/if}}
|
||||
},
|
||||
dev: {
|
||||
command: "{{packageManager}} run dev"
|
||||
}
|
||||
});
|
||||
{{else if (includes frontend "nuxt")}}
|
||||
export const web = await Nuxt("web", {
|
||||
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
|
||||
name: `${app.name}-${app.stage}-web`,
|
||||
bindings: {
|
||||
{{#if (eq backend "convex")}}
|
||||
NUXT_PUBLIC_CONVEX_URL: process.env.NUXT_PUBLIC_CONVEX_URL || "",
|
||||
{{else}}
|
||||
NUXT_PUBLIC_SERVER_URL: process.env.NUXT_PUBLIC_SERVER_URL || "",
|
||||
{{/if}}
|
||||
},
|
||||
dev: {
|
||||
command: "{{packageManager}} run dev"
|
||||
}
|
||||
});
|
||||
{{else if (includes frontend "svelte")}}
|
||||
export const web = await SvelteKit("web", {
|
||||
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
|
||||
name: `${app.name}-${app.stage}-web`,
|
||||
bindings: {
|
||||
{{#if (eq backend "convex")}}
|
||||
PUBLIC_CONVEX_URL: process.env.PUBLIC_CONVEX_URL || "",
|
||||
{{else}}
|
||||
PUBLIC_SERVER_URL: process.env.PUBLIC_SERVER_URL || "",
|
||||
{{/if}}
|
||||
},
|
||||
dev: {
|
||||
command: "{{packageManager}} run dev"
|
||||
}
|
||||
});
|
||||
{{else if (includes frontend "tanstack-start")}}
|
||||
export const web = await TanStackStart("web", {
|
||||
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
|
||||
name: `${app.name}-${app.stage}-web`,
|
||||
bindings: {
|
||||
{{#if (eq backend "convex")}}
|
||||
VITE_CONVEX_URL: process.env.VITE_CONVEX_URL || "",
|
||||
{{else}}
|
||||
VITE_SERVER_URL: process.env.VITE_SERVER_URL || "",
|
||||
{{/if}}
|
||||
},
|
||||
dev: {
|
||||
command: "{{packageManager}} run dev"
|
||||
}
|
||||
});
|
||||
{{else if (includes frontend "tanstack-router")}}
|
||||
export const web = await Vite("web", {
|
||||
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
|
||||
name: `${app.name}-${app.stage}-web`,
|
||||
assets: "dist",
|
||||
bindings: {
|
||||
{{#if (eq backend "convex")}}
|
||||
VITE_CONVEX_URL: process.env.VITE_CONVEX_URL || "",
|
||||
{{else}}
|
||||
VITE_SERVER_URL: process.env.VITE_SERVER_URL || "",
|
||||
{{/if}}
|
||||
},
|
||||
dev: {
|
||||
command: "{{packageManager}} run dev"
|
||||
}
|
||||
});
|
||||
{{else if (includes frontend "react-router")}}
|
||||
export const web = await ReactRouter("web", {
|
||||
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
|
||||
name: `${app.name}-${app.stage}-web`,
|
||||
bindings: {
|
||||
{{#if (eq backend "convex")}}
|
||||
VITE_CONVEX_URL: process.env.VITE_CONVEX_URL || "",
|
||||
{{else}}
|
||||
VITE_SERVER_URL: process.env.VITE_SERVER_URL || "",
|
||||
{{/if}}
|
||||
},
|
||||
dev: {
|
||||
command: "{{packageManager}} run dev"
|
||||
}
|
||||
});
|
||||
{{else if (includes frontend "solid")}}
|
||||
export const web = await Vite("web", {
|
||||
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
|
||||
name: `${app.name}-${app.stage}-web`,
|
||||
assets: "dist",
|
||||
bindings: {
|
||||
{{#if (eq backend "convex")}}
|
||||
VITE_CONVEX_URL: process.env.VITE_CONVEX_URL || "",
|
||||
{{else}}
|
||||
VITE_SERVER_URL: process.env.VITE_SERVER_URL || "",
|
||||
{{/if}}
|
||||
},
|
||||
dev: {
|
||||
command: "{{packageManager}} run dev"
|
||||
}
|
||||
});
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq serverDeploy "alchemy")}}
|
||||
export const server = await Worker("server", {
|
||||
{{#if (eq webDeploy "alchemy")}}cwd: "apps/server",{{/if}}
|
||||
name: `${app.name}-${app.stage}`,
|
||||
entrypoint: "src/index.ts",
|
||||
compatibility: "node",
|
||||
bindings: {
|
||||
{{#if (eq dbSetup "d1")}}
|
||||
DB: db,
|
||||
{{else if (and (ne database "none") (ne dbSetup "none"))}}
|
||||
DATABASE_URL: alchemy.secret(process.env.DATABASE_URL),
|
||||
{{/if}}
|
||||
CORS_ORIGIN: process.env.CORS_ORIGIN || "",
|
||||
{{#if auth}}
|
||||
BETTER_AUTH_SECRET: alchemy.secret(process.env.BETTER_AUTH_SECRET),
|
||||
BETTER_AUTH_URL: process.env.BETTER_AUTH_URL || "",
|
||||
{{/if}}
|
||||
{{#if (includes examples "ai")}}
|
||||
GOOGLE_GENERATIVE_AI_API_KEY: alchemy.secret(process.env.GOOGLE_GENERATIVE_AI_API_KEY),
|
||||
{{/if}}
|
||||
{{#if (eq dbSetup "turso")}}
|
||||
DATABASE_AUTH_TOKEN: alchemy.secret(process.env.DATABASE_AUTH_TOKEN),
|
||||
{{/if}}
|
||||
},
|
||||
dev: {
|
||||
port: 3000,
|
||||
},
|
||||
});
|
||||
|
||||
await WranglerJson("wrangler", {
|
||||
worker: server,
|
||||
});
|
||||
{{/if}}
|
||||
|
||||
|
||||
|
||||
{{#if (and (eq webDeploy "alchemy") (eq serverDeploy "alchemy"))}}
|
||||
console.log(`Web -> ${web.url}`);
|
||||
console.log(`Server -> ${server.url}`);
|
||||
{{else if (eq webDeploy "alchemy")}}
|
||||
console.log(`Web -> ${web.url}`);
|
||||
{{else if (eq serverDeploy "alchemy")}}
|
||||
console.log(`Server -> ${server.url}`);
|
||||
{{/if}}
|
||||
|
||||
await app.finalize();
|
||||
20
apps/cli/templates/deploy/alchemy/env.d.ts.hbs
Normal file
20
apps/cli/templates/deploy/alchemy/env.d.ts.hbs
Normal file
@@ -0,0 +1,20 @@
|
||||
// This file infers types for the cloudflare:workers environment from your Alchemy Worker.
|
||||
// @see https://alchemy.run/concepts/bindings/#type-safe-bindings
|
||||
|
||||
{{#if (eq webDeploy "alchemy")}}
|
||||
import type { server } from "../../alchemy.run";
|
||||
{{else}}
|
||||
import type { server } from "./alchemy.run";
|
||||
{{/if}}
|
||||
|
||||
export type CloudflareEnv = typeof server.Env;
|
||||
|
||||
declare global {
|
||||
type Env = CloudflareEnv;
|
||||
}
|
||||
|
||||
declare module "cloudflare:workers" {
|
||||
namespace Cloudflare {
|
||||
export interface Env extends CloudflareEnv {}
|
||||
}
|
||||
}
|
||||
11
apps/cli/templates/deploy/alchemy/wrangler.jsonc.hbs
Normal file
11
apps/cli/templates/deploy/alchemy/wrangler.jsonc.hbs
Normal file
@@ -0,0 +1,11 @@
|
||||
// This is a temporary wrangler.jsonc file that will be overwritten by alchemy
|
||||
// It's only here so that `wrangler dev` can work or use alchemy dev instead
|
||||
{
|
||||
"name": "{{projectName}}",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-08-16",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"nodejs_compat_populate_process_env"
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||
*/
|
||||
{
|
||||
"$schema": "../../node_modules/wrangler/config-schema.json",
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "{{projectName}}",
|
||||
"main": "./.output/server/index.mjs",
|
||||
"compatibility_date": "2025-07-01",
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "../../node_modules/wrangler/config-schema.json",
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"main": ".open-next/worker.js",
|
||||
"name": "{{projectName}}",
|
||||
"compatibility_date": "2025-07-05",
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "../../node_modules/wrangler/config-schema.json",
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "{{projectName}}",
|
||||
"compatibility_date": "2025-04-03",
|
||||
"assets": {
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "../../node_modules/wrangler/config-schema.json",
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "{{projectName}}",
|
||||
"compatibility_date": "2025-04-03",
|
||||
"assets": {
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "../../node_modules/wrangler/config-schema.json",
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "{{projectName}}",
|
||||
"main": ".output/server/index.mjs",
|
||||
"compatibility_date": "2025-07-05",
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "../../node_modules/wrangler/config-schema.json",
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "{{projectName}}",
|
||||
"compatibility_date": "2025-04-03",
|
||||
"assets": {
|
||||
@@ -3,7 +3,7 @@
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||
*/
|
||||
{
|
||||
"$schema": "../../node_modules/wrangler/config-schema.json",
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "{{projectName}}",
|
||||
"main": ".svelte-kit/cloudflare/_worker.js",
|
||||
"compatibility_date": "2025-07-05",
|
||||
@@ -5,6 +5,8 @@
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
.wrangler
|
||||
.alchemy
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
@@ -22,3 +24,4 @@ logs
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
{{#unless (or (eq backend "convex") (eq backend "none"))}}
|
||||
,
|
||||
}{{#unless (or (eq backend "convex") (eq backend "none"))}},
|
||||
{
|
||||
"path": "../server"
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
.alchemy
|
||||
|
||||
# Environment & local files
|
||||
.env*
|
||||
|
||||
@@ -6,7 +6,6 @@ import { NavLink } from "react-router";
|
||||
{{else if (or (includes frontend "tanstack-router") (includes frontend "tanstack-start"))}}
|
||||
import { Link } from "@tanstack/react-router";
|
||||
{{/if}}
|
||||
|
||||
{{#unless (includes frontend "tanstack-start")}}
|
||||
import { ModeToggle } from "./mode-toggle";
|
||||
{{/unless}}
|
||||
|
||||
@@ -7,4 +7,5 @@ dist-ssr
|
||||
.env.*
|
||||
|
||||
.wrangler
|
||||
.alchemy
|
||||
.dev.vars*
|
||||
@@ -13,7 +13,6 @@
|
||||
"@tanstack/router-plugin": "^1.109.2",
|
||||
"@tanstack/solid-form": "^1.9.0",
|
||||
"@tanstack/solid-router": "^1.110.0",
|
||||
"@tanstack/solid-router-devtools": "^1.109.2",
|
||||
"lucide-solid": "^0.507.0",
|
||||
"solid-js": "^1.9.4",
|
||||
"tailwindcss": "^4.0.6",
|
||||
|
||||
@@ -5,6 +5,7 @@ node_modules
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
.alchemy
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
|
||||
@@ -12,20 +12,18 @@
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.20.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"svelte": "^5.28.2",
|
||||
"svelte-check": "^4.1.6",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"typescript": "^5.8.3",
|
||||
"@tanstack/svelte-query-devtools": "^5.74.6",
|
||||
"vite": "^7.0.2"
|
||||
"@sveltejs/adapter-auto": "^6.1.0",
|
||||
"@sveltejs/kit": "^2.31.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.1.2",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"svelte": "^5.38.1",
|
||||
"svelte-check": "^4.3.1",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/svelte-form": "^1.7.0",
|
||||
"@tanstack/svelte-query": "^5.74.4",
|
||||
"zod": "^4.0.2"
|
||||
"@tanstack/svelte-form": "^1.19.2",
|
||||
"zod": "^4.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { join } from "node:path";
|
||||
import consola from "consola";
|
||||
import { execa } from "execa";
|
||||
import {
|
||||
ensureDirSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
readJsonSync,
|
||||
removeSync,
|
||||
} from "fs-extra";
|
||||
import { ensureDir, existsSync, readFile, readJson, remove } from "fs-extra";
|
||||
import * as JSONC from "jsonc-parser";
|
||||
import { FailedToExitError } from "trpc-cli";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
@@ -17,6 +11,8 @@ async function runCli(argv: string[], cwd: string) {
|
||||
const previous = process.cwd();
|
||||
process.chdir(cwd);
|
||||
try {
|
||||
consola.info(`Running CLI command: bts ${argv.join(" ")}`);
|
||||
|
||||
const cli = createBtsCli();
|
||||
await cli
|
||||
.run({
|
||||
@@ -37,12 +33,12 @@ async function runCli(argv: string[], cwd: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function createTmpDir(_prefix: string) {
|
||||
async function createTmpDir(_prefix: string) {
|
||||
const dir = join(__dirname, "..", ".smoke");
|
||||
if (existsSync(dir)) {
|
||||
removeSync(dir);
|
||||
await remove(dir);
|
||||
}
|
||||
ensureDirSync(dir);
|
||||
await ensureDir(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -50,6 +46,10 @@ async function runCliExpectingError(args: string[], cwd: string) {
|
||||
const previous = process.cwd();
|
||||
process.chdir(cwd);
|
||||
try {
|
||||
consola.info(
|
||||
`Running CLI command (expecting error): bts ${args.join(" ")}`,
|
||||
);
|
||||
|
||||
const cli = createBtsCli();
|
||||
let threw = false;
|
||||
await cli
|
||||
@@ -72,15 +72,15 @@ async function runCliExpectingError(args: string[], cwd: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function assertScaffoldedProject(dir: string) {
|
||||
async function assertScaffoldedProject(dir: string) {
|
||||
const pkgJsonPath = join(dir, "package.json");
|
||||
expect(existsSync(pkgJsonPath)).toBe(true);
|
||||
const pkg = readJsonSync(pkgJsonPath);
|
||||
const pkg = await readJson(pkgJsonPath);
|
||||
expect(typeof pkg.name).toBe("string");
|
||||
expect(Array.isArray(pkg.workspaces)).toBe(true);
|
||||
}
|
||||
|
||||
function assertProjectStructure(
|
||||
async function assertProjectStructure(
|
||||
dir: string,
|
||||
options: {
|
||||
hasWeb?: boolean;
|
||||
@@ -107,6 +107,13 @@ function assertProjectStructure(
|
||||
expect(existsSync(join(dir, "package.json"))).toBe(true);
|
||||
expect(existsSync(join(dir, ".gitignore"))).toBe(true);
|
||||
|
||||
try {
|
||||
const pmConfig = (await readBtsConfig(dir)) as { packageManager?: string };
|
||||
if (pmConfig && pmConfig.packageManager === "bun") {
|
||||
expect(existsSync(join(dir, "bunfig.toml"))).toBe(true);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (hasWeb) {
|
||||
expect(existsSync(join(dir, "apps", "web", "package.json"))).toBe(true);
|
||||
const webDir = join(dir, "apps", "web");
|
||||
@@ -132,6 +139,26 @@ function assertProjectStructure(
|
||||
hasAppDir ||
|
||||
hasPublicDir,
|
||||
).toBe(true);
|
||||
|
||||
const bts = (await readBtsConfig(dir)) as {
|
||||
webDeploy?: string;
|
||||
serverDeploy?: string;
|
||||
frontend?: string[];
|
||||
};
|
||||
if (bts.webDeploy === "wrangler") {
|
||||
expect(existsSync(join(dir, "apps", "web", "wrangler.jsonc"))).toBe(true);
|
||||
}
|
||||
|
||||
if (
|
||||
bts.webDeploy === "alchemy" &&
|
||||
bts.serverDeploy !== "alchemy" &&
|
||||
bts.frontend &&
|
||||
bts.frontend.length > 0
|
||||
) {
|
||||
const webRunner = join(dir, "apps", "web", "alchemy.run.ts");
|
||||
consola.info(`Checking Alchemy web runner at: ${webRunner}`);
|
||||
expect(existsSync(webRunner)).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNative) {
|
||||
@@ -154,8 +181,42 @@ function assertProjectStructure(
|
||||
expect(existsSync(join(dir, "apps", "server", "src", "index.ts"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(existsSync(join(dir, "apps", "server", "tsconfig.json"))).toBe(true);
|
||||
|
||||
const bts = (await readBtsConfig(dir)) as {
|
||||
serverDeploy?: string;
|
||||
webDeploy?: string;
|
||||
};
|
||||
if (bts.serverDeploy === "wrangler") {
|
||||
expect(existsSync(join(dir, "apps", "server", "wrangler.jsonc"))).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
if (bts.serverDeploy === "alchemy") {
|
||||
const serverRunner = join(dir, "apps", "server", "alchemy.run.ts");
|
||||
const serverEnv = join(dir, "apps", "server", "env.d.ts");
|
||||
consola.info(`Checking Alchemy server runner at: ${serverRunner}`);
|
||||
consola.info(`Checking Alchemy env types at: ${serverEnv}`);
|
||||
expect(existsSync(serverRunner)).toBe(true);
|
||||
expect(existsSync(serverEnv)).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const btsAll = (await readBtsConfig(dir)) as {
|
||||
serverDeploy?: string;
|
||||
webDeploy?: string;
|
||||
};
|
||||
if (btsAll.serverDeploy === "alchemy" && btsAll.webDeploy === "alchemy") {
|
||||
const rootRunner = join(dir, "alchemy.run.ts");
|
||||
const serverEnv = join(dir, "apps", "server", "env.d.ts");
|
||||
consola.info(`Checking Alchemy root runner at: ${rootRunner}`);
|
||||
consola.info(`Checking Alchemy env types at: ${serverEnv}`);
|
||||
expect(existsSync(rootRunner)).toBe(true);
|
||||
expect(existsSync(serverEnv)).toBe(true);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (hasConvexBackend) {
|
||||
const hasPackagesDir = existsSync(join(dir, "packages"));
|
||||
const hasConvexRelated =
|
||||
@@ -208,11 +269,11 @@ function assertProjectStructure(
|
||||
}
|
||||
|
||||
expect(existsSync(join(dir, "bts.jsonc"))).toBe(true);
|
||||
const btsConfig = readFileSync(join(dir, "bts.jsonc"), "utf8");
|
||||
const btsConfig = await readFile(join(dir, "bts.jsonc"), "utf8");
|
||||
expect(btsConfig).toContain("Better-T-Stack configuration");
|
||||
}
|
||||
|
||||
function assertBtsConfig(
|
||||
async function assertBtsConfig(
|
||||
dir: string,
|
||||
expectedConfig: Partial<{
|
||||
frontend: string[];
|
||||
@@ -225,11 +286,13 @@ function assertBtsConfig(
|
||||
api: string;
|
||||
runtime: string;
|
||||
packageManager: string;
|
||||
webDeploy: string;
|
||||
serverDeploy: string;
|
||||
}>,
|
||||
) {
|
||||
const btsConfigPath = join(dir, "bts.jsonc");
|
||||
expect(existsSync(btsConfigPath)).toBe(true);
|
||||
const content = readFileSync(btsConfigPath, "utf8");
|
||||
const content = await readFile(btsConfigPath, "utf8");
|
||||
|
||||
type BtsConfig = {
|
||||
frontend?: string[];
|
||||
@@ -242,6 +305,8 @@ function assertBtsConfig(
|
||||
api?: string;
|
||||
runtime?: string;
|
||||
packageManager?: string;
|
||||
webDeploy?: string;
|
||||
serverDeploy?: string;
|
||||
};
|
||||
|
||||
const errors: JSONC.ParseError[] = [];
|
||||
@@ -286,13 +351,19 @@ function assertBtsConfig(
|
||||
if (expectedConfig.packageManager) {
|
||||
expect(config.packageManager).toBe(expectedConfig.packageManager);
|
||||
}
|
||||
if (expectedConfig.webDeploy) {
|
||||
expect(config.webDeploy).toBe(expectedConfig.webDeploy);
|
||||
}
|
||||
if (expectedConfig.serverDeploy) {
|
||||
expect(config.serverDeploy).toBe(expectedConfig.serverDeploy);
|
||||
}
|
||||
}
|
||||
|
||||
function readBtsConfig(dir: string) {
|
||||
async function readBtsConfig(dir: string) {
|
||||
const btsConfigPath = join(dir, "bts.jsonc");
|
||||
if (!existsSync(btsConfigPath)) return {} as Record<string, unknown>;
|
||||
|
||||
const content = readFileSync(btsConfigPath, "utf8");
|
||||
const content = await readFile(btsConfigPath, "utf8");
|
||||
const errors: JSONC.ParseError[] = [];
|
||||
const parsed = JSONC.parse(content, errors, {
|
||||
allowTrailingComma: true,
|
||||
@@ -309,7 +380,7 @@ describe("create-better-t-stack smoke", () => {
|
||||
let workdir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
workdir = createTmpDir("cli");
|
||||
workdir = await createTmpDir("cli");
|
||||
consola.start("Building CLI...");
|
||||
const buildProc = execa("bun", ["run", "build"], {
|
||||
cwd: join(__dirname, ".."),
|
||||
@@ -329,7 +400,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
consola.info("Programmatic CLI mode");
|
||||
});
|
||||
|
||||
// Exhaustive matrix: all frontends x standard backends (no db, no orm, no api, no auth)
|
||||
describe("frontend x backend matrix (no db, no api)", () => {
|
||||
const FRONTENDS = [
|
||||
"tanstack-router",
|
||||
@@ -391,15 +461,15 @@ describe("create-better-t-stack smoke", () => {
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
assertScaffoldedProject(projectDir);
|
||||
assertProjectStructure(projectDir, {
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertProjectStructure(projectDir, {
|
||||
hasWeb: WEB_FRONTENDS.has(frontend),
|
||||
hasNative:
|
||||
frontend === "native-nativewind" ||
|
||||
frontend === "native-unistyles",
|
||||
hasServer: true,
|
||||
});
|
||||
assertBtsConfig(projectDir, {
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: [frontend],
|
||||
backend,
|
||||
database: "none",
|
||||
@@ -474,9 +544,9 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
try {
|
||||
removeSync(workdir);
|
||||
await remove(workdir);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
@@ -1101,6 +1171,52 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds with PostgreSQL + Drizzle", async () => {
|
||||
const projectName = "app-postgres-drizzle";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"bun",
|
||||
"--database",
|
||||
"postgres",
|
||||
"--orm",
|
||||
"drizzle",
|
||||
"--api",
|
||||
"trpc",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
assertScaffoldedProject(projectDir);
|
||||
assertProjectStructure(projectDir, {
|
||||
hasWeb: true,
|
||||
hasServer: true,
|
||||
hasDatabase: true,
|
||||
});
|
||||
assertBtsConfig(projectDir, {
|
||||
database: "postgres",
|
||||
orm: "drizzle",
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds with MongoDB + Mongoose", async () => {
|
||||
const projectName = "app-mongo-mongoose";
|
||||
await runCli(
|
||||
@@ -1457,6 +1573,116 @@ describe("create-better-t-stack smoke", () => {
|
||||
workdir,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects Turso db-setup with non-SQLite database", async () => {
|
||||
await runCliExpectingError(
|
||||
[
|
||||
"invalid-combo",
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"bun",
|
||||
"--database",
|
||||
"postgres",
|
||||
"--orm",
|
||||
"prisma",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"turso",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("YOLO mode", () => {
|
||||
it("bypasses db-setup/database validation (Turso + Postgres + Prisma)", async () => {
|
||||
const projectName = "app-yolo-turso-postgres";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"bun",
|
||||
"--database",
|
||||
"postgres",
|
||||
"--orm",
|
||||
"prisma",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"turso",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
"--yolo",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
database: "postgres",
|
||||
orm: "prisma",
|
||||
});
|
||||
});
|
||||
|
||||
it("bypasses web-deploy requires web frontend (none + wrangler)", async () => {
|
||||
const projectName = "app-yolo-webdeploy-no-frontend";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"none",
|
||||
"--backend",
|
||||
"none",
|
||||
"--web-deploy",
|
||||
"wrangler",
|
||||
"--addons",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
"--yolo",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
backend: "none",
|
||||
webDeploy: "wrangler",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("runtime compatibility", () => {
|
||||
@@ -1500,10 +1726,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
runtime: "workers",
|
||||
orm: "drizzle",
|
||||
});
|
||||
|
||||
expect(
|
||||
existsSync(join(projectDir, "apps", "server", "wrangler.jsonc")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects incompatible runtime and backend combinations", async () => {
|
||||
@@ -1810,7 +2032,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Git and install flag variations
|
||||
it("scaffolds with git enabled", async () => {
|
||||
const projectName = "app-with-git";
|
||||
await runCli(
|
||||
@@ -1855,6 +2076,8 @@ describe("create-better-t-stack smoke", () => {
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--directory-conflict",
|
||||
"overwrite",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
@@ -1887,7 +2110,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
expect(existsSync(join(projectDir, "node_modules"))).toBe(true);
|
||||
});
|
||||
|
||||
// Additional addons beyond turborepo and biome
|
||||
it("scaffolds with PWA addon", async () => {
|
||||
const projectName = "app-addon-pwa";
|
||||
await runCli(
|
||||
@@ -2008,7 +2230,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Authentication combinations
|
||||
it("scaffolds with authentication enabled", async () => {
|
||||
const projectName = "app-with-auth";
|
||||
await runCli(
|
||||
@@ -2055,7 +2276,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// MySQL database
|
||||
it("scaffolds with MySQL + Prisma", async () => {
|
||||
const projectName = "app-mysql-prisma";
|
||||
await runCli(
|
||||
@@ -2138,7 +2358,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// oRPC API with more frontends
|
||||
it("scaffolds oRPC with Next.js", async () => {
|
||||
const projectName = "app-orpc-next";
|
||||
await runCli(
|
||||
@@ -2303,7 +2522,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Backend next combinations
|
||||
it("scaffolds with Next.js backend", async () => {
|
||||
const projectName = "app-backend-next";
|
||||
await runCli(
|
||||
@@ -2345,7 +2563,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Node runtime combinations
|
||||
it("scaffolds with Node runtime", async () => {
|
||||
const projectName = "app-node-runtime";
|
||||
await runCli(
|
||||
@@ -2459,14 +2676,16 @@ describe("create-better-t-stack smoke", () => {
|
||||
"app-orpc-solid",
|
||||
"app-backend-next",
|
||||
"app-node-runtime",
|
||||
].forEach((n) => projectNames.add(n));
|
||||
].forEach((n) => {
|
||||
projectNames.add(n);
|
||||
});
|
||||
|
||||
const detectPackageManager = (
|
||||
const detectPackageManager = async (
|
||||
projectDir: string,
|
||||
): "bun" | "pnpm" | "npm" => {
|
||||
): Promise<"bun" | "pnpm" | "npm"> => {
|
||||
const bts = readBtsConfig(projectDir) as { packageManager?: string };
|
||||
const pkgJsonPath = join(projectDir, "package.json");
|
||||
const pkg = existsSync(pkgJsonPath) ? readJsonSync(pkgJsonPath) : {};
|
||||
const pkg = existsSync(pkgJsonPath) ? await readJson(pkgJsonPath) : {};
|
||||
const pkgMgrField =
|
||||
(pkg.packageManager as string | undefined) || bts.packageManager;
|
||||
|
||||
@@ -2531,7 +2750,7 @@ describe("create-better-t-stack smoke", () => {
|
||||
consola.info(`${dirName} not found, skipping`);
|
||||
return;
|
||||
}
|
||||
const pm = detectPackageManager(projectDir);
|
||||
const pm = await detectPackageManager(projectDir);
|
||||
|
||||
consola.info(`Processing ${dirName} (pm=${pm})`);
|
||||
try {
|
||||
@@ -2552,11 +2771,11 @@ describe("create-better-t-stack smoke", () => {
|
||||
}
|
||||
|
||||
const pkgJsonPath = join(projectDir, "package.json");
|
||||
const pkg = readJsonSync(pkgJsonPath);
|
||||
const pkg = await readJson(pkgJsonPath);
|
||||
const scripts = pkg.scripts || {};
|
||||
consola.info(`Scripts: ${Object.keys(scripts).join(", ")}`);
|
||||
|
||||
const bts = readBtsConfig(projectDir) as {
|
||||
const bts = (await readBtsConfig(projectDir)) as {
|
||||
backend?: string;
|
||||
frontend?: string[];
|
||||
};
|
||||
@@ -2603,33 +2822,25 @@ describe("create-better-t-stack smoke", () => {
|
||||
|
||||
if (scripts["check-types"]) {
|
||||
consola.start(`Type checking ${dirName}...`);
|
||||
try {
|
||||
const typeRes = await runScript(
|
||||
pm,
|
||||
projectDir,
|
||||
"check-types",
|
||||
[],
|
||||
120_000,
|
||||
);
|
||||
if (typeRes.exitCode === 0) {
|
||||
consola.success(`${dirName} type check passed`);
|
||||
} else {
|
||||
consola.warn(
|
||||
`${dirName} type check failed (exit code ${typeRes.exitCode}) - likely due to missing generated files`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
consola.warn(
|
||||
`${dirName} type check failed - likely due to missing generated files:`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
const typeRes = await runScript(
|
||||
pm,
|
||||
projectDir,
|
||||
"check-types",
|
||||
[],
|
||||
120_000,
|
||||
);
|
||||
expect(typeRes.exitCode).toBe(0);
|
||||
consola.success(`${dirName} type check passed`);
|
||||
}
|
||||
|
||||
if (!scripts.build && !scripts["check-types"]) {
|
||||
consola.info(
|
||||
`No build or check-types script for ${dirName}, skipping`,
|
||||
);
|
||||
} else if (!scripts.build && scripts["check-types"]) {
|
||||
consola.info(
|
||||
`Only check-types script available for ${dirName}, type checking will be performed`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
consola.error(`${dirName} failed`, error);
|
||||
@@ -2639,4 +2850,279 @@ describe("create-better-t-stack smoke", () => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
describe("deploy combinations", () => {
|
||||
it("scaffolds workers runtime + web deploy wrangler", async () => {
|
||||
const projectName = "app-web-wrangler";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"workers",
|
||||
"--web-deploy",
|
||||
"wrangler",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "hono",
|
||||
runtime: "workers",
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds workers runtime + web deploy alchemy", async () => {
|
||||
const projectName = "app-web-alchemy";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"workers",
|
||||
"--web-deploy",
|
||||
"alchemy",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "hono",
|
||||
runtime: "workers",
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds workers runtime + server deploy alchemy (server-only)", async () => {
|
||||
const projectName = "app-server-only-alchemy";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--directory-conflict",
|
||||
"overwrite",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"workers",
|
||||
"--server-deploy",
|
||||
"alchemy",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "hono",
|
||||
runtime: "workers",
|
||||
serverDeploy: "alchemy",
|
||||
});
|
||||
consola.info("Verifying server-only Alchemy artifacts");
|
||||
expect(
|
||||
existsSync(join(projectDir, "apps", "server", "alchemy.run.ts")),
|
||||
).toBe(true);
|
||||
expect(existsSync(join(projectDir, "apps", "server", "env.d.ts"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(existsSync(join(projectDir, "alchemy.run.ts"))).toBe(false);
|
||||
});
|
||||
|
||||
it("scaffolds workers runtime + server deploy wrangler", async () => {
|
||||
const projectName = "app-server-wrangler";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"workers",
|
||||
"--server-deploy",
|
||||
"wrangler",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "hono",
|
||||
runtime: "workers",
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds workers runtime + server deploy alchemy", async () => {
|
||||
const projectName = "app-server-alchemy";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"workers",
|
||||
"--server-deploy",
|
||||
"alchemy",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "hono",
|
||||
runtime: "workers",
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds web deploy wrangler with backend none (no server deploy)", async () => {
|
||||
const projectName = "app-web-wrangler-only";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"none",
|
||||
"--web-deploy",
|
||||
"wrangler",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "none",
|
||||
webDeploy: "wrangler",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user