From 274c46109fe56d4e38cebcdf34a101ae9ab18dbb Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 3 Feb 2025 15:13:32 +0000 Subject: [PATCH 1/4] chore: C3 - use comment-json to parse and stringify JSON files This makes it possible to preserve comments in JSON(C) files that contain them. --- .../e2e-tests/frameworks.test.ts | 4 +- packages/create-cloudflare/package.json | 1 + .../create-cloudflare/src/helpers/files.ts | 18 ++++--- packages/create-cloudflare/src/templates.ts | 6 +-- .../templates-experimental/angular/c3.ts | 28 +++++++++-- .../create-cloudflare/templates/angular/c3.ts | 29 +++++++++-- .../create-cloudflare/templates/next/c3.ts | 5 +- pnpm-lock.yaml | 48 +++++++++++++++---- 8 files changed, 109 insertions(+), 30 deletions(-) diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index a91dd2b40429..8a090696478c 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -781,7 +781,9 @@ const addTestVarsToWranglerToml = async (projectPath: string) => { writeToml(wranglerTomlPath, wranglerToml); } else if (existsSync(wranglerJsonPath)) { - const wranglerJson = readJSON(wranglerJsonPath); + const wranglerJson = readJSON(wranglerJsonPath) as { + vars: Record; + }; // Add a TEST var to the wrangler.toml wranglerJson.vars ??= {}; wranglerJson.vars.TEST = "C3_TEST"; diff --git a/packages/create-cloudflare/package.json b/packages/create-cloudflare/package.json index 7964f97b6fc0..893057998faf 100644 --- a/packages/create-cloudflare/package.json +++ b/packages/create-cloudflare/package.json @@ -65,6 +65,7 @@ "@typescript-eslint/parser": "^6.9.0", "chalk": "^5.2.0", "command-exists": "^1.2.9", + "comment-json": "^4.2.5", "cross-spawn": "^7.0.3", "deepmerge": "^4.3.1", "degit": "^2.8.4", diff --git a/packages/create-cloudflare/src/helpers/files.ts b/packages/create-cloudflare/src/helpers/files.ts index 9dd18c9d97ec..afc55ef849ac 100644 --- a/packages/create-cloudflare/src/helpers/files.ts +++ b/packages/create-cloudflare/src/helpers/files.ts @@ -1,9 +1,9 @@ import fs, { existsSync, statSync } from "fs"; import { join } from "path"; import TOML from "@iarna/toml"; -import { parse } from "jsonc-parser"; +import { parse, stringify } from "comment-json"; import type { JsonMap } from "@iarna/toml"; -import type { C3Context } from "types"; +import type { C3Context, PackageJson } from "types"; export const copyFile = (path: string, dest: string) => { try { @@ -57,7 +57,7 @@ export const directoryExists = (path: string): boolean => { } }; -export const readJSON = (path: string) => { +export const readJSON = (path: string): unknown => { const contents = readFile(path); return contents ? parse(contents) : contents; }; @@ -67,8 +67,12 @@ export const readToml = (path: string) => { return contents ? TOML.parse(contents) : {}; }; -export const writeJSON = (path: string, object: object, stringifySpace = 2) => { - writeFile(path, JSON.stringify(object, null, stringifySpace)); +export const writeJSON = ( + path: string, + object: unknown, + stringifySpace = 2, +) => { + writeFile(path, stringify(object, null, stringifySpace)); }; export const writeToml = (path: string, object: JsonMap) => { @@ -132,8 +136,8 @@ export const usesEslint = (ctx: C3Context): EslintUsageInfo => { } try { - const pkgJson = readJSON(`${ctx.project.path}/package.json`); - if (pkgJson.eslintConfig) { + const pkgJson = readJSON(`${ctx.project.path}/package.json`) as PackageJson; + if (pkgJson?.eslintConfig) { return { used: true, configType: "package.json", diff --git a/packages/create-cloudflare/src/templates.ts b/packages/create-cloudflare/src/templates.ts index ace78c71acde..138ac500a157 100644 --- a/packages/create-cloudflare/src/templates.ts +++ b/packages/create-cloudflare/src/templates.ts @@ -717,7 +717,7 @@ export const updatePackageName = async (ctx: C3Context) => { // Update package.json with project name const placeholderNames = ["", "TBD", ""]; const pkgJsonPath = resolve(ctx.project.path, "package.json"); - const pkgJson = readJSON(pkgJsonPath); + const pkgJson = readJSON(pkgJsonPath) as PackageJson; if (!placeholderNames.includes(pkgJson.name)) { return; @@ -741,11 +741,11 @@ export const updatePackageScripts = async (ctx: C3Context) => { s.start("Updating `package.json` scripts"); const pkgJsonPath = resolve(ctx.project.path, "package.json"); - let pkgJson = readJSON(pkgJsonPath); + let pkgJson = readJSON(pkgJsonPath) as PackageJson; // Run any transformers defined by the template const transformed = await ctx.template.transformPackageJson(pkgJson, ctx); - pkgJson = deepmerge(pkgJson, transformed); + pkgJson = deepmerge(pkgJson, transformed as PackageJson); writeJSON(pkgJsonPath, pkgJson); s.stop(`${brandColor("updated")} ${dim("`package.json`")}`); diff --git a/packages/create-cloudflare/templates-experimental/angular/c3.ts b/packages/create-cloudflare/templates-experimental/angular/c3.ts index cbdd80278ab2..70e7e89d60c7 100644 --- a/packages/create-cloudflare/templates-experimental/angular/c3.ts +++ b/packages/create-cloudflare/templates-experimental/angular/c3.ts @@ -7,7 +7,7 @@ import { readFile, readJSON, writeFile } from "helpers/files"; import { detectPackageManager } from "helpers/packageManagers"; import { installPackages } from "helpers/packages"; import type { TemplateConfig } from "../../src/templates"; -import type { C3Context } from "types"; +import type { C3Context, PackageJson } from "types"; const { npm } = detectPackageManager(); @@ -63,10 +63,10 @@ async function updateAppCode() { // Remove unwanted dependencies s.start(`Updating package.json`); const packageJsonPath = resolve("package.json"); - const packageManifest = readJSON(packageJsonPath); + const packageManifest = readJSON(packageJsonPath) as PackageJson; - delete packageManifest["dependencies"]["express"]; - delete packageManifest["devDependencies"]["@types/express"]; + delete packageManifest["dependencies"]?.["express"]; + delete packageManifest["devDependencies"]?.["@types/express"]; writeFile(packageJsonPath, JSON.stringify(packageManifest, null, 2)); s.stop(`${brandColor(`updated`)} ${dim(`\`package.json\``)}`); @@ -75,7 +75,7 @@ async function updateAppCode() { function updateAngularJson(ctx: C3Context) { const s = spinner(); s.start(`Updating angular.json config`); - const angularJson = readJSON(resolve("angular.json")); + const angularJson = readJSON(resolve("angular.json")) as AngularJson; // Update builder const architectSection = angularJson.projects[ctx.project.name].architect; architectSection.build.options.outputPath = "dist"; @@ -110,3 +110,21 @@ const config: TemplateConfig = { }), }; export default config; + +type AngularJson = { + projects: Record< + string, + { + architect: { + build: { + options: { + outputPath: string; + outputMode: string; + ssr: Record; + assets: string[]; + }; + }; + }; + } + >; +}; diff --git a/packages/create-cloudflare/templates/angular/c3.ts b/packages/create-cloudflare/templates/angular/c3.ts index c11c5855c159..88facc921287 100644 --- a/packages/create-cloudflare/templates/angular/c3.ts +++ b/packages/create-cloudflare/templates/angular/c3.ts @@ -8,7 +8,7 @@ import { readFile, readJSON, writeFile } from "helpers/files"; import { detectPackageManager } from "helpers/packageManagers"; import { installPackages } from "helpers/packages"; import type { TemplateConfig } from "../../src/templates"; -import type { C3Context } from "types"; +import type { C3Context, PackageJson } from "types"; const { npm } = detectPackageManager(); @@ -64,10 +64,10 @@ async function updateAppCode() { // Remove unwanted dependencies s.start(`Updating package.json`); const packageJsonPath = resolve("package.json"); - const packageManifest = readJSON(packageJsonPath); + const packageManifest = readJSON(packageJsonPath) as PackageJson; - delete packageManifest["dependencies"]["express"]; - delete packageManifest["devDependencies"]["@types/express"]; + delete packageManifest["dependencies"]?.["express"]; + delete packageManifest["devDependencies"]?.["@types/express"]; writeFile(packageJsonPath, JSON.stringify(packageManifest, null, 2)); s.stop(`${brandColor(`updated`)} ${dim(`\`package.json\``)}`); @@ -76,7 +76,8 @@ async function updateAppCode() { function updateAngularJson(ctx: C3Context) { const s = spinner(); s.start(`Updating angular.json config`); - const angularJson = readJSON(resolve("angular.json")); + const angularJson = readJSON("angular.json") as AngularJson; + // Update builder const architectSection = angularJson.projects[ctx.project.name].architect; architectSection.build.options.outputPath = "dist"; @@ -112,3 +113,21 @@ const config: TemplateConfig = { }), }; export default config; + +type AngularJson = { + projects: Record< + string, + { + architect: { + build: { + options: { + outputPath: string; + outputMode: string; + ssr: Record; + assets: string[]; + }; + }; + }; + } + >; +}; diff --git a/packages/create-cloudflare/templates/next/c3.ts b/packages/create-cloudflare/templates/next/c3.ts index 4202bf280a70..9371bad3acaa 100644 --- a/packages/create-cloudflare/templates/next/c3.ts +++ b/packages/create-cloudflare/templates/next/c3.ts @@ -125,7 +125,10 @@ export const shouldInstallNextOnPagesEslintPlugin = async ( }; export const writeEslintrc = async (ctx: C3Context): Promise => { - const eslintConfig = readJSON(`${ctx.project.path}/.eslintrc.json`); + const eslintConfig = readJSON(`${ctx.project.path}/.eslintrc.json`) as { + plugins: string[]; + extends: string | string[]; + }; eslintConfig.plugins ??= []; eslintConfig.plugins.push("eslint-plugin-next-on-pages"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2af8f317ad19..d4407d06307e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1172,6 +1172,9 @@ importers: command-exists: specifier: ^1.2.9 version: 1.2.9 + comment-json: + specifier: ^4.2.5 + version: 4.2.5 cross-spawn: specifier: ^7.0.3 version: 7.0.3 @@ -1291,7 +1294,7 @@ importers: version: 8.57.0 eslint-config-turbo: specifier: latest - version: 2.3.4(eslint@8.57.0)(turbo@2.2.3) + version: 2.4.0(eslint@8.57.0)(turbo@2.2.3) eslint-plugin-import: specifier: 2.26.x version: 2.26.0(@typescript-eslint/parser@6.10.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0) @@ -6000,6 +6003,9 @@ packages: resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} engines: {node: '>= 0.4'} + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -6474,6 +6480,10 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + comment-json@4.2.5: + resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} + engines: {node: '>= 6'} + common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} @@ -7064,8 +7074,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-config-turbo@2.3.4: - resolution: {integrity: sha512-MxPl+IKkR7mRGcHoiZAMHYl+RZnjqBsxTLf+IGnx8BrJQe9/CoLT7oBlUxXGvh9bsd5MTaqCxly5h8BE1v/7AA==} + eslint-config-turbo@2.4.0: + resolution: {integrity: sha512-AiRdy83iwyG4+iMSxXQGUbEClxkGxSlXYH8E2a+0972ao75OWnlDBiiuLMOzDpJubR+QVGC4zonn29AIFCSbFw==} peerDependencies: eslint: '>6.6.0' turbo: '>2.0.0' @@ -7140,8 +7150,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - eslint-plugin-turbo@2.3.4: - resolution: {integrity: sha512-9ncoUJkQGkC28NmlQiS17oB9mrE8XaSulRZiB5pv9vmRbYjOfUwyGhY3EIcoBRdww81igxOzXmAmvNNd6GFBPg==} + eslint-plugin-turbo@2.4.0: + resolution: {integrity: sha512-qCgoRi/OTc1VMxab7+sdKiV1xlkY4qjK9sM+kS7+WogrB1DxLguJSQXvk4HA13SD5VmJsq+8FYOw5q4EUk6Ixg==} peerDependencies: eslint: '>6.6.0' turbo: '>2.0.0' @@ -7612,6 +7622,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -9693,6 +9707,10 @@ packages: remove-accents@0.4.2: resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -14950,6 +14968,8 @@ snapshots: get-intrinsic: 1.2.5 is-string: 1.0.7 + array-timsort@1.0.3: {} + array-union@2.1.0: {} array.prototype.flat@1.3.1: @@ -15499,6 +15519,14 @@ snapshots: commander@7.2.0: {} + comment-json@4.2.5: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + common-path-prefix@3.0.0: {} commondir@1.0.1: {} @@ -16274,10 +16302,10 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-config-turbo@2.3.4(eslint@8.57.0)(turbo@2.2.3): + eslint-config-turbo@2.4.0(eslint@8.57.0)(turbo@2.2.3): dependencies: eslint: 8.57.0 - eslint-plugin-turbo: 2.3.4(eslint@8.57.0)(turbo@2.2.3) + eslint-plugin-turbo: 2.4.0(eslint@8.57.0)(turbo@2.2.3) turbo: 2.2.3 eslint-import-resolver-node@0.3.7: @@ -16362,7 +16390,7 @@ snapshots: semver: 6.3.1 string.prototype.matchall: 4.0.8 - eslint-plugin-turbo@2.3.4(eslint@8.57.0)(turbo@2.2.3): + eslint-plugin-turbo@2.4.0(eslint@8.57.0)(turbo@2.2.3): dependencies: dotenv: 16.0.3 eslint: 8.57.0 @@ -16958,6 +16986,8 @@ snapshots: has-flag@4.0.0: {} + has-own-prop@2.0.0: {} + has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 @@ -19032,6 +19062,8 @@ snapshots: remove-accents@0.4.2: {} + repeat-string@1.6.1: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} From a1caf6df94fad1594a697b22fadbaf44ca48c6aa Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 3 Feb 2025 15:14:16 +0000 Subject: [PATCH 2/4] test: C3 - tiny refactoring of workers tests for wrangler config existence --- packages/create-cloudflare/e2e-tests/workers.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/create-cloudflare/e2e-tests/workers.test.ts b/packages/create-cloudflare/e2e-tests/workers.test.ts index c7229107e732..fff02866b522 100644 --- a/packages/create-cloudflare/e2e-tests/workers.test.ts +++ b/packages/create-cloudflare/e2e-tests/workers.test.ts @@ -1,3 +1,4 @@ +import { existsSync } from "fs"; import { join } from "path"; import { readJSON, readToml } from "helpers/files"; import { detectPackageManager } from "helpers/packageManagers"; @@ -197,18 +198,20 @@ describe const tomlPath = join(project.path, "wrangler.toml"); const jsonPath = join(project.path, "wrangler.json"); - try { - expect(jsonPath).toExist(); + if (existsSync(jsonPath)) { const config = readJSON(jsonPath) as { main?: string }; if (config.main) { expect(join(project.path, config.main)).toExist(); } - } catch (e) { - expect(tomlPath).toExist(); + } else if (existsSync(tomlPath)) { const config = readToml(tomlPath) as { main?: string }; if (config.main) { expect(join(project.path, config.main)).toExist(); } + } else { + expect.fail( + `Expected at least one of "${jsonPath}" and "${tomlPath}" to exist.`, + ); } const { verifyDeploy, verifyTest } = testConfig; From f5edeca28ff4cbc6f0accec1bcd1e81bd1787c8b Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 3 Feb 2025 18:55:09 +0000 Subject: [PATCH 3/4] feat: C3 - add experimental Vite+workerd on Workers+Assets template --- .../create-cloudflare/src/helpers/codemod.ts | 4 +- packages/create-cloudflare/src/templates.ts | 2 + .../templates-experimental/react/c3.ts | 188 ++++++++++++++++++ .../react/js/api/index.js | 13 ++ .../react/js/src/App.jsx | 54 +++++ .../react/js/wrangler.json | 9 + .../react/ts/api/index.ts | 17 ++ .../react/ts/src/App.tsx | 54 +++++ .../react/ts/tsconfig.worker.json | 8 + .../react/ts/wrangler.json | 9 + 10 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 packages/create-cloudflare/templates-experimental/react/c3.ts create mode 100644 packages/create-cloudflare/templates-experimental/react/js/api/index.js create mode 100644 packages/create-cloudflare/templates-experimental/react/js/src/App.jsx create mode 100644 packages/create-cloudflare/templates-experimental/react/js/wrangler.json create mode 100644 packages/create-cloudflare/templates-experimental/react/ts/api/index.ts create mode 100644 packages/create-cloudflare/templates-experimental/react/ts/src/App.tsx create mode 100644 packages/create-cloudflare/templates-experimental/react/ts/tsconfig.worker.json create mode 100644 packages/create-cloudflare/templates-experimental/react/ts/wrangler.json diff --git a/packages/create-cloudflare/src/helpers/codemod.ts b/packages/create-cloudflare/src/helpers/codemod.ts index ae7e1313e4f8..b6a049fd62d7 100644 --- a/packages/create-cloudflare/src/helpers/codemod.ts +++ b/packages/create-cloudflare/src/helpers/codemod.ts @@ -155,7 +155,9 @@ export const mergeObjectProperties = ( }); }; -const getPropertyName = (newProp: recast.types.namedTypes.ObjectProperty) => { +export const getPropertyName = ( + newProp: recast.types.namedTypes.ObjectProperty, +) => { return newProp.key.type === "Identifier" ? newProp.key.name : newProp.key.type === "StringLiteral" diff --git a/packages/create-cloudflare/src/templates.ts b/packages/create-cloudflare/src/templates.ts index 138ac500a157..d5f30980a9c1 100644 --- a/packages/create-cloudflare/src/templates.ts +++ b/packages/create-cloudflare/src/templates.ts @@ -29,6 +29,7 @@ import honoTemplateExperimental from "templates-experimental/hono/c3"; import nextTemplateExperimental from "templates-experimental/next/c3"; import nuxtTemplateExperimental from "templates-experimental/nuxt/c3"; import qwikTemplateExperimental from "templates-experimental/qwik/c3"; +import reactTemplateExperimental from "templates-experimental/react/c3"; import remixTemplateExperimental from "templates-experimental/remix/c3"; import solidTemplateExperimental from "templates-experimental/solid/c3"; import svelteTemplateExperimental from "templates-experimental/svelte/c3"; @@ -177,6 +178,7 @@ export function getFrameworkMap({ experimental = false }): TemplateMap { next: nextTemplateExperimental, nuxt: nuxtTemplateExperimental, qwik: qwikTemplateExperimental, + react: reactTemplateExperimental, remix: remixTemplateExperimental, solid: solidTemplateExperimental, svelte: svelteTemplateExperimental, diff --git a/packages/create-cloudflare/templates-experimental/react/c3.ts b/packages/create-cloudflare/templates-experimental/react/c3.ts new file mode 100644 index 000000000000..c24cbf4aa64c --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/react/c3.ts @@ -0,0 +1,188 @@ +import assert from "assert"; +import { logRaw } from "@cloudflare/cli"; +import { brandColor, dim } from "@cloudflare/cli/colors"; +import { inputPrompt, spinner } from "@cloudflare/cli/interactive"; +import { runFrameworkGenerator } from "frameworks/index"; +import { + getPropertyName, + mergeObjectProperties, + transformFile, +} from "helpers/codemod"; +import { readJSON, usesTypescript, writeJSON } from "helpers/files"; +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 { npm } = detectPackageManager(); + +const generate = async (ctx: C3Context) => { + const variant = await getVariant(); + ctx.args.lang = variant.lang; + + await runFrameworkGenerator(ctx, [ + ctx.project.name, + "--template", + variant?.value, + ]); + + logRaw(""); +}; + +const configure = async (ctx: C3Context) => { + await installPackages(["@cloudflare/vite-plugin"], { + dev: true, + startText: "Installing the Cloudflare Vite plugin", + doneText: `${brandColor(`updated`)} ${dim("wrangler@latest")}`, + }); + + await transformViteConfig(ctx); + + if (usesTypescript(ctx)) { + updateTsconfigJson(); + } +}; + +function transformViteConfig(ctx: C3Context) { + const filePath = `vite.config.${usesTypescript(ctx) ? "ts" : "js"}`; + + transformFile(filePath, { + visitProgram(n) { + // Add an import of the @cloudflare/vite-plugin + // ``` + // import {cloudflare} from "@cloudflare/vite-plugin"; + // ``` + const lastImportIndex = n.node.body.findLastIndex( + (t) => t.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] as types.namedTypes.ObjectExpression; + + const pluginsProp = config.properties.find( + (prop) => + prop.type === "ObjectProperty" && getPropertyName(prop) === "plugins", + ) as types.namedTypes.ObjectProperty; + const pluginsArray = + pluginsProp.value as types.namedTypes.ArrayExpression; + + mergeObjectProperties( + n.node.arguments[0] as types.namedTypes.ObjectExpression, + [ + b.objectProperty( + b.identifier("plugins"), + b.arrayExpression([ + ...pluginsArray.elements, + b.callExpression(b.identifier("cloudflare"), []), + ]), + ), + ], + ); + + return false; + }, + }); +} + +function updateTsconfigJson() { + const s = spinner(); + s.start(`Updating tsconfig.json config`); + // Add a reference to the extra tsconfig.worker.json file. + // ``` + // "references": [ ..., { path: "./tsconfig.worker.json" } ] + // ``` + const tsconfig = readJSON("tsconfig.json") as { references: object[] }; + if (tsconfig && typeof tsconfig === "object") { + tsconfig.references ??= []; + tsconfig.references.push({ path: "./tsconfig.worker.json" }); + } + writeJSON("tsconfig.json", tsconfig); + s.stop(`${brandColor(`updated`)} ${dim(`\`angular.json\``)}`); +} + +async function getVariant() { + const variantsOptions = [ + { + value: "react-ts", + lang: "ts", + label: "TypeScript", + }, + { + value: "react-swc-ts", + lang: "ts", + label: "TypeScript + SWC", + }, + { + value: "react", + lang: "js", + label: "JavaScript", + }, + { + value: "react-swc", + lang: "js", + label: "JavaScript + SWC", + }, + ]; + const value = await inputPrompt({ + type: "select", + question: "Select a variant:", + label: "variant", + options: variantsOptions, + defaultValue: variantsOptions[0].value, + }); + + const selected = variantsOptions.find((variant) => variant.value === value); + assert(selected, "Expected a variant to be selected"); + return selected; +} + +const config: TemplateConfig = { + configVersion: 1, + id: "react", + frameworkCli: "create-vite", + displayName: "React", + platform: "workers", + path: "templates-experimental/react", + copyFiles: { + variants: { + ts: { + path: "./ts", + }, + js: { + path: "./js", + }, + }, + }, + generate, + configure, + transformPackageJson: async () => ({ + scripts: { + deploy: `${npm} run build && wrangler deploy`, + }, + }), + devScript: "dev", + deployScript: "deploy", + previewScript: "preview", +}; +export default config; diff --git a/packages/create-cloudflare/templates-experimental/react/js/api/index.js b/packages/create-cloudflare/templates-experimental/react/js/api/index.js new file mode 100644 index 000000000000..5f8fd38675f3 --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/react/js/api/index.js @@ -0,0 +1,13 @@ +export default { + fetch(request, env) { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api/")) { + return Response.json({ + name: "Cloudflare", + }); + } + + return env.ASSETS.fetch(request); + }, +} diff --git a/packages/create-cloudflare/templates-experimental/react/js/src/App.jsx b/packages/create-cloudflare/templates-experimental/react/js/src/App.jsx new file mode 100644 index 000000000000..1742ae16f3e0 --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/react/js/src/App.jsx @@ -0,0 +1,54 @@ +import { useState } from "react"; +import reactLogo from "./assets/react.svg"; +import viteLogo from "/vite.svg"; +import "./App.css"; + +function App() { + const [count, setCount] = useState(0); + const [name, setName] = useState("unknown"); + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+
+ +

+ Edit api/index.ts to change the name +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/packages/create-cloudflare/templates-experimental/react/js/wrangler.json b/packages/create-cloudflare/templates-experimental/react/js/wrangler.json new file mode 100644 index 000000000000..8ed92c88ebf9 --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/react/js/wrangler.json @@ -0,0 +1,9 @@ +{ + "name": "", + "main": "api/index.js", + "compatibility_date": "", + "assets": { "not_found_handling": "single-page-application", "binding": "ASSETS" }, + "observability": { + "enabled": true + } +} diff --git a/packages/create-cloudflare/templates-experimental/react/ts/api/index.ts b/packages/create-cloudflare/templates-experimental/react/ts/api/index.ts new file mode 100644 index 000000000000..f9b59120e4db --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/react/ts/api/index.ts @@ -0,0 +1,17 @@ +interface Env { + ASSETS: Fetcher; +} + +export default { + fetch(request, env) { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api/")) { + return Response.json({ + name: "Cloudflare", + }); + } + + return env.ASSETS.fetch(request); + }, +} satisfies ExportedHandler; diff --git a/packages/create-cloudflare/templates-experimental/react/ts/src/App.tsx b/packages/create-cloudflare/templates-experimental/react/ts/src/App.tsx new file mode 100644 index 000000000000..1b918322e45a --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/react/ts/src/App.tsx @@ -0,0 +1,54 @@ +import { useState } from "react"; +import reactLogo from "./assets/react.svg"; +import viteLogo from "/vite.svg"; +import "./App.css"; + +function App() { + const [count, setCount] = useState(0); + const [name, setName] = useState("unknown"); + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+
+ +

+ Edit api/index.ts to change the name +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/packages/create-cloudflare/templates-experimental/react/ts/tsconfig.worker.json b/packages/create-cloudflare/templates-experimental/react/ts/tsconfig.worker.json new file mode 100644 index 000000000000..1f58f0e8d468 --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/react/ts/tsconfig.worker.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo", + "types": ["@cloudflare/workers-types/2023-07-01", "vite/client"], + }, + "include": ["api"], +} diff --git a/packages/create-cloudflare/templates-experimental/react/ts/wrangler.json b/packages/create-cloudflare/templates-experimental/react/ts/wrangler.json new file mode 100644 index 000000000000..2d81848635f5 --- /dev/null +++ b/packages/create-cloudflare/templates-experimental/react/ts/wrangler.json @@ -0,0 +1,9 @@ +{ + "name": "", + "main": "api/index.ts", + "compatibility_date": "", + "assets": { "not_found_handling": "single-page-application", "binding": "ASSETS" }, + "observability": { + "enabled": true + } +} From e19b405c658e2c3ac92c62ce0956f3a02f394a46 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 4 Feb 2025 11:12:25 +0000 Subject: [PATCH 4/4] test --- .../e2e-tests/frameworks.test.ts | 18 +++++++++++++++++- .../templates-experimental/react/c3.ts | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index 8a090696478c..c9f76a9505cf 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -100,6 +100,23 @@ function getFrameworkTests(opts: { }, flags: ["--style", "sass"], }, + react: { + promptHandlers: [ + { + matcher: /Select a variant:/, + input: [keys.enter], + }, + ], + testCommitMessage: true, + verifyDeploy: { + route: "/", + expectedText: "Vite + React", + }, + verifyPreview: { + route: "/", + expectedText: "Vite + React", + }, + }, gatsby: { unsupportedPms: ["bun", "pnpm"], promptHandlers: [ @@ -534,7 +551,6 @@ function getFrameworkTests(opts: { ], testCommitMessage: true, unsupportedOSs: ["win32"], - unsupportedPms: ["yarn"], timeout: LONG_TIMEOUT, verifyDeploy: { route: "/", diff --git a/packages/create-cloudflare/templates-experimental/react/c3.ts b/packages/create-cloudflare/templates-experimental/react/c3.ts index c24cbf4aa64c..cd9c094da756 100644 --- a/packages/create-cloudflare/templates-experimental/react/c3.ts +++ b/packages/create-cloudflare/templates-experimental/react/c3.ts @@ -179,6 +179,7 @@ const config: TemplateConfig = { transformPackageJson: async () => ({ scripts: { deploy: `${npm} run build && wrangler deploy`, + preview: `${npm} vite build && vite preview`, }, }), devScript: "dev",