feat(cli): add react router support with alchemy (#542)

This commit is contained in:
Aman Varshney
2025-08-26 13:55:01 +05:30
committed by GitHub
parent 5fe7a73e86
commit 37c8e7cdf3
20 changed files with 163 additions and 276 deletions

View File

@@ -138,7 +138,7 @@ export const dependencyVersionMap = {
"@sveltejs/adapter-cloudflare": "^7.2.1", "@sveltejs/adapter-cloudflare": "^7.2.1",
"@cloudflare/workers-types": "^4.20250822.0", "@cloudflare/workers-types": "^4.20250822.0",
alchemy: "^0.62.1", alchemy: "^0.63.0",
// temporary workaround for alchemy + tanstack start // temporary workaround for alchemy + tanstack start
nitropack: "^2.12.4", nitropack: "^2.12.4",

View File

@@ -76,11 +76,12 @@ export async function createProject(options: ProjectConfig) {
await handleExtras(projectDir, options); await handleExtras(projectDir, options);
await setupEnvironmentVariables(options);
await updatePackageConfigurations(projectDir, options);
await setupWebDeploy(options); await setupWebDeploy(options);
await setupServerDeploy(options); await setupServerDeploy(options);
await setupEnvironmentVariables(options);
await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options); await createReadme(projectDir, options);
await writeBtsConfig(options); await writeBtsConfig(options);

View File

@@ -35,6 +35,8 @@ function generateReadmeContent(options: ProjectConfig): string {
frontend = ["tanstack-router"], frontend = ["tanstack-router"],
backend = "hono", backend = "hono",
api = "trpc", api = "trpc",
webDeploy,
serverDeploy,
} = options; } = options;
const isConvex = backend === "convex"; const isConvex = backend === "convex";
@@ -103,6 +105,7 @@ Follow the prompts to create a new Convex project and connect it to your applica
packageManagerRunCmd, packageManagerRunCmd,
orm, orm,
options.dbSetup, options.dbSetup,
options.serverDeploy,
) )
} }
@@ -120,6 +123,8 @@ ${
: "" : ""
} }
${generateDeploymentCommands(packageManagerRunCmd, webDeploy, serverDeploy)}
## Project Structure ## Project Structure
\`\`\` \`\`\`
@@ -475,6 +480,7 @@ function generateDatabaseSetup(
packageManagerRunCmd: string, packageManagerRunCmd: string,
orm: ORM, orm: ORM,
dbSetup: DatabaseSetup, dbSetup: DatabaseSetup,
serverDeploy?: string,
): string { ): string {
if (database === "none") { if (database === "none") {
return ""; return "";
@@ -494,7 +500,9 @@ function generateDatabaseSetup(
1. Start the local SQLite database: 1. Start the local SQLite database:
${ ${
dbSetup === "d1" dbSetup === "d1"
? "Local development for a Cloudflare D1 database will already be running as part of the `wrangler dev` command." ? serverDeploy === "alchemy"
? "D1 local development and migrations are handled automatically by Alchemy during dev and deploy."
: "Local development for a Cloudflare D1 database will already be running as part of the `wrangler dev` command."
: `\`\`\`bash : `\`\`\`bash
cd apps/server && ${packageManagerRunCmd} db:local cd apps/server && ${packageManagerRunCmd} db:local
\`\`\` \`\`\`
@@ -632,3 +640,51 @@ function generateScriptsList(
return scripts; return scripts;
} }
function generateDeploymentCommands(
packageManagerRunCmd: string,
webDeploy?: string,
serverDeploy?: string,
): string {
const lines: string[] = [];
if (webDeploy === "alchemy" || serverDeploy === "alchemy") {
lines.push("## Deployment (Alchemy)");
if (webDeploy === "alchemy" && serverDeploy !== "alchemy") {
lines.push(
`- Web dev: cd apps/web && ${packageManagerRunCmd} dev`,
`- Web deploy: cd apps/web && ${packageManagerRunCmd} deploy`,
`- Web destroy: cd apps/web && ${packageManagerRunCmd} destroy`,
);
}
if (serverDeploy === "alchemy" && webDeploy !== "alchemy") {
lines.push(
`- Server dev: cd apps/server && ${packageManagerRunCmd} dev`,
`- Server deploy: cd apps/server && ${packageManagerRunCmd} deploy`,
`- Server destroy: cd apps/server && ${packageManagerRunCmd} destroy`,
);
}
if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
lines.push(
`- Dev: ${packageManagerRunCmd} dev`,
`- Deploy: ${packageManagerRunCmd} deploy`,
`- Destroy: ${packageManagerRunCmd} destroy`,
);
}
}
if (webDeploy === "wrangler" || serverDeploy === "wrangler") {
lines.push("\n## Deployment (Cloudflare Wrangler)");
if (webDeploy === "wrangler") {
lines.push(`- Web deploy: cd apps/web && ${packageManagerRunCmd} deploy`);
}
if (serverDeploy === "wrangler") {
lines.push(
`- Server dev: cd apps/server && ${packageManagerRunCmd} dev`,
`- Server deploy: cd apps/server && ${packageManagerRunCmd} deploy`,
);
}
}
return lines.length ? `\n${lines.join("\n")}\n` : "";
}

View File

@@ -412,15 +412,15 @@ function getAlchemyDeployInstructions(
if (webDeploy === "alchemy" && serverDeploy !== "alchemy") { if (webDeploy === "alchemy" && serverDeploy !== "alchemy") {
instructions.push( instructions.push(
`${pc.bold("Deploy web with Alchemy:")}\n${pc.cyan("•")} Dev: ${`cd apps/web && ${runCmd} alchemy:dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/web && ${runCmd} destroy`}`, `${pc.bold("Deploy web with Alchemy:")}\n${pc.cyan("•")} Dev: ${`cd apps/web && ${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/web && ${runCmd} destroy`}`,
); );
} else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") { } else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") {
instructions.push( instructions.push(
`${pc.bold("Deploy server with Alchemy:")}\n${pc.cyan("•")} Dev: ${`cd apps/server && ${runCmd} alchemy:dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/server && ${runCmd} destroy`}`, `${pc.bold("Deploy server with Alchemy:")}\n${pc.cyan("•")} Dev: ${`cd apps/server && ${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/server && ${runCmd} destroy`}`,
); );
} else if (webDeploy === "alchemy" && serverDeploy === "alchemy") { } else if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
instructions.push( instructions.push(
`${pc.bold("Deploy with Alchemy:")}\n${pc.cyan("•")} Dev: ${`${runCmd} alchemy:dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`, `${pc.bold("Deploy with Alchemy:")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`,
); );
} }

View File

@@ -835,12 +835,6 @@ export async function setupDeploymentTemplates(
serverAppDir, serverAppDir,
context, context,
); );
await processAndCopyFiles(
"wrangler.jsonc.hbs",
alchemyTemplateSrc,
serverAppDir,
context,
);
} }
} }
} else { } else {
@@ -885,12 +879,6 @@ export async function setupDeploymentTemplates(
serverAppDir, serverAppDir,
context, context,
); );
await processAndCopyFiles(
"wrangler.jsonc.hbs",
alchemyTemplateSrc,
serverAppDir,
context,
);
} }
} }
} }

View File

@@ -29,7 +29,7 @@ export async function setupCombinedAlchemyDeploy(
...pkg.scripts, ...pkg.scripts,
deploy: "alchemy deploy", deploy: "alchemy deploy",
destroy: "alchemy destroy", destroy: "alchemy destroy",
"alchemy:dev": "alchemy dev", dev: "alchemy dev",
}; };
await fs.writeJson(rootPkgPath, pkg, { spaces: 2 }); await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
} }
@@ -49,18 +49,32 @@ export async function setupCombinedAlchemyDeploy(
const isSolid = frontend.includes("solid"); const isSolid = frontend.includes("solid");
if (isNext) { if (isNext) {
await setupNextAlchemyDeploy(projectDir, packageManager); await setupNextAlchemyDeploy(projectDir, packageManager, {
skipAppScripts: true,
});
} else if (isNuxt) { } else if (isNuxt) {
await setupNuxtAlchemyDeploy(projectDir, packageManager); await setupNuxtAlchemyDeploy(projectDir, packageManager, {
skipAppScripts: true,
});
} else if (isSvelte) { } else if (isSvelte) {
await setupSvelteAlchemyDeploy(projectDir, packageManager); await setupSvelteAlchemyDeploy(projectDir, packageManager, {
skipAppScripts: true,
});
} else if (isTanstackStart) { } else if (isTanstackStart) {
await setupTanStackStartAlchemyDeploy(projectDir, packageManager); await setupTanStackStartAlchemyDeploy(projectDir, packageManager, {
skipAppScripts: true,
});
} else if (isTanstackRouter) { } else if (isTanstackRouter) {
await setupTanStackRouterAlchemyDeploy(projectDir, packageManager); await setupTanStackRouterAlchemyDeploy(projectDir, packageManager, {
skipAppScripts: true,
});
} else if (isReactRouter) { } else if (isReactRouter) {
await setupReactRouterAlchemyDeploy(projectDir, packageManager); await setupReactRouterAlchemyDeploy(projectDir, packageManager, {
skipAppScripts: true,
});
} else if (isSolid) { } else if (isSolid) {
await setupSolidAlchemyDeploy(projectDir, packageManager); await setupSolidAlchemyDeploy(projectDir, packageManager, {
skipAppScripts: true,
});
} }
} }

View File

@@ -6,6 +6,7 @@ import { addPackageDependency } from "../../../utils/add-package-deps";
export async function setupNextAlchemyDeploy( export async function setupNextAlchemyDeploy(
projectDir: string, projectDir: string,
_packageManager: PackageManager, _packageManager: PackageManager,
options?: { skipAppScripts?: boolean },
) { ) {
const webAppDir = path.join(projectDir, "apps/web"); const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) return; if (!(await fs.pathExists(webAppDir))) return;
@@ -19,12 +20,14 @@ export async function setupNextAlchemyDeploy(
if (await fs.pathExists(pkgPath)) { if (await fs.pathExists(pkgPath)) {
const pkg = await fs.readJson(pkgPath); const pkg = await fs.readJson(pkgPath);
pkg.scripts = { if (!options?.skipAppScripts) {
...pkg.scripts, pkg.scripts = {
deploy: "alchemy deploy", ...pkg.scripts,
destroy: "alchemy destroy", deploy: "alchemy deploy",
"alchemy:dev": "alchemy dev", destroy: "alchemy destroy",
}; dev: "alchemy dev",
};
}
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); await fs.writeJson(pkgPath, pkg, { spaces: 2 });
} }
} }

View File

@@ -7,6 +7,7 @@ import { addPackageDependency } from "../../../utils/add-package-deps";
export async function setupNuxtAlchemyDeploy( export async function setupNuxtAlchemyDeploy(
projectDir: string, projectDir: string,
_packageManager: PackageManager, _packageManager: PackageManager,
options?: { skipAppScripts?: boolean },
) { ) {
const webAppDir = path.join(projectDir, "apps/web"); const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) return; if (!(await fs.pathExists(webAppDir))) return;
@@ -20,12 +21,14 @@ export async function setupNuxtAlchemyDeploy(
if (await fs.pathExists(pkgPath)) { if (await fs.pathExists(pkgPath)) {
const pkg = await fs.readJson(pkgPath); const pkg = await fs.readJson(pkgPath);
pkg.scripts = { if (!options?.skipAppScripts) {
...pkg.scripts, pkg.scripts = {
deploy: "alchemy deploy", ...pkg.scripts,
destroy: "alchemy destroy", deploy: "alchemy deploy",
"alchemy:dev": "alchemy dev", destroy: "alchemy destroy",
}; dev: "alchemy dev",
};
}
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); await fs.writeJson(pkgPath, pkg, { spaces: 2 });
} }

View File

@@ -1,18 +1,18 @@
import path from "node:path"; import path from "node:path";
import fs from "fs-extra"; import fs from "fs-extra";
import { IndentationText, Node, Project, QuoteKind } from "ts-morph";
import type { PackageManager } from "../../../types"; import type { PackageManager } from "../../../types";
import { addPackageDependency } from "../../../utils/add-package-deps"; import { addPackageDependency } from "../../../utils/add-package-deps";
export async function setupReactRouterAlchemyDeploy( export async function setupReactRouterAlchemyDeploy(
projectDir: string, projectDir: string,
_packageManager: PackageManager, _packageManager: PackageManager,
options?: { skipAppScripts?: boolean },
) { ) {
const webAppDir = path.join(projectDir, "apps/web"); const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) return; if (!(await fs.pathExists(webAppDir))) return;
await addPackageDependency({ await addPackageDependency({
devDependencies: ["alchemy", "@cloudflare/vite-plugin", "dotenv"], devDependencies: ["alchemy", "dotenv"],
projectDir: webAppDir, projectDir: webAppDir,
}); });
@@ -20,149 +20,14 @@ export async function setupReactRouterAlchemyDeploy(
if (await fs.pathExists(pkgPath)) { if (await fs.pathExists(pkgPath)) {
const pkg = await fs.readJson(pkgPath); const pkg = await fs.readJson(pkgPath);
pkg.scripts = { if (!options?.skipAppScripts) {
...pkg.scripts, pkg.scripts = {
deploy: "alchemy deploy", ...pkg.scripts,
destroy: "alchemy destroy", deploy: "alchemy deploy",
"alchemy:dev": "alchemy dev", destroy: "alchemy destroy",
}; dev: "alchemy dev",
};
}
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); 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

@@ -6,6 +6,7 @@ import { addPackageDependency } from "../../../utils/add-package-deps";
export async function setupSolidAlchemyDeploy( export async function setupSolidAlchemyDeploy(
projectDir: string, projectDir: string,
_packageManager: PackageManager, _packageManager: PackageManager,
options?: { skipAppScripts?: boolean },
) { ) {
const webAppDir = path.join(projectDir, "apps/web"); const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) return; if (!(await fs.pathExists(webAppDir))) return;
@@ -19,12 +20,14 @@ export async function setupSolidAlchemyDeploy(
if (await fs.pathExists(pkgPath)) { if (await fs.pathExists(pkgPath)) {
const pkg = await fs.readJson(pkgPath); const pkg = await fs.readJson(pkgPath);
pkg.scripts = { if (!options?.skipAppScripts) {
...pkg.scripts, pkg.scripts = {
deploy: "alchemy deploy", ...pkg.scripts,
destroy: "alchemy destroy", deploy: "alchemy deploy",
"alchemy:dev": "alchemy dev", destroy: "alchemy destroy",
}; dev: "alchemy dev",
};
}
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); await fs.writeJson(pkgPath, pkg, { spaces: 2 });
} }
} }

View File

@@ -7,6 +7,7 @@ import { addPackageDependency } from "../../../utils/add-package-deps";
export async function setupSvelteAlchemyDeploy( export async function setupSvelteAlchemyDeploy(
projectDir: string, projectDir: string,
_packageManager: PackageManager, _packageManager: PackageManager,
options?: { skipAppScripts?: boolean },
) { ) {
const webAppDir = path.join(projectDir, "apps/web"); const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) return; if (!(await fs.pathExists(webAppDir))) return;
@@ -20,12 +21,15 @@ export async function setupSvelteAlchemyDeploy(
if (await fs.pathExists(pkgPath)) { if (await fs.pathExists(pkgPath)) {
const pkg = await fs.readJson(pkgPath); const pkg = await fs.readJson(pkgPath);
pkg.scripts = { if (!options?.skipAppScripts) {
...pkg.scripts, pkg.scripts = {
deploy: "alchemy deploy", ...pkg.scripts,
destroy: "alchemy destroy", deploy: "alchemy deploy",
"alchemy:dev": "alchemy dev", destroy: "alchemy destroy",
}; dev: "alchemy dev",
};
}
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); await fs.writeJson(pkgPath, pkg, { spaces: 2 });
} }

View File

@@ -6,6 +6,7 @@ import { addPackageDependency } from "../../../utils/add-package-deps";
export async function setupTanStackRouterAlchemyDeploy( export async function setupTanStackRouterAlchemyDeploy(
projectDir: string, projectDir: string,
_packageManager: PackageManager, _packageManager: PackageManager,
options?: { skipAppScripts?: boolean },
) { ) {
const webAppDir = path.join(projectDir, "apps/web"); const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) return; if (!(await fs.pathExists(webAppDir))) return;
@@ -19,12 +20,15 @@ export async function setupTanStackRouterAlchemyDeploy(
if (await fs.pathExists(pkgPath)) { if (await fs.pathExists(pkgPath)) {
const pkg = await fs.readJson(pkgPath); const pkg = await fs.readJson(pkgPath);
pkg.scripts = { if (!options?.skipAppScripts) {
...pkg.scripts, pkg.scripts = {
deploy: "alchemy deploy", ...pkg.scripts,
destroy: "alchemy destroy", deploy: "alchemy deploy",
"alchemy:dev": "alchemy dev", destroy: "alchemy destroy",
}; dev: "alchemy dev",
};
}
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); await fs.writeJson(pkgPath, pkg, { spaces: 2 });
} }
} }

View File

@@ -7,6 +7,7 @@ import { addPackageDependency } from "../../../utils/add-package-deps";
export async function setupTanStackStartAlchemyDeploy( export async function setupTanStackStartAlchemyDeploy(
projectDir: string, projectDir: string,
_packageManager: PackageManager, _packageManager: PackageManager,
options?: { skipAppScripts?: boolean },
) { ) {
const webAppDir = path.join(projectDir, "apps/web"); const webAppDir = path.join(projectDir, "apps/web");
if (!(await fs.pathExists(webAppDir))) return; if (!(await fs.pathExists(webAppDir))) return;
@@ -20,12 +21,15 @@ export async function setupTanStackStartAlchemyDeploy(
if (await fs.pathExists(pkgPath)) { if (await fs.pathExists(pkgPath)) {
const pkg = await fs.readJson(pkgPath); const pkg = await fs.readJson(pkgPath);
pkg.scripts = { if (!options?.skipAppScripts) {
...pkg.scripts, pkg.scripts = {
deploy: "alchemy deploy", ...pkg.scripts,
destroy: "alchemy destroy", deploy: "alchemy deploy",
"alchemy:dev": "alchemy dev", destroy: "alchemy destroy",
}; dev: "alchemy dev",
};
}
await fs.writeJson(pkgPath, pkg, { spaces: 2 }); await fs.writeJson(pkgPath, pkg, { spaces: 2 });
} }

View File

@@ -99,11 +99,9 @@ export async function setupAlchemyServerDeploy(
packageJson.scripts = { packageJson.scripts = {
...packageJson.scripts, ...packageJson.scripts,
dev: "wrangler dev --port=3000", dev: "alchemy dev",
build: "wrangler deploy --dry-run",
deploy: "alchemy deploy", deploy: "alchemy deploy",
destroy: "alchemy destroy", destroy: "alchemy destroy",
"alchemy:dev": "alchemy dev",
}; };
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });

View File

@@ -47,9 +47,7 @@ export async function getDeploymentChoice(
return "none"; return "none";
} }
const hasIncompatibleFrontend = frontend.some( const hasIncompatibleFrontend = frontend.some((f) => f === "next");
(f) => f === "next" || f === "react-router",
);
const availableDeployments = hasIncompatibleFrontend const availableDeployments = hasIncompatibleFrontend
? ["wrangler", "none"] ? ["wrangler", "none"]
: ["wrangler", "alchemy", "none"]; : ["wrangler", "alchemy", "none"];
@@ -84,9 +82,7 @@ export async function getDeploymentToAdd(
return "none"; return "none";
} }
const hasIncompatibleFrontend = frontend.some( const hasIncompatibleFrontend = frontend.some((f) => f === "next");
(f) => f === "next" || f === "react-router",
);
const options: DeploymentOption[] = []; const options: DeploymentOption[] = [];

View File

@@ -324,9 +324,7 @@ export function validateAlchemyCompatibility(
const isAlchemyServerDeploy = serverDeploy === "alchemy"; const isAlchemyServerDeploy = serverDeploy === "alchemy";
if (isAlchemyWebDeploy || isAlchemyServerDeploy) { if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
const incompatibleFrontends = frontends.filter( const incompatibleFrontends = frontends.filter((f) => f === "next");
(f) => f === "next" || f === "react-router",
);
if (incompatibleFrontends.length > 0) { if (incompatibleFrontends.length > 0) {
const deployType = const deployType =

View File

@@ -17,7 +17,7 @@ import { Vite } from "alchemy/cloudflare";
{{/if}} {{/if}}
{{/if}} {{/if}}
{{#if (eq serverDeploy "alchemy")}} {{#if (eq serverDeploy "alchemy")}}
import { Worker, WranglerJson } from "alchemy/cloudflare"; import { Worker } from "alchemy/cloudflare";
{{#if (eq dbSetup "d1")}} {{#if (eq dbSetup "d1")}}
import { D1Database } from "alchemy/cloudflare"; import { D1Database } from "alchemy/cloudflare";
{{/if}} {{/if}}
@@ -44,7 +44,6 @@ await Exec("db-generate", {
}); });
const db = await D1Database("database", { const db = await D1Database("database", {
name: `${app.name}-${app.stage}-db`,
migrationsDir: "apps/server/src/db/migrations", migrationsDir: "apps/server/src/db/migrations",
}); });
{{/if}} {{/if}}
@@ -53,7 +52,6 @@ const db = await D1Database("database", {
{{#if (includes frontend "next")}} {{#if (includes frontend "next")}}
export const web = await Next("web", { export const web = await Next("web", {
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}} {{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
name: `${app.name}-${app.stage}-web`,
bindings: { bindings: {
{{#if (eq backend "convex")}} {{#if (eq backend "convex")}}
NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL || "", NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL || "",
@@ -68,7 +66,6 @@ export const web = await Next("web", {
{{else if (includes frontend "nuxt")}} {{else if (includes frontend "nuxt")}}
export const web = await Nuxt("web", { export const web = await Nuxt("web", {
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}} {{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
name: `${app.name}-${app.stage}-web`,
bindings: { bindings: {
{{#if (eq backend "convex")}} {{#if (eq backend "convex")}}
NUXT_PUBLIC_CONVEX_URL: process.env.NUXT_PUBLIC_CONVEX_URL || "", NUXT_PUBLIC_CONVEX_URL: process.env.NUXT_PUBLIC_CONVEX_URL || "",
@@ -83,7 +80,6 @@ export const web = await Nuxt("web", {
{{else if (includes frontend "svelte")}} {{else if (includes frontend "svelte")}}
export const web = await SvelteKit("web", { export const web = await SvelteKit("web", {
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}} {{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
name: `${app.name}-${app.stage}-web`,
bindings: { bindings: {
{{#if (eq backend "convex")}} {{#if (eq backend "convex")}}
PUBLIC_CONVEX_URL: process.env.PUBLIC_CONVEX_URL || "", PUBLIC_CONVEX_URL: process.env.PUBLIC_CONVEX_URL || "",
@@ -98,7 +94,6 @@ export const web = await SvelteKit("web", {
{{else if (includes frontend "tanstack-start")}} {{else if (includes frontend "tanstack-start")}}
export const web = await TanStackStart("web", { export const web = await TanStackStart("web", {
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}} {{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
name: `${app.name}-${app.stage}-web`,
bindings: { bindings: {
{{#if (eq backend "convex")}} {{#if (eq backend "convex")}}
VITE_CONVEX_URL: process.env.VITE_CONVEX_URL || "", VITE_CONVEX_URL: process.env.VITE_CONVEX_URL || "",
@@ -113,7 +108,6 @@ export const web = await TanStackStart("web", {
{{else if (includes frontend "tanstack-router")}} {{else if (includes frontend "tanstack-router")}}
export const web = await Vite("web", { export const web = await Vite("web", {
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}} {{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
name: `${app.name}-${app.stage}-web`,
assets: "dist", assets: "dist",
bindings: { bindings: {
{{#if (eq backend "convex")}} {{#if (eq backend "convex")}}
@@ -129,7 +123,6 @@ export const web = await Vite("web", {
{{else if (includes frontend "react-router")}} {{else if (includes frontend "react-router")}}
export const web = await ReactRouter("web", { export const web = await ReactRouter("web", {
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}} {{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
name: `${app.name}-${app.stage}-web`,
bindings: { bindings: {
{{#if (eq backend "convex")}} {{#if (eq backend "convex")}}
VITE_CONVEX_URL: process.env.VITE_CONVEX_URL || "", VITE_CONVEX_URL: process.env.VITE_CONVEX_URL || "",
@@ -144,7 +137,6 @@ export const web = await ReactRouter("web", {
{{else if (includes frontend "solid")}} {{else if (includes frontend "solid")}}
export const web = await Vite("web", { export const web = await Vite("web", {
{{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}} {{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}}
name: `${app.name}-${app.stage}-web`,
assets: "dist", assets: "dist",
bindings: { bindings: {
{{#if (eq backend "convex")}} {{#if (eq backend "convex")}}
@@ -163,7 +155,6 @@ export const web = await Vite("web", {
{{#if (eq serverDeploy "alchemy")}} {{#if (eq serverDeploy "alchemy")}}
export const server = await Worker("server", { export const server = await Worker("server", {
{{#if (eq webDeploy "alchemy")}}cwd: "apps/server",{{/if}} {{#if (eq webDeploy "alchemy")}}cwd: "apps/server",{{/if}}
name: `${app.name}-${app.stage}`,
entrypoint: "src/index.ts", entrypoint: "src/index.ts",
compatibility: "node", compatibility: "node",
bindings: { bindings: {
@@ -188,14 +179,8 @@ export const server = await Worker("server", {
port: 3000, port: 3000,
}, },
}); });
await WranglerJson("wrangler", {
worker: server,
});
{{/if}} {{/if}}
{{#if (and (eq webDeploy "alchemy") (eq serverDeploy "alchemy"))}} {{#if (and (eq webDeploy "alchemy") (eq serverDeploy "alchemy"))}}
console.log(`Web -> ${web.url}`); console.log(`Web -> ${web.url}`);
console.log(`Server -> ${server.url}`); console.log(`Server -> ${server.url}`);

View File

@@ -1,11 +0,0 @@
// 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"
]
}

View File

@@ -2804,27 +2804,8 @@ describe("create-better-t-stack smoke", () => {
consola.success(`${dirName} built successfully`); consola.success(`${dirName} built successfully`);
} }
if (scripts["check-types"]) { if (!scripts.build) {
consola.start(`Type checking ${dirName}...`); consola.info(`No build script for ${dirName}, skipping`);
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) { } catch (error) {
consola.error(`${dirName} failed`, error); consola.error(`${dirName} failed`, error);

View File

@@ -1005,7 +1005,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
if (isAlchemyWebDeploy || isAlchemyServerDeploy) { if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
const incompatibleFrontends = nextStack.webFrontend.filter( const incompatibleFrontends = nextStack.webFrontend.filter(
(f) => f === "next" || f === "react-router", (f) => f === "next",
); );
if (incompatibleFrontends.length > 0) { if (incompatibleFrontends.length > 0) {
@@ -1029,9 +1029,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.webDeploy.hasIssue = true; notes.webDeploy.hasIssue = true;
notes.serverDeploy.hasIssue = true; notes.serverDeploy.hasIssue = true;
nextStack.webFrontend = nextStack.webFrontend.filter( nextStack.webFrontend = nextStack.webFrontend.filter((f) => f !== "next");
(f) => f !== "next" && f !== "react-router",
);
if (nextStack.webFrontend.length === 0) { if (nextStack.webFrontend.length === 0) {
nextStack.webFrontend = ["tanstack-router"]; nextStack.webFrontend = ["tanstack-router"];
@@ -1632,10 +1630,7 @@ const StackBuilder = () => {
const { adjustedStack } = analyzeStackCompatibility(simulatedStack); const { adjustedStack } = analyzeStackCompatibility(simulatedStack);
const finalStack = adjustedStack ?? simulatedStack; const finalStack = adjustedStack ?? simulatedStack;
if ( if (category === "webFrontend" && optionId === "next") {
category === "webFrontend" &&
(optionId === "next" || optionId === "react-router")
) {
const isAlchemyWebDeploy = finalStack.webDeploy === "alchemy"; const isAlchemyWebDeploy = finalStack.webDeploy === "alchemy";
const isAlchemyServerDeploy = finalStack.serverDeploy === "alchemy"; const isAlchemyServerDeploy = finalStack.serverDeploy === "alchemy";