cloudflare / workers-sdk

⛅️ Home to Wrangler, the CLI for Cloudflare Workers®
https://developers.cloudflare.com/workers/
Apache License 2.0
2.42k stars 594 forks source link

Do not deprecate wrangler's proxy feature, e.g. `wrangler pages dev -- vite` or keep at least the injection part with a new api such as `wrangler inject -- vite` #5315

Open newsve opened 3 months ago

newsve commented 3 months ago

Edit: I edited the issue title since this thread has been evolved about how to inject CF env vars into a hot reloading dev server, the actual issue is further down: https://github.com/cloudflare/workers-sdk/issues/5315#issuecomment-2012389845

Which Cloudflare product(s) does this pertain to?

Pages

What version(s) of the tool(s) are you using?

3.35.0

What version of Node are you using?

v20.11.1

What operating system and version are you using?

Ubuntu 22.04.2 LTS

Describe the Bug

Observed behavior

When the app redirects wrangler pages dev does the redirect but the URL in the browser's URL bar doesn't change (or redirect's header lacks the redirect status and new URL and the response header lacks a location header.

This is a big problem because it breaks relative links on redirected pages and doesn't reflect production.

Expected behavior

A proper redirect with URL change and proper headers

Steps to reproduce

git clone https://github.com/newsve/wrangler-no-redirect.git
npm i
npm run dev

In your browser go first to localhost:5713 which is the og SvelteKit/Vite server without wrangler, you see a redirect happng to localhost:5713/some-random-page. Now try the same with localhost:8788 which is via wrangler, still redirects but the URL in the URL bar does not change.

Also check and compare the headers (left: 5713, right 8788), note that the 8788 one lacks a response location header:

image

Please provide a link to a minimal reproduction

git clone https://github.com/newsve/wrangler-no-redirect.git

Please provide any relevant error logs

No response

GregBrimble commented 3 months ago

Thanks for the report. This can be fixed by building assets to a directory and pointing Wrangler at that (wrangler pages dev <directory>), rather than using the proxy feature (wrangler pages dev -- <command>). This proxy feature will be deprecated shortly because of incompatibilities like you flag in this issue: #5317.

newsve commented 3 months ago

Thanks for your prompt answer.

When I develop I usually don't have a build yet which wrangler pages dev could consume. And, I assumed wrangler pages dev is for developing, so running dev envs with HMR and live reload, etc., it is not meant for testing static production builds without hot realod/HMR. Please correct me if I am wrong.

Most modern web frameworks are based on Vite and when I look into, e.g. SvelteKit's output folder .svelte-kit I don't see any cloudflare/ folder when in developing mode (vite dev or just vite). Only when I do a build with vite build, then I see that folder. But actually I never do latter because CF does this automatically when pushing to GH. So, not sure how the dev workflow would be then.

So, maybe I do miss something and you can hint me with an example wrangler command targeting a Vite framework such as SvelteKit how this should work when developing with hot reload. Thanks!

GregBrimble commented 3 months ago

Yes, unfortunately the best we can suggest right now is running vite build --watch (https://vitejs.dev/guide/build#rebuild-on-files-changes) and simultaneously, wrangler pages dev <directory>. We're actively considering how to improve this experience, so please bear with us. cc @dario-piotrowicz

newsve commented 3 months ago

But will wrangler pages dev still inject Cloudflare binds, e.g. to services like KV, or the props on request.cf, so the framework can consume those? This is somewhat the key USP of wrangler.

GregBrimble commented 3 months ago

Yes, it will.

newsve commented 3 months ago

Just an idea:

Right now, I use only the output of vite's dev server on 5173 despite wrangler started and proxied latter. And it is great because it has already all injected binds without the need to use wrangler's proxied version on 8788.

A dev doesn't need more. So if there is way to get just the binds injected into vite dev without actually wrangler proxying vite would be already a huge step (because something like vite build --watch isn't as nice as vite dev for development).

IgorMinar commented 3 months ago

hi @newsve, if you use the latest cloudflare adapter for sveltekit it will allow you to access all the bindings from within sveltekit's dev server. you can scaffold a new project using create-cloudflare to test it out and check out https://developers.cloudflare.com/pages/framework-guides/deploy-a-svelte-site/#sveltekit-cloudflare-configuration - the docs don't mention it yet (WIP) but you can configure the bindings via wrangler.toml. Again, check out a clean create-cloudflare project where all of this is preconfigured already.

newsve commented 3 months ago

Thanks, I just skimmed through a create-cloudflare installation, and wow, super impressive! All the supported frameworks and in one place, amazing!

One question though: I can't use the locally emulated Cloudflare services anymore like with wrangler in dev mode (wrangler doesn't run with npm run dev, just with build and preview), so I would need to bind my local dev server with an actual Cloudflare service in the cloud and have there always min. two of each kind (one for dev and one for production).

Did I get it right?

IgorMinar commented 3 months ago

Did I get it right?

not quite, with the create-cloudflare integration we start a wrangler/miniflare process behind the scenes and integrate it into vite. So you can access a local D1 database, or KV store from your app while it's running via vite dev

newsve commented 3 months ago

Incredible! Is vite then somehow patched? Because in package.json, I only find "dev": "vite dev", (or do you mean the preview and build scripts?) and in vite.config.js I find no invocation of wrangler/miniflare either:

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';

export default defineConfig({
        plugins: [sveltekit()],
        test: {
                include: ['src/**/*.{test,spec}.{js,ts}']
        }
});
newsve commented 3 months ago

tl;dr:

Hey Cloudflare Team,

Wanted to flag again a potential issue with deprecating Wrangler's Pages Dev Proxy functionality. Please follow these steps setting up a Svelte project with your new create-cloudflare CLI (which is again highly impressive!):

npm create cloudflare@latest // Chose Create app/Website o. app/Svelte/Typescript

Now, add src/routes/+page.server.ts:

import type { PageServerLoad } from './$types'

export const load: PageServerLoad = async ({ request }) => {
  console.log(request.cf)
  return
}

Try npm run dev and check the server logs, access the new app in your browser, console.log prints nothing.

Try npm run preview and check the server logs, and yes, request.cf is set! but you don't get:

  1. Must-have: Rebuild on file changed (somehow doesn't work)
  2. Must-have: Live-reload in the browser (this wouldn't work if even 1 did work)
  3. Nice-to-have: HMR in the browser (this wouldn't work if even 1 did work)

2 and 3 rely on Vite's unmatched dev server which is key to Vite's success and which you can't use with npm run preview.

The current developer experience with Cloudflare services takes a hit due to the necessity of restarting the server or refreshing the browser on every file change. This becomes especially problematic for those relying on Cloudflare-specific features, such as request.cf, to dynamically adjust behavior based on the node's location. The cumbersome workaround of using npm run preview isn't a viable solution for a smooth and efficient development workflow.

I'm concerned this move might push developers towards alternative solutions or other providers, particularly when the ease of integration and testing of CF services is compromised. The deprecation of wrangler pages dev -- could lead to missed opportunities in leveraging Cloudflare to its full potential.

Would it be possible to explore a solution that allows injecting Cloudflare's features into the development environment, specifically through npm run dev, e.g. with something like wrangler inject -- vite, without fully acting as a proxy? Currently, I bypass the proxy's verbosity on 8788 and limitations by directly using Vite's output on 5173. So, we would need just the injecting part of wrangler pages dev -- vite.

aroman commented 3 months ago

Strongly agree with @newsve's points above... I was shocked to learn that wrangler is deprecating the proxy feature without a viable replacement (vite build --watch will not meet our needs).

To be clear: hot-reload support is not a "nice-to-have" for us, it is essential to our workflow: we literally cannot develop without it. Our application has a lot of intricate client state (we're building games) and forcing hard-reloads fundamentally breaks our team's workflow. cc: @admah

newsve commented 3 months ago

fwiw, I changed issue title/subject since the actual issue evolved to env var injection to hot reloading dev servers such as vite dev (instead of starting a new thread), hope that's ok and look forward to CF's thoughts and ideas on how to tackle this issue

malobre commented 3 months ago

And even if you don't need HMR, the live-reload feature does not work, see #5351.

ducan-ne commented 3 months ago

Try to upgrade to the latest of wrangler and have to to downgrade after then because of this, at least provide a way to make it work with current tooling, AFAIK it's impossible to do it with Vite to support HMR, bundleless, etc. Or you guys want me to move to rollup and rollup --watch

ducan-ne commented 3 months ago

As user perpective, I think honox API is the BOAT here, they provide a vite plugin and I don't need to do anything Below is my command, is pretty long but runable

pnpm dlx kill-port-process 8788 && wrangler pages dev --compatibility-date=2024-04-01 --compatibility-flag nodejs_compat --proxy 5173 --log-level log -- vite --host 0.0.0.0 -l silent

// ps: yes I need to kill-port-process 8788 because wrangler randomly not teardown process if it's crash or something honox is pretty good at DX, however they generate a _worker.js as output, which is I understand will cost 1 request for each static file.

kakauandme commented 2 months ago

+1 to the issue

jonaswre commented 2 months ago

After chasing this problem for 2 days. I realised there is a way to integrate miniflare without using wrangles pages dev at all.

the wrangler package provides a function called getPlatformProxy that will give you access to everything.

The svelte adapter handles it in the following manner.

https://github.com/sveltejs/kit/blob/bbab296f6fcc05af6b999182798bcdedabbaa4c9/packages/adapter-cloudflare/index.js#L130

AVGVSTVS96 commented 2 months ago

+1

I'm trying to find a solution to be able to run a dev server for my Astro + Pages Function GPT Chat application without -- <command>. Like mentioned by others here, it's important to be able to use live reloading when making changes in a dev environment without have to rebuild and restart the dev server every time.

I wrote a discussion post on this with more details about my workflow and project: How do I start an Astro + Pages Function dev server if -- is being deprecated?


@jonaswre Could you elaborate on this? I'm a new programmer and am struggling to understand how to implement this solution for my Astro project.

After chasing this problem for 2 days. I realised there is a way to integrate miniflare without using wrangles pages dev at all.

the wrangler package provides a function called getPlatformProxy that will give you access to everything.

The svelte adapter handles it in the following manner.

https://github.com/sveltejs/kit/blob/bbab296f6fcc05af6b999182798bcdedabbaa4c9/packages/adapter-cloudflare/index.js#L130

jonaswre commented 2 months ago

@AVGVSTVS96 seems like astro has the same concept like sveltekit. But from the doc it seems like its not enabled by default.

https://docs.astro.build/en/guides/integrations-guide/cloudflare/#platformproxy

I've added the following script to my package.json

    "preview": "npm run build && wrangler pages dev .svelte-kit/cloudflare",

This is a way to have a look at something that closely resembles how its running in prod.

This should help

newsve commented 2 months ago

@jonaswre Thanks for looking into it!

I realised there is a way to integrate miniflare without using wrangles pages dev at all. the wrangler package provides a function called getPlatformProxy that will give you access to everything The svelte adapter handles it in the following manner.

Please check again my mini example which indeed uses the cloudflare-adapter in https://github.com/cloudflare/workers-sdk/issues/5315#issuecomment-2012389845

But while using the cloudflare-adpater it still doesn't exposes anything in dev mode.

"preview": "npm run build && wrangler pages dev .svelte-kit/cloudflare",

This is a way to have a look at something that closely resembles how its running in prod.

npm run preview is amazing but devs need dev mode for a close feedback loop with all the things which preview lacks: rebuild on file change, live reload and/or HMR in the browser after rebuild and all in few ms. preview doesn't give us any of that.

How do you want to develop against all the services such as D1, KV, DO, etc.? Using native binds doesn't work in dev mode and preview is way too clunky to do anything more than a hello world or do you want to manually rebuiild, wait and refresh your browser on any little change? So you go back to use REST calls with API tokens in env vars just like before. But then why do we need wrangler in the first place (except for managing the cloud resources in a dashboard manner once or twice a month) ?

jonaswre commented 2 months ago

Thats the Point. With the Platform Proxy you should just be able to use the normal dev server from your framework. No need for wrangler dev.

In sveltekit I can just use "vite dev" as usual the platform proxy injects everything it should.

Have you enabled the platform proxy as seen in the like I provided?

If you need further help please provide a repo where I can reproduce your issue. That makes it way easier to see all the things you have configured.

One more thing to notice. You need to use .dev.vars instead/in combination with your .env file. Miniflare will use envs from .dev.vars instead of .env

And make sure to access the runtime through your context found in the following example.

https://docs.astro.build/en/guides/integrations-guide/cloudflare/#usage.

KyGuy2002 commented 2 months ago

I am having the same problem. I understand the reasoning behind wanting to switch away from this, but if a viable alternative is not available then removal is not an option.

Reading the blog and other issues, it looks like CF is working with the full stack frameworks to make it integrate nicely with them, but for plain Vite or any other unsupported tools, there is no way to do this anymore.

I think this should be added back with a disclaimer about the potential issues, but still allow its use for projects that rely on it.

jonaswre commented 2 months ago

@KyGuy2002

There is.

The wrangler npm package exposes a getPlatformProxy function.

You can use it in the same way that the svelte or astro adapter use it.

If you look at the source code of those adapters, you will see they don't use any specific tools for the integration that you won't be able to use in "plain" vite.

Basic concept is:

let DB = env.db

If(env == Dev) { platform = getPlatformProxy() DB = platform.env.db }

DB.query(...)

The framework adapters inject this, in the way they think platform specifics should be handled.

For svelte in all functions that could interact with the platform you have access to a event object. That event object has a platform property which always contain the bindings from wrangler. In Prod/Preview they contain the real deal. In dev they referenced the local services.

I think the development workflow is meant to be the following:

  1. Dev your features using your hmr development server ( access cloudflare using the getPlatformProxy)
  2. Build
  3. Verify your result using a better representation of the cloudflare infrastructure using "wrangler dev "
  4. Deploy to Preview
  5. Deploy to Production
KyGuy2002 commented 2 months ago

@jonaswre

Thanks for responding.

I'm not sure im understanding what you mean. I am using the vanilla pages functions in the functions directory, and then my react site from src. Don't I need vite dev to serve the react app, and wrangler proxy those requests and handle the functions ones?

Thanks.

AVGVSTVS96 commented 2 months ago

@jonaswre If I understand correctly, I should be able to enable platformProxy for cloudflare adapter in astro config file and cloudflare should inject bindings into my dev server? Does bindings include ability to access my cloudflare function, or is this just for database stuff?

I tried this and have not been successful, no access to function in dev mode.

Do I also have to setup something else to access the cloudflare runtime? Or should this just work?

Here's a link to the branch in my repo where I tried this: https://github.com/AVGVSTVS96/astroSite/tree/cloudflareProxy

newsve commented 2 months ago
  1. Dev your features using your hmr development server ( access cloudflare using the getPlatformProxy)

@jonaswre would you mind to make a small poc repo? Even C3 (npm create cloudflare@latest) with SvelteKit as app doesn't expose anything nor does it run wrangler dev along vite. Or just take @AVGVSTVS96 repo and add the missing parts.

Without being able to access the native, og CF bindings in a hot-reloading dev environment, how should we develop against killer products such as CF KV?

jonaswre commented 2 months ago

https://github.com/jonaswre/super-math-5233

I've created a small example that does something useless with KV.

Steps do reproduce:

  1. npm create cloudflare@latest
  2. chose svelte
  3. wrangler kv:namespace create wrangler_demo
  4. add config to wrangler.toml
  5. Add types to app.d.ts
  6. Use KV in hooks
  7. npm run dev

But I guess the "magic" is how to access the KV. Don't use process.env but event.platform.env.

src/hooks.server.ts

import type { Handle } from "@sveltejs/kit";

export const handle: Handle = async ({ event, resolve }) => {
    if(event.platform !== undefined) {
        const key = "SET_ME";
        const last_value =  await event.platform.env.wrangler_demo.get(key);
        const new_value = (last_value ?? '')+1;
        await event.platform.env.wrangler_demo.put(key, new_value);
        event.locals.current = new_value;

    }
    return await resolve(event);
}

src/app.d.ts

// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
    namespace App {
        interface Locals {
            current: string | null;
        }
        interface Platform {
            env: {
                wrangler_demo: KVNamespace;
            };
            cf: CfProperties
            ctx: ExecutionContext
        }
    }
}

export {};
AVGVSTVS96 commented 2 months ago

@jonaswre Is this not possible in my static Astro project?

@jonaswre If I understand correctly, I should be able to enable platformProxy for cloudflare adapter in astro config file and cloudflare should inject bindings into my dev server? Does bindings include ability to access my cloudflare function, or is this just for database stuff?

I tried this and have not been successful, no access to function in dev mode.

Do I also have to setup something else to access the cloudflare runtime? Or should this just work?

Here's a link to the branch in my repo where I tried this: https://github.com/AVGVSTVS96/astroSite/tree/cloudflareProxy

jonaswre commented 2 months ago

I have no experience with astro but from the documentation you should be able to.

https://docs.astro.build/en/guides/integrations-guide/cloudflare/#cloudflare-runtime

const { env } = Astro.locals.runtime;

It seems like the folloing line injects the platformProxy the same way its handled in svelte. https://github.com/withastro/adapters/blob/38c32c9db490dbe58d813c7eed1113d4f7f8683d/packages/cloudflare/src/index.ts#L194

Regarding you functions. You might be right they won't be run using "astro dev". But you don't need them in dev mode. What I mean by that - they don't need to be "injected", you can run them in a seperate "wrangler dev process" and access them over the wire (as you would do in prod?) Im also pretty new to the wrangler dev setup, so correct me if im wrong.

a page = a worker + some other stuff. a worker = 1 main.js 1 main.js = one app

What I mean by that. If you have an astro app running on the server you wont be able to run another app (functions) on that same worker. So what you need is two workers - 1 for the astro app and one for some other functions.

I have the same setup in one of my projects.

I have one app running a frontend (sveltekit (including ssr)) but I want to be able to access my server from other services/ui as well. And with that I want to be able to replace the frontend more easily. And the frontend should have no code thats concerned with the backend processing (Queues, KV, Webhook Setups, etc).

So I have hat least 2 Workers - one for the backend and one for the frontend.

In my dev setup i've used hono.dev for the backend. The Hono.dev server is just started using "wrangler dev" which will refect changes quickly (the next request, which is enough for a backend service - no need for any hot reload) and when working on the frontend i start both the backend worker and the frontend worker in seperate processes. The frontend worker connects to the backend worker using the api over http.

This setup is pretty complex but it's what I decided to do to be able to seperate business logic and frontend further. My main reasoning for this is that I have some async processing going on that should be done seperate.

But usally i would just do it all in sveltekit/astro. Which makes everything alot easier - no need to maintain an api, start diffrent workers, etc.

One last adadum, if want your services to share the same database/store you can persit you data locally in that case two local workers can also share the same data.

image

rschristian commented 2 months ago

You can use it in the same way that the svelte or astro adapter use it.

If you look at the source code of those adapters, you will see they don't use any specific tools for the integration that you won't be able to use in "plain" vite.

If this is going to be the recommended path forward, the community really needs a spec implementation rather than piecing together what some frameworks have thrown together (not to mention that has no guarantee of being correct).

jonaswre commented 2 months ago

If this is going to be the recommended path forward, the community really needs a spec implementation rather than piecing together what some frameworks have thrown together (not to mention that has no guarantee of being correct).

I think it would be best for the deprecation warning to guide people to the following page: https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy and then add a section that lets people know that if they use a framework they should check the adapters documentations. If you have some very custom setup you need to put some thought into your dev mode anyway.

I think the information is all there they should just point people to the new solution. Just telling people it will no longer work creates threads like this.

And if there are a lot of people, just using vite without any other framework, they could come together an implement a vite plugin that handles it for vite.

As I said not much magic going on in the adapters.

Astro

const platformProxy = await getPlatformProxy({
        configPath: args.platformProxy.configPath ?? 'wrangler.toml',
        experimentalJsonConfig: args.platformProxy.experimentalJsonConfig ?? false,
        persist: args.platformProxy.persist ?? true,
    });

    const clientLocalsSymbol = Symbol.for('astro.locals');

    server.middlewares.use(async function middleware(req, res, next) {
        Reflect.set(req, clientLocalsSymbol, {
            runtime: {
                env: platformProxy.env,
                cf: platformProxy.cf,
                caches: platformProxy.caches,
                ctx: {
                    waitUntil: (promise: Promise<any>) => platformProxy.ctx.waitUntil(promise),
                    passThroughOnException: () => platformProxy.ctx.passThroughOnException(),
                },
            },
        });
        next();
    });
  1. call platform proxy
  2. inject in the way they handle platform specifics.

Sveltekit

const proxy = await getPlatformProxy(options.platformProxy);
    const platform = /** @type {App.Platform} */ ({
        env: proxy.env,
        context: proxy.ctx,
        caches: proxy.caches,
        cf: proxy.cf
    });

    /** @type {Record<string, any>} */
    const env = {};
    const prerender_platform = /** @type {App.Platform} */ (/** @type {unknown} */ ({ env }));

    for (const key in proxy.env) {
        Object.defineProperty(env, key, {
            get: () => {
                throw new Error(`Cannot access platform.env.${key} in a prerenderable route`);
            }
        });
    }

    return {
        platform: ({ prerender }) => {
            return prerender ? prerender_platform : platform;
        }
    };
  1. call platform proxy
  2. inject in the way they handle platform specifics.

Remix

 let { getPlatformProxy } = await importWrangler();
      // Do not include `dispose` in Cloudflare context
      let { dispose, ...cloudflare } = await getPlatformProxy<Env, Cf>(options);
      let context = { cloudflare };
      return () => {
        if (!viteDevServer.config.server.middlewareMode) {
          viteDevServer.middlewares.use(async (nodeReq, nodeRes, next) => {
            try {
              let build = (await viteDevServer.ssrLoadModule(
                serverBuildId
              )) as ServerBuild;

              let handler = createRequestHandler(build, "development");
              let req = fromNodeRequest(nodeReq);
              let loadContext = getLoadContext
                ? await getLoadContext({ request: req, context })
                : context;
              let res = await handler(req, loadContext);
              await toNodeRequest(res, nodeRes);
            } catch (error) {
              next(error);
            }
          });
        }
      };
  1. call platform proxy
  2. inject in the way they handle platform specifics.

While looking through the implementation it would probably be very easy to change the remix adapter to a standalone vite adapter.

https://github.com/remix-run/remix/blob/b3f75da44749d6d69dd613668fd346b0e6802d9c/packages/remix-dev/vite/cloudflare-proxy-plugin.ts#L34

rschristian commented 2 months ago

if they use a framework they should check the adapters documentations. If you have some very custom setup you need to put some thought into your dev mode anyway.

There's a huge gap between not using a framework and "very custom setup" -- a plain Vite/Webpack/Parcel app is not a "very custom setup".

Pointing at what frameworks have cobbled together, again, is not sufficient. A spec (frameworkless, vanilla) implementation is the bare minimum needed to have a healthy migration, else, people will stick to old versions.

Can't expect people to rummage around in other projects in lieu of first-party practical examples and docs.

jonaswre commented 2 months ago

You are right having to look for the solution is annoying. That's why I suggested to add a reference to the docs.

But they already did a pretty nice job of hiding the details.

export let kv = process.env.kv1;
If (process.env.DEV == true) {
   const {platform} = getPlatformProxy();
   kv = platform.env.kv1
}

This is pretty great to emulate the whole cloudflare infrastructure.

You haven't seen bad dx if you didn't try to develop web services in azure.

In open source land there is always the option to develop a vite/webpack/parcel plugin.

The reason why I think plain JavaScript is custom because most people who develop web apps usually pick framework or separate server and client code. And in the regular client spa you can't access cloudflare services directly anyways.

rschristian commented 1 month ago

You are right having to look for the solution is annoying. That's why I suggested to add a reference to the docs.

I, and I'd imagine all others, have read the docs. While the getPlatformProxy API is indeed loosely documented, emulating the full CloudFlare suite is an exercise entirely left up to the reader. This is not good.

This is pretty great to emulate the whole cloudflare infrastructure. ... In open source land there is always the option to develop a vite/webpack/parcel plugin.

Neat, where's the docs showing off integrations with the full suite? How to manage the dichotomy of a dev mode that is entirely built off of custom, third-party middlewares while in prod we have rigid file system-based routing requirements and a marginally different API?

We need examples showing how you integrate this with workers/page functions, how you use D1, R2, etc. None of those examples make it clear how we're supposed to do that.

And in the regular client spa you can't access cloudflare services directly anyways.

I think you're missing the forest for the trees here. You can access these services via a worker/page function/etc., which in dev the proxy is a rather good option for, hence why we're here.