diff --git a/.changeset/six-pans-marry.md b/.changeset/six-pans-marry.md new file mode 100644 index 000000000000..68c8939d0db2 --- /dev/null +++ b/.changeset/six-pans-marry.md @@ -0,0 +1,11 @@ +--- +"create-cloudflare": minor +--- + +feature: Use new `vite-cloudflare` template in Remix projects. + +Remix has released a [new official Cloudflare template](https://remix.run/docs/en/main/future/vite#cloudflare-proxy) that uses `getPlatformProxy` under the hood to provide better support for bindings in dev. Remix projects created with C3 will now use this new template. + +Along with this change, projects will use the default vite-based dev command from `create-remix` instead of using `wrangler pages dev` on build output. + +A new `build-cf-types` script has also been added to re-generate the `Env` type defined in `load-context.ts` based on the contents of `wrangler.toml`. A default `wrangler.toml` will be added to new Remix projects to accomodate this workflow. diff --git a/packages/create-cloudflare/e2e-tests/fixtures/remix/app/routes/test.tsx b/packages/create-cloudflare/e2e-tests/fixtures/remix/app/routes/test.tsx new file mode 100644 index 000000000000..1b1483aa4389 --- /dev/null +++ b/packages/create-cloudflare/e2e-tests/fixtures/remix/app/routes/test.tsx @@ -0,0 +1,20 @@ +import type { LoaderFunctionArgs } from "@remix-run/cloudflare"; + +export async function loader({ context }: LoaderFunctionArgs) { + const { env } = context.cloudflare; + + const { TEST } = env; + + return new Response( + JSON.stringify({ + success: true, + test: TEST, + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + } + ); +} diff --git a/packages/create-cloudflare/e2e-tests/fixtures/remix/wrangler.toml b/packages/create-cloudflare/e2e-tests/fixtures/remix/wrangler.toml new file mode 100644 index 000000000000..4679b8cbbddd --- /dev/null +++ b/packages/create-cloudflare/e2e-tests/fixtures/remix/wrangler.toml @@ -0,0 +1,2 @@ +[vars] +TEST = "C3_TEST" diff --git a/packages/create-cloudflare/e2e-tests/frameworks.test.ts b/packages/create-cloudflare/e2e-tests/frameworks.test.ts index 40b974346f49..39008aaff0c8 100644 --- a/packages/create-cloudflare/e2e-tests/frameworks.test.ts +++ b/packages/create-cloudflare/e2e-tests/frameworks.test.ts @@ -34,7 +34,11 @@ import type { Suite } from "vitest"; const TEST_TIMEOUT = 1000 * 60 * 5; const LONG_TIMEOUT = 1000 * 60 * 10; -const TEST_RETRIES = 1; +const TEST_PM = process.env.TEST_PM ?? ""; +const NO_DEPLOY = process.env.E2E_NO_DEPLOY ?? false; +const TEST_RETRIES = process.env.E2E_RETRIES + ? parseInt(process.env.E2E_RETRIES) + : 1; type FrameworkTestConfig = RunnerConfig & { testCommitMessage: boolean; @@ -141,10 +145,21 @@ const frameworkTests: Record = { testCommitMessage: true, timeout: LONG_TIMEOUT, unsupportedPms: ["yarn"], + unsupportedOSs: ["win32"], verifyDeploy: { route: "/", expectedText: "Welcome to Remix", }, + verifyDev: { + route: "/test", + expectedText: "C3_TEST", + }, + verifyBuild: { + outputDir: "./build/client", + script: "build", + route: "/test", + expectedText: "C3_TEST", + }, }, next: { promptHandlers: [ @@ -272,20 +287,11 @@ describe.concurrent(`E2E: Web frameworks`, () => { }); Object.keys(frameworkTests).forEach((framework) => { - const { - quarantine, - timeout, - testCommitMessage, - unsupportedPms, - unsupportedOSs, - } = frameworkTests[framework]; + const { quarantine, timeout, unsupportedPms, unsupportedOSs } = + frameworkTests[framework]; const quarantineModeMatch = isQuarantineMode() == (quarantine ?? false); - const retries = process.env.E2E_RETRIES - ? parseInt(process.env.E2E_RETRIES) - : TEST_RETRIES; - // If the framework in question is being run in isolation, always run it. // Otherwise, only run the test if it's configured `quarantine` value matches // what is set in E2E_QUARANTINE @@ -294,7 +300,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { : quarantineModeMatch; // Skip if the package manager is unsupported - shouldRun &&= !unsupportedPms?.includes(process.env.TEST_PM ?? ""); + shouldRun &&= !unsupportedPms?.includes(TEST_PM); // Skip if the OS is unsupported shouldRun &&= !unsupportedOSs?.includes(process.platform); @@ -337,12 +343,10 @@ describe.concurrent(`E2E: Web frameworks`, () => { const wranglerPath = join(projectPath, "node_modules/wrangler"); expect(wranglerPath).toExist(); - if (testCommitMessage) { - await testDeploymentCommitMessage(projectName, framework); - } - // Make a request to the deployed project and verify it was successful await verifyDeployment( + framework, + projectName, `${deploymentUrl}${verifyDeploy.route}`, verifyDeploy.expectedText ); @@ -369,7 +373,7 @@ describe.concurrent(`E2E: Web frameworks`, () => { } }, { - retry: retries, + retry: TEST_RETRIES, timeout: timeout || TEST_TIMEOUT, } ); @@ -388,7 +392,7 @@ const runCli = async ( "webFramework", "--framework", framework, - "--deploy", + NO_DEPLOY ? "--no-deploy" : "--deploy", "--no-open", "--no-git", ]; @@ -396,6 +400,9 @@ const runCli = async ( args.push(...argv); const { output } = await runC3(args, promptHandlers, logStream); + if (NO_DEPLOY) { + return null; + } const deployedUrlRe = /deployment is ready at: (https:\/\/.+\.(pages|workers)\.dev)/; @@ -410,9 +417,21 @@ const runCli = async ( }; const verifyDeployment = async ( + framework: string, + projectName: string, deploymentUrl: string, expectedText: string ) => { + if (NO_DEPLOY) { + return; + } + + const { testCommitMessage } = frameworkTests[framework]; + + if (testCommitMessage) { + await testDeploymentCommitMessage(projectName, framework); + } + await retry({ times: 5 }, async () => { await sleep(1000); const res = await fetch(deploymentUrl); @@ -447,7 +466,7 @@ const verifyDevScript = async ( pm, "run", template.devScript as string, - pm === "npm" ? "--" : "", + ...(pm === "npm" ? ["--"] : []), "--port", `${TEST_PORT}`, ], diff --git a/packages/create-cloudflare/scripts/codemodDev.ts b/packages/create-cloudflare/scripts/codemodDev.ts index fa902fc84043..8624703b6434 100644 --- a/packages/create-cloudflare/scripts/codemodDev.ts +++ b/packages/create-cloudflare/scripts/codemodDev.ts @@ -57,6 +57,6 @@ const printSnippet = () => { `; const program = parseTs(snippet).program; - console.log(program.body[0].consequent); + console.log(program.body[0]); }; // printSnippet(); diff --git a/packages/create-cloudflare/templates/remix/c3.ts b/packages/create-cloudflare/templates/remix/c3.ts index 8a629f50d419..dde1bded3a17 100644 --- a/packages/create-cloudflare/templates/remix/c3.ts +++ b/packages/create-cloudflare/templates/remix/c3.ts @@ -1,4 +1,7 @@ import { logRaw } from "@cloudflare/cli"; +import { brandColor, dim } from "@cloudflare/cli/colors"; +import { spinner } from "@cloudflare/cli/interactive"; +import { transformFile } from "helpers/codemod"; import { runFrameworkGenerator } from "helpers/command.js"; import { detectPackageManager } from "helpers/packages"; import type { TemplateConfig } from "../../src/templates"; @@ -10,24 +13,55 @@ const generate = async (ctx: C3Context) => { await runFrameworkGenerator(ctx, [ ctx.project.name, "--template", - "https://github.com/remix-run/remix/tree/main/templates/cloudflare-pages", + "https://github.com/remix-run/remix/tree/main/templates/vite-cloudflare", ]); logRaw(""); // newline }; +const configure = async () => { + const typeDefsPath = "load-context.ts"; + + const s = spinner(); + s.start(`Updating \`${typeDefsPath}\``); + + // Remove the empty Env declaration from the template to allow the type from + // worker-configuration.d.ts to take over + transformFile(typeDefsPath, { + visitTSInterfaceDeclaration(n) { + if (n.node.id.type === "Identifier" && n.node.id.name !== "Env") { + return this.traverse(n); + } + + // Removes the node + n.replace(); + return false; + }, + }); + + s.stop(`${brandColor("updated")} \`${dim(typeDefsPath)}\``); +}; + const config: TemplateConfig = { configVersion: 1, id: "remix", - displayName: "Remix", platform: "pages", + displayName: "Remix", + copyFiles: { + path: "./templates", + }, generate, + configure, transformPackageJson: async () => ({ scripts: { - "pages:deploy": `${npm} run build && wrangler pages deploy ./public`, + deploy: `${npm} run build && wrangler pages deploy ./build/client`, + preview: `${npm} run build && wrangler pages dev ./build/client`, + "build-cf-types": `wrangler types`, }, }), devScript: "dev", + deployScript: "deploy", + previewScript: "preview", testFlags: ["--typescript", "--no-install", "--no-git-init"], }; export default config; diff --git a/packages/create-cloudflare/templates/remix/templates/worker-configuration.d.ts b/packages/create-cloudflare/templates/remix/templates/worker-configuration.d.ts new file mode 100644 index 000000000000..cc2db3f0d7e5 --- /dev/null +++ b/packages/create-cloudflare/templates/remix/templates/worker-configuration.d.ts @@ -0,0 +1,3 @@ +// Generated by Wrangler on Fri Feb 16 2024 15:52:18 GMT-0600 (Central Standard Time) +// After adding bindings to `wrangler.toml`, regenerate this interface via `npm build-cf-types` +interface Env {} diff --git a/packages/create-cloudflare/templates/remix/templates/wrangler.toml b/packages/create-cloudflare/templates/remix/templates/wrangler.toml new file mode 100644 index 000000000000..08652e52729b --- /dev/null +++ b/packages/create-cloudflare/templates/remix/templates/wrangler.toml @@ -0,0 +1,50 @@ +name = "" +compatibility_date = "" + +# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) +# Note: Use secrets to store sensitive data. +# Docs: https://developers.cloudflare.com/workers/platform/environment-variables +# [vars] +# MY_VARIABLE = "production_value" + +# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. +# Docs: https://developers.cloudflare.com/workers/runtime-apis/kv +# [[kv_namespaces]] +# binding = "MY_KV_NAMESPACE" +# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. +# Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ +# [[r2_buckets]] +# binding = "MY_BUCKET" +# bucket_name = "my-bucket" + +# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. +# Docs: https://developers.cloudflare.com/queues/get-started +# [[queues.producers]] +# binding = "MY_QUEUE" +# queue = "my-queue" + +# Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. +# Docs: https://developers.cloudflare.com/queues/get-started +# [[queues.consumers]] +# queue = "my-queue" + +# Bind another Worker service. Use this binding to call another Worker without network overhead. +# Docs: https://developers.cloudflare.com/workers/platform/services +# [[services]] +# binding = "MY_SERVICE" +# service = "my-service" + +# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. +# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. +# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects +# [[durable_objects.bindings]] +# name = "MY_DURABLE_OBJECT" +# class_name = "MyDurableObject" + +# Durable Object migrations. +# Docs: https://developers.cloudflare.com/workers/learning/using-durable-objects#configure-durable-object-classes-with-migrations +# [[migrations]] +# tag = "v1" +# new_classes = ["MyDurableObject"] diff --git a/packages/create-cloudflare/turbo.json b/packages/create-cloudflare/turbo.json index 0b40c14dc6a2..b95b1bf457c1 100644 --- a/packages/create-cloudflare/turbo.json +++ b/packages/create-cloudflare/turbo.json @@ -14,7 +14,8 @@ "FRAMEWORK_CLI_TO_TEST", "E2E_QUARANTINE", "E2E_PROJECT_PATH", - "E2E_RETRIES" + "E2E_RETRIES", + "E2E_NO_DEPLOY" ] } }