feat(cli): add alchemy and improve cli tooling and structure (#520)

This commit is contained in:
Aman Varshney
2025-08-20 23:43:58 +05:30
committed by GitHub
parent c5430ae4fd
commit 5788876c47
152 changed files with 5804 additions and 2264 deletions

View File

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

View File

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

View 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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";

View 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 });
}
}

View 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);
}

View File

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

View File

@@ -0,0 +1,112 @@
import path from "node:path";
import fs from "fs-extra";
import {
type ArrayLiteralExpression,
type CallExpression,
Node,
type ObjectLiteralExpression,
type PropertyAssignment,
SyntaxKind,
} from "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,
packageManager: PackageManager,
) {
const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) return;
await addPackageDependency({
devDependencies: ["nitro-cloudflare-dev", "wrangler"],
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: `${packageManager} run build && wrangler deploy`,
"cf-typegen": "wrangler types",
};
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
}
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
if (!(await fs.pathExists(nuxtConfigPath))) return;
const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
if (!sourceFile) return;
const defineCall = sourceFile
.getDescendantsOfKind(SyntaxKind.CallExpression)
.find((expr) => {
const expression = expr.getExpression();
return (
Node.isIdentifier(expression) &&
expression.getText() === "defineNuxtConfig"
);
}) as CallExpression | undefined;
if (!defineCall) return;
const configObj = defineCall.getArguments()[0] as
| ObjectLiteralExpression
| undefined;
if (!configObj) return;
const today = new Date().toISOString().slice(0, 10);
const compatProp = configObj.getProperty("compatibilityDate");
if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) {
(compatProp as PropertyAssignment).setInitializer(`'${today}'`);
} else {
configObj.addPropertyAssignment({
name: "compatibilityDate",
initializer: `'${today}'`,
});
}
const nitroInitializer = `{
preset: "cloudflare_module",
cloudflare: {
deployConfig: true,
nodeCompat: true
}
}`;
const nitroProp = configObj.getProperty("nitro");
if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) {
(nitroProp as PropertyAssignment).setInitializer(nitroInitializer);
} else {
configObj.addPropertyAssignment({
name: "nitro",
initializer: nitroInitializer,
});
}
const modulesProp = configObj.getProperty("modules");
if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
const arrayExpr = modulesProp.getFirstDescendantByKind(
SyntaxKind.ArrayLiteralExpression,
) as ArrayLiteralExpression | undefined;
if (arrayExpr) {
const alreadyHas = arrayExpr
.getElements()
.some(
(el) => el.getText().replace(/['"`]/g, "") === "nitro-cloudflare-dev",
);
if (!alreadyHas) arrayExpr.addElement("'nitro-cloudflare-dev'");
}
} else {
configObj.addPropertyAssignment({
name: "modules",
initializer: "['nitro-cloudflare-dev']",
});
}
await tsProject.save();
}

View File

@@ -0,0 +1,74 @@
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";
export async function setupSvelteWorkersDeploy(
projectDir: string,
packageManager: PackageManager,
) {
const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) return;
await addPackageDependency({
devDependencies: ["@sveltejs/adapter-cloudflare", "wrangler"],
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: `${packageManager} run build && wrangler deploy`,
"cf-typegen": "wrangler types ./src/worker-configuration.d.ts",
};
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
}
const possibleConfigFiles = [
path.join(webAppDir, "svelte.config.js"),
path.join(webAppDir, "svelte.config.ts"),
];
const existingConfigPath = (
await Promise.all(
possibleConfigFiles.map(async (p) => ((await fs.pathExists(p)) ? p : "")),
)
).find((p) => p);
if (existingConfigPath) {
const sourceFile =
tsProject.addSourceFileAtPathIfExists(existingConfigPath);
if (!sourceFile) return;
const adapterImport = sourceFile
.getImportDeclarations()
.find((imp: ImportDeclaration) =>
["@sveltejs/adapter-auto", "@sveltejs/adapter-node"].includes(
imp.getModuleSpecifierValue(),
),
);
if (adapterImport) {
adapterImport.setModuleSpecifier("@sveltejs/adapter-cloudflare");
} else {
const alreadyHasCloudflare = sourceFile
.getImportDeclarations()
.some(
(imp) =>
imp.getModuleSpecifierValue() === "@sveltejs/adapter-cloudflare",
);
if (!alreadyHasCloudflare) {
sourceFile.insertImportDeclaration(0, {
defaultImport: "adapter",
moduleSpecifier: "@sveltejs/adapter-cloudflare",
});
}
}
await tsProject.save();
}
}

View File

@@ -0,0 +1,75 @@
import path from "node:path";
import fs from "fs-extra";
import {
type CallExpression,
Node,
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";
export async function setupTanstackStartWorkersDeploy(
projectDir: string,
packageManager: PackageManager,
) {
const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) return;
await addPackageDependency({
devDependencies: ["wrangler"],
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: `${packageManager} run build && wrangler deploy`,
"cf-typegen": "wrangler types --env-interface Env",
};
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
}
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
if (!(await fs.pathExists(viteConfigPath))) return;
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
if (!sourceFile) return;
const defineCall = sourceFile
.getDescendantsOfKind(SyntaxKind.CallExpression)
.find((expr) => {
const expression = expr.getExpression();
return (
Node.isIdentifier(expression) && expression.getText() === "defineConfig"
);
}) as CallExpression | undefined;
if (!defineCall) return;
const configObj = defineCall.getArguments()[0] as
| ObjectLiteralExpression
| undefined;
if (!configObj) return;
const pluginsArray = ensureArrayProperty(configObj, "plugins");
const tanstackPluginIndex = pluginsArray
.getElements()
.findIndex((el) => el.getText().includes("tanstackStart("));
const tanstackPluginText = 'tanstackStart({ target: "cloudflare-module" })';
if (tanstackPluginIndex === -1) {
pluginsArray.addElement(tanstackPluginText);
} else {
pluginsArray
.getElements()
[tanstackPluginIndex].replaceWithText(tanstackPluginText);
}
await tsProject.save();
}

View File

@@ -0,0 +1,74 @@
import path from "node:path";
import fs from "fs-extra";
import {
type CallExpression,
Node,
type ObjectLiteralExpression,
SyntaxKind,
} from "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");
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
if (!(await fs.pathExists(viteConfigPath))) {
throw new Error("vite.config.ts not found in web app directory");
}
await addPackageDependency({
devDependencies: ["@cloudflare/vite-plugin", "wrangler"],
projectDir: webAppDir,
});
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
if (!sourceFile) {
throw new Error("vite.config.ts not found in web app directory");
}
const hasCloudflareImport = sourceFile
.getImportDeclarations()
.some((imp) => imp.getModuleSpecifierValue() === "@cloudflare/vite-plugin");
if (!hasCloudflareImport) {
sourceFile.insertImportDeclaration(0, {
namedImports: ["cloudflare"],
moduleSpecifier: "@cloudflare/vite-plugin",
});
}
const defineCall = sourceFile
.getDescendantsOfKind(SyntaxKind.CallExpression)
.find((expr) => {
const expression = expr.getExpression();
return (
Node.isIdentifier(expression) && expression.getText() === "defineConfig"
);
});
if (!defineCall) {
throw new Error("Could not find defineConfig call in vite config");
}
const callExpr = defineCall as CallExpression;
const configObject = callExpr.getArguments()[0] as
| ObjectLiteralExpression
| undefined;
if (!configObject) {
throw new Error("defineConfig argument is not an object literal");
}
const pluginsArray = ensureArrayProperty(configObject, "plugins");
const hasCloudflarePlugin = pluginsArray
.getElements()
.some((el) => el.getText().includes("cloudflare("));
if (!hasCloudflarePlugin) {
pluginsArray.addElement("cloudflare()");
}
await tsProject.save();
}