From 5a5f2d930dfe50b8786b69ec5bb5b847fca83b9b Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 1 Oct 2025 19:25:35 +0100 Subject: [PATCH 1/2] C3: add TanStack Start framework template --- .changeset/busy-horses-kick.md | 5 + .../e2e/helpers/framework-helpers.ts | 4 +- .../e2e/tests/cli/cli.test.ts | 2 +- .../e2e/tests/frameworks/test-config.ts | 14 ++ .../src/frameworks/package.json | 1 + packages/create-cloudflare/src/templates.ts | 2 + .../templates/tanstack/c3.ts | 125 ++++++++++++++++++ .../tanstack/templates/wrangler.jsonc | 7 + 8 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 .changeset/busy-horses-kick.md create mode 100644 packages/create-cloudflare/templates/tanstack/c3.ts create mode 100644 packages/create-cloudflare/templates/tanstack/templates/wrangler.jsonc diff --git a/.changeset/busy-horses-kick.md b/.changeset/busy-horses-kick.md new file mode 100644 index 000000000000..1bfed204bc24 --- /dev/null +++ b/.changeset/busy-horses-kick.md @@ -0,0 +1,5 @@ +--- +"create-cloudflare": minor +--- + +add TanStack Start framework template diff --git a/packages/create-cloudflare/e2e/helpers/framework-helpers.ts b/packages/create-cloudflare/e2e/helpers/framework-helpers.ts index 740f8bb7a4c7..6b4beb5aafa6 100644 --- a/packages/create-cloudflare/e2e/helpers/framework-helpers.ts +++ b/packages/create-cloudflare/e2e/helpers/framework-helpers.ts @@ -184,11 +184,11 @@ export async function verifyPreviewScript( // so wait up to 5 mins for the dev-server to be ready. await retry( { times: 300, sleepMs: 5000 }, - async () => await fetch(`http://127.0.0.1:${port}${verifyPreview.route}`), + async () => await fetch(`http://localhost:${port}${verifyPreview.route}`), ); // Make a request to the specified test route - const res = await fetch(`http://127.0.0.1:${port}${verifyPreview.route}`); + const res = await fetch(`http://localhost:${port}${verifyPreview.route}`); expect(await res.text()).toContain(verifyPreview.expectedText); } finally { // Kill the process gracefully so ports can be cleaned up diff --git a/packages/create-cloudflare/e2e/tests/cli/cli.test.ts b/packages/create-cloudflare/e2e/tests/cli/cli.test.ts index 5593b4b1c24f..ee15c41b5e57 100644 --- a/packages/create-cloudflare/e2e/tests/cli/cli.test.ts +++ b/packages/create-cloudflare/e2e/tests/cli/cli.test.ts @@ -587,7 +587,7 @@ describe("Create Cloudflare CLI", () => { npm create cloudflare -- --framework next -- --ts pnpm create cloudflare --framework next -- --ts Allowed Values: - analog, angular, astro, docusaurus, gatsby, hono, next, nuxt, qwik, react, react-router, solid, svelte, vue, waku + analog, angular, astro, docusaurus, gatsby, hono, next, nuxt, qwik, react, react-router, solid, svelte, vue, waku, tanstack --platform= Whether the application should be deployed to Pages or Workers. This is only applicable for Frameworks templates that support both Pages and Workers. Allowed Values: diff --git a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts index 046c8f9be67f..4fa575ed690a 100644 --- a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts +++ b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts @@ -582,6 +582,20 @@ function getFrameworkTestConfig(pm: string): NamedFrameworkTestConfig[] { }, nodeCompat: false, }, + { + name: "tanstack", + testCommitMessage: true, + timeout: LONG_TIMEOUT, + verifyDeploy: { + route: "/", + expectedText: "Learn TanStack", + }, + verifyPreview: { + route: "/", + expectedText: "Learn TanStack", + }, + nodeCompat: true, + }, ]; } diff --git a/packages/create-cloudflare/src/frameworks/package.json b/packages/create-cloudflare/src/frameworks/package.json index 452f02234e98..26c5d7953c94 100644 --- a/packages/create-cloudflare/src/frameworks/package.json +++ b/packages/create-cloudflare/src/frameworks/package.json @@ -17,6 +17,7 @@ "create-solid": "0.6.11", "create-vue": "3.18.1", "create-waku": "0.12.5-0.26.1-0", + "@tanstack/create-start": "0.24.1", "gatsby": "5.15.0", "sv": "0.9.7", "nuxi": "3.28.0" diff --git a/packages/create-cloudflare/src/templates.ts b/packages/create-cloudflare/src/templates.ts index e92598e0cdb7..ff7419cd6723 100644 --- a/packages/create-cloudflare/src/templates.ts +++ b/packages/create-cloudflare/src/templates.ts @@ -42,6 +42,7 @@ import reactTemplate from "templates/react/c3"; import scheduledTemplate from "templates/scheduled/c3"; import solidTemplate from "templates/solid/c3"; import svelteTemplate from "templates/svelte/c3"; +import tanstackTemplate from "templates/tanstack/c3"; import vueTemplate from "templates/vue/c3"; import wakuTemplate from "templates/waku/c3"; import { isInsideGitRepo } from "./git"; @@ -206,6 +207,7 @@ export function getFrameworkMap({ experimental = false }): TemplateMap { svelte: svelteTemplate, vue: vueTemplate, waku: wakuTemplate, + tanstack: tanstackTemplate, }; } } diff --git a/packages/create-cloudflare/templates/tanstack/c3.ts b/packages/create-cloudflare/templates/tanstack/c3.ts new file mode 100644 index 000000000000..2bc60fbb4008 --- /dev/null +++ b/packages/create-cloudflare/templates/tanstack/c3.ts @@ -0,0 +1,125 @@ +import assert from "node:assert"; +import { logRaw } from "@cloudflare/cli"; +import { brandColor, dim } from "@cloudflare/cli/colors"; +import { runFrameworkGenerator } from "frameworks/index"; +import { transformFile } from "helpers/codemod"; +import { detectPackageManager } from "helpers/packageManagers"; +import { installPackages } from "helpers/packages"; +import * as recast from "recast"; +import type { TemplateConfig } from "../../src/templates"; +import type { types } from "recast"; +import type { C3Context } from "types"; + +const b = recast.types.builders; +const t = recast.types.namedTypes; +const { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + await runFrameworkGenerator(ctx, [ + ctx.project.name, + "--framework", + "react", + // to prevent asking about git twice, just let c3 do it + "--no-git", + ]); + + logRaw(""); // newline +}; + +const configure = async () => { + await installPackages(["@cloudflare/vite-plugin"], { + dev: true, + startText: "Installing the Cloudflare Vite plugin", + doneText: `${brandColor(`installed`)} ${dim("@cloudflare/vite-plugin")}`, + }); + + updateViteConfig(); +}; + +const updateViteConfig = () => { + const filePath = "vite.config.ts"; + + transformFile(filePath, { + visitProgram(n) { + // Add an import of the @cloudflare/vite-plugin + // ``` + // import {cloudflare} from "@cloudflare/vite-plugin"; + // ``` + const lastImportIndex = n.node.body.findLastIndex( + (statement) => statement.type === "ImportDeclaration", + ); + const lastImport = n.get("body", lastImportIndex); + const importAst = b.importDeclaration( + [b.importSpecifier(b.identifier("cloudflare"))], + b.stringLiteral("@cloudflare/vite-plugin"), + ); + lastImport.insertAfter(importAst); + + return this.traverse(n); + }, + visitCallExpression: function (n) { + // Add the imported plugin to the config + // ``` + // defineConfig({ + // plugins: [react(), cloudflare()], + // }); + const callee = n.node.callee as types.namedTypes.Identifier; + if (callee.name !== "defineConfig") { + return this.traverse(n); + } + + const config = n.node.arguments[0]; + assert(t.ObjectExpression.check(config)); + const pluginsProp = config.properties.find((prop) => isPluginsProp(prop)); + assert(pluginsProp && t.ArrayExpression.check(pluginsProp.value)); + pluginsProp.value.elements.push( + b.callExpression(b.identifier("cloudflare"), [ + b.objectExpression([ + b.objectProperty( + b.identifier("viteEnvironment"), + b.objectExpression([ + b.objectProperty(b.identifier("name"), b.literal("ssr")), + ]), + ), + ]), + ]), + ); + + return false; + }, + }); +}; + +function isPluginsProp( + prop: unknown, +): prop is types.namedTypes.ObjectProperty | types.namedTypes.Property { + return ( + (t.Property.check(prop) || t.ObjectProperty.check(prop)) && + t.Identifier.check(prop.key) && + prop.key.name === "plugins" + ); +} + +const config: TemplateConfig = { + configVersion: 1, + id: "tanstack", + platform: "workers", + frameworkCli: "@tanstack/create-start", + displayName: "TanStack Start", + generate, + configure, + copyFiles: { + path: "./templates", + }, + transformPackageJson: async () => ({ + scripts: { + deploy: `${npm} run build && wrangler deploy`, + preview: `${npm} run build && vite preview`, + "cf-typegen": `wrangler types`, + }, + }), + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates/tanstack/templates/wrangler.jsonc b/packages/create-cloudflare/templates/tanstack/templates/wrangler.jsonc new file mode 100644 index 000000000000..e42e37d095d6 --- /dev/null +++ b/packages/create-cloudflare/templates/tanstack/templates/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "", + "compatibility_date": "", + "compatibility_flags": ["nodejs_compat"], + "main": "@tanstack/react-start/server-entry" +} From 18f705b1b1b290f52e81c43117f4b6fb1d36307c Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 16 Oct 2025 12:26:28 +0100 Subject: [PATCH 2/2] adapt c3 tanstack template to latest tanstack CLI --- .../e2e/tests/frameworks/test-config.ts | 4 +- .../src/frameworks/package.json | 2 +- .../templates/tanstack/c3.ts | 88 +------------------ .../tanstack/templates/wrangler.jsonc | 7 -- 4 files changed, 5 insertions(+), 96 deletions(-) delete mode 100644 packages/create-cloudflare/templates/tanstack/templates/wrangler.jsonc diff --git a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts index 4fa575ed690a..27cf67d2a4d3 100644 --- a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts +++ b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts @@ -588,11 +588,11 @@ function getFrameworkTestConfig(pm: string): NamedFrameworkTestConfig[] { timeout: LONG_TIMEOUT, verifyDeploy: { route: "/", - expectedText: "Learn TanStack", + expectedText: "TanStack Start Starter", }, verifyPreview: { route: "/", - expectedText: "Learn TanStack", + expectedText: "TanStack Start Starter", }, nodeCompat: true, }, diff --git a/packages/create-cloudflare/src/frameworks/package.json b/packages/create-cloudflare/src/frameworks/package.json index 26c5d7953c94..031a007cc8e7 100644 --- a/packages/create-cloudflare/src/frameworks/package.json +++ b/packages/create-cloudflare/src/frameworks/package.json @@ -17,7 +17,7 @@ "create-solid": "0.6.11", "create-vue": "3.18.1", "create-waku": "0.12.5-0.26.1-0", - "@tanstack/create-start": "0.24.1", + "@tanstack/create-start": "0.33.0", "gatsby": "5.15.0", "sv": "0.9.7", "nuxi": "3.28.0" diff --git a/packages/create-cloudflare/templates/tanstack/c3.ts b/packages/create-cloudflare/templates/tanstack/c3.ts index 2bc60fbb4008..a5ac5859d568 100644 --- a/packages/create-cloudflare/templates/tanstack/c3.ts +++ b/packages/create-cloudflare/templates/tanstack/c3.ts @@ -1,22 +1,16 @@ -import assert from "node:assert"; import { logRaw } from "@cloudflare/cli"; -import { brandColor, dim } from "@cloudflare/cli/colors"; import { runFrameworkGenerator } from "frameworks/index"; -import { transformFile } from "helpers/codemod"; import { detectPackageManager } from "helpers/packageManagers"; -import { installPackages } from "helpers/packages"; -import * as recast from "recast"; import type { TemplateConfig } from "../../src/templates"; -import type { types } from "recast"; import type { C3Context } from "types"; -const b = recast.types.builders; -const t = recast.types.namedTypes; const { npm } = detectPackageManager(); const generate = async (ctx: C3Context) => { await runFrameworkGenerator(ctx, [ ctx.project.name, + "--host", + "cloudflare", "--framework", "react", // to prevent asking about git twice, just let c3 do it @@ -26,80 +20,6 @@ const generate = async (ctx: C3Context) => { logRaw(""); // newline }; -const configure = async () => { - await installPackages(["@cloudflare/vite-plugin"], { - dev: true, - startText: "Installing the Cloudflare Vite plugin", - doneText: `${brandColor(`installed`)} ${dim("@cloudflare/vite-plugin")}`, - }); - - updateViteConfig(); -}; - -const updateViteConfig = () => { - const filePath = "vite.config.ts"; - - transformFile(filePath, { - visitProgram(n) { - // Add an import of the @cloudflare/vite-plugin - // ``` - // import {cloudflare} from "@cloudflare/vite-plugin"; - // ``` - const lastImportIndex = n.node.body.findLastIndex( - (statement) => statement.type === "ImportDeclaration", - ); - const lastImport = n.get("body", lastImportIndex); - const importAst = b.importDeclaration( - [b.importSpecifier(b.identifier("cloudflare"))], - b.stringLiteral("@cloudflare/vite-plugin"), - ); - lastImport.insertAfter(importAst); - - return this.traverse(n); - }, - visitCallExpression: function (n) { - // Add the imported plugin to the config - // ``` - // defineConfig({ - // plugins: [react(), cloudflare()], - // }); - const callee = n.node.callee as types.namedTypes.Identifier; - if (callee.name !== "defineConfig") { - return this.traverse(n); - } - - const config = n.node.arguments[0]; - assert(t.ObjectExpression.check(config)); - const pluginsProp = config.properties.find((prop) => isPluginsProp(prop)); - assert(pluginsProp && t.ArrayExpression.check(pluginsProp.value)); - pluginsProp.value.elements.push( - b.callExpression(b.identifier("cloudflare"), [ - b.objectExpression([ - b.objectProperty( - b.identifier("viteEnvironment"), - b.objectExpression([ - b.objectProperty(b.identifier("name"), b.literal("ssr")), - ]), - ), - ]), - ]), - ); - - return false; - }, - }); -}; - -function isPluginsProp( - prop: unknown, -): prop is types.namedTypes.ObjectProperty | types.namedTypes.Property { - return ( - (t.Property.check(prop) || t.ObjectProperty.check(prop)) && - t.Identifier.check(prop.key) && - prop.key.name === "plugins" - ); -} - const config: TemplateConfig = { configVersion: 1, id: "tanstack", @@ -107,10 +27,6 @@ const config: TemplateConfig = { frameworkCli: "@tanstack/create-start", displayName: "TanStack Start", generate, - configure, - copyFiles: { - path: "./templates", - }, transformPackageJson: async () => ({ scripts: { deploy: `${npm} run build && wrangler deploy`, diff --git a/packages/create-cloudflare/templates/tanstack/templates/wrangler.jsonc b/packages/create-cloudflare/templates/tanstack/templates/wrangler.jsonc deleted file mode 100644 index e42e37d095d6..000000000000 --- a/packages/create-cloudflare/templates/tanstack/templates/wrangler.jsonc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "node_modules/wrangler/config-schema.json", - "name": "", - "compatibility_date": "", - "compatibility_flags": ["nodejs_compat"], - "main": "@tanstack/react-start/server-entry" -}