Open DrewRidley opened 3 years ago
I can look into this feature in the coming days to see how feasible it would be to introduce it as a PR, however, if the issue requires more significant changes to the adapter I might want to wait and consult some others to learn more about the impact this feature request might have.
I have done some more research and found a few areas the adapter can be improved. Namely, the documentation needs a significant upgrade. The documentation is confusing for new users and does not explain what the toml
file should look like. Secondly, the adapter does not currently use kv-asset-handler
to its full potential and does not set any browser cache headers on the static files served. At the very least, the adapter needs to include some way for users to be able to specify their cache preferences for static files. In light of all of these changes, I have determined that its best to refactor the module to output code in the esm
format, rather than the node
format previously used. This will have numerous advantages, but the primary motivation for implementation was that it is ESM modules are a prerequisite to using Durable Objects.
Unfortunately, I am struggling with getting ESBuild to bundle the dependencies of a sample project in when using format: 'esm'
. format: node
works perfectly, but when changing the format, the dependency resolution strategy is somehow altered.
Any help or clarification would be much appreciated!
As a short update;
I managed to get the module syntax fully compiled. I am just waiting for cloudflare to merge my PR of kv-asset-handler
which introduces support for the module syntax there. The adapter also has some new configuration options so my PR will include some changes to the documentation.
Most notably however, my PR will introduce a breaking change. This will likely need to be discussed further with a larger group of the community. The reason I would like to introduce the breaking change is that eventually cloudflare plans to deprecate the old syntax, and force usage of their new modular syntax. For this reason, I think it makes sense for the modular syntax to be the default for all workers created using this adapter.
Luckily, the breaking change in question can easily be mitigated with a clear and concise message informing the users of how to adapt their project to compile, and it only requires a minor two line change to their wrangler.toml
. In all, I think this breaking change will serve to better the adapters position in providing users with another deployment option for their sveltekit sites.
I have the code ready for a PR, I just need to wait until this PR is merged first. https://github.com/cloudflare/kv-asset-handler/pull/200 So I will be following this PR closely and will submit mine here on sveltekit once it has been merged and pushed to NPM
So, few things going on here:
(@DrewRidley and I also spoke in Discord a little bit, so this won't be anything new)
Durable Object (DO) development is heavily tied to the ESM format for Workers. So this means that while DOs are in beta (they still are), the ESM format is in beta too, by extension.
The adapter should 100% be outputting the ESM variant code only – so that the developer has the freedom to opt into DOs if/when they see fit. However, SvelteKit shouldn't be doing this until DO/ESM development is out of beta. Personally, I have no idea if there are other breaking changes on the docket for either of these, but DO is still under "beta" label intentionally, reserving that right for "last-second" changes.
The kv-asset-handler
package will surely be updated once ESM/DOs are out of beta – maybe before (I don't know). I'm sure DO going into GA will be the signal that a lot of projects were waiting for before making compatibility changes, my personal projects included.
@DrewRidley Changing the platform
option for esbuild changes its resolution paths; see here. I use none
& control the resolution via hardcoded mainFields
and conditions
values.
@lukeed @DrewRidley Durable Objects are now GA. Do you have the PR?
Sorry for the delayed response! It looks like the best way to support durable objects is to have a folder of durables that is detected and injected by the adapter. I can have a PR done within the next few days. As for websockets it's probably best that we transparently pass them through in the adapter, and let the user implement them using hooks.
Any update on this @DrewRidley?
adapter-cloudflare-workers
is now ESM-based, and provides access to env
and context
(via event.platform
). Aside from inconsistency between dev and build, which is covered by #3535 and #4292, is there anything more to do here?
I'm interest in this part. A home for the DO classes and getting them compiled correctly. Seemingly need to be concatenated?
It looks like the best way to support durable objects is to have a folder of durables that is detected and injected by the adapter.
EDIT: The below DOES NOT work, as is.
Durable Object classes have to be defined as named exports on the main
module (i.e. the Worker module). Users don’t have a way to edit the main
module provided by the adapter (short of creating a custom adapter).
With Wrangler 2, this is no longer an issue.
Unlike Wrangler 1, Wrangler 2 can resolve es modules. Simply re-export default
from the output main
module like so…
// @file ./custom-worker-entry.js
export { default } from "./.cloudflare/worker.js";
export class MyDurableObject {
// …
}
…and update wrangler.toml
.
- main = "./.cloudflare/worker.js"
+ main = "./custom-worker-entry.js"
site.bucket = "./.cloudflare/public"
+ [durable_objects]
+ bindings = [{
+ name = "MY_DURABLE_OBJECT",
+ class_name = "MyDurableObject",
+ }]
If you change main
to ./custom-worker-entry.js
, then ./.cloudflare/worker.js
will no longer be created, so I don't think this will work?
export { default } from "./.cloudflare/worker.js";
It seems like we would probably need a way to tell the adapter where our DOs are defined, so that it can then inject something like
export * from '../src/my-durable-objects.js';`
(which I assume works? will confess only passing familiarity with DOs)
The main file needs to export the DO class definitions directly, and the names of the classes must align with bindings. class_name
defined in the TOML config (user responsibility)
Gotcha. So we need to be able to specify an entry point that I guess just gets concatenated to main
? (Though we'll also need to think about how this would work in dev mode post-#3535/#4292)
Doh… didn’t test my assumption. Sorry, folks.
However, I think enabling this pattern—importing a built module into a “facade” module—makes a lot of sense.
Perhaps the esbuild
output ought to be a fixed path (i.e. instead of the path defined in wrangler.toml#main
.
wrangler.toml#main
should instead point to a “facade” module (provided by the adapter) that re-exports the built module, per my previous comment.
This would enable the Durable Objects use-case, as well as wrangler init
and create-cloudflare
workflows.
I can take a crack at a PR, pending a maintainer’s approval of the described implementation. Gimme a 👍 if so.
I'd suggest having the adapter(s) expect a special bindings.[tj]s
root-level file, and if present, it's injected into the final build via kit.vite.esbuild.inject
options (whatever the key path is). Something like:
// src/bindings.ts
// or
// bindings.ts
export { Counter } from './other/counter.ts';
so that the final Worker output is something like:
// path/to/build/worker.mjs
class C$1 {
// user source from other/counter.ts
}
export { C$1 as Counter };
const worker$1 = {
// generated (current) worker code
}
export default worker$1;
What about the 1mb script limit tho? Seems like what we really need is guidance from Cloudflare on how this should work locally with wrangler2 and and Service Bindings (which is surely their preferred way forward, though that’s a guess on my part). Basically, all these options to me look like a surefire way to blow past the 1mb limit in a project of sufficient size, or any project given sufficient time + features. Maybe that’s not such a real concern but still. Since svelte is compiling everything down to a single worker script that’s going to bite someone at some point.
note: I don’t have any insight myself, and am actively working with my account manager at Cloudflare to get guidance; the “Cloudflare way” doesn’t seem well-articulated yet. (Would love to be wrong about that tho.)
Sorry but this concern is irrelevant because it's also applicable to every Worker authored. The point of SK here is to automate/produce a Worker file without the user having to write actual the Worker code from scratch. All Workers users – regardless of framework choice, if at all – will still be deploying the same end-results abiding by the same end-user limits.
Every account exec can have the script size limits raise from 1MB, if necessary, although I strongly suspect the majority of SK apps won't need to do this.
If, for whatever reason, you are running into the limit and your limit(s) can't be raised, then you should use a service binding, where Worker1 (the SK app) is bound to Worker2 (the Worker + Durable Objects) and requests can be passed off safely. Alternatively, you may skip the bindings & use Custom Domains for Workers to send fetch()
requests to Worker2, which you still author & deploy separately.
Edit: Custom Domains for Workers would be my suggested & preferred route, where api.foo.com
is the endpoint that I author & deploy separately and my SK app (powering foo.com
) talks to it if/when necessary.
I brought up the 1mb limit as a discussion point, mostly — bundling every script and 3rd-party library together into a single worker file seems philosophically opposed to where Cloudflare is headed, as evidenced by your comment showing two similar but slightly-different approaches they've released. The trade-offs are nuanced, and are totally a broader point than "how do you bind a DO to SK with wrangler?"
I was speaking up as an advocate to make this decision thoughtfully, on behalf of DX, and with architectural flexibility in mind.
Edit: Cloudflare published a blog post today that backs up with Luke's saying and points to a comment of his in February explaining the division of labor between Svelte, SvelteKit, and something like worktop or workers itself. Hope it helps someone else have an a-ha moment.
Are there any working solutions to this problem "in the wild" yet? Just ran into it today myself and it seems like such a simple problem, just export the DO classes along with the main handler, yet I have yet to see a working solution.
@tyvdh , you can deploy DOs seperately and bind to svelte-kit app. This remedies the problem a bit.
Yeah you can and I have but that places part of the code external to the main repo and breaks a unified deployment which is a bummer. It also introduces an extra complexity around development which I'm actually more interested in, running the entire development experience under a single repo and pnpm start
command.
Which Miniflare has a concept for mounting external workers so there may still be something here I'm missing around development unification while having production separation.
This would actually potentially be a preferred method as this seems to be the way Cloudflare is heading anyway. Many individual functions / services tied together via bindings during deployment. It's really just a question of maintaining a good development experience and a scaleable production environment.
fwiw I'm currently maintaining a merge
file which is responsible for bundling the durable object classes with the adapter's worker output.
Here's what mine looks like:
import fs from 'fs'
import path from 'path'
const workerFile = path.join(process.cwd(), '/worker-site/worker.mjs')
const workerCode = fs.readFileSync(workerFile).toString('utf8')
const doFile = path.join(process.cwd(), '/src/helpers/do.js')
const doCode = fs.readFileSync(doFile).toString('utf8')
fs.writeFileSync(workerFile, `
${workerCode}
${doCode}
`)
I then call vite build && node merge.js
to build my project. Works fine. If you had imports in the do.js file (I don't) you'd need to bundle that file with esbuild or rollup first vs just importing it as a raw string.
I've got a fully functioning repo here: https://github.com/tyvdh/starlite-sveltekit
Please note it's not well documented in the README but the main files are:
server.js
for setting up the dev server. Run with: pnpm start
src/lib/durable-objects/index.js
for exporting any durable objects. Will be pulled into both the dev server and the final production build.src/lib/websockets/routes.js
for configuring any websocket endpoints and their related files. Again will work in both the dev and final production server. (please note in dev you'll need to call the socket at the :3030
port vs the 3000
port)It's a tricky problem between the dev and production environments and trying not to duplicate configurations but I'm pretty happy with this while we wait for something simpler. Ultimately I think the answer likely lies somewhere between marrying the vite server and miniflare for development and allowing websocket endpoints to pass through the cloudflare worker adapter on production.
This works though and it actually works fine for both the cloudflare worker and pages adapters. For pages you'll just have to deploy the durable objects separately and then link them manually in the dashboard as there isn't currently any way to both deploy and link when uploading a page project with durable object bindings. You could easily configure an empty worker with the durable objects to deploy as part of some separate deployment command from the git commit process that pages use for deployments. I do see eventually pages getting some sort of a configuration file though where you can both attach and deploy bindings during the automated build process.
Hello guys, I was wondering if the cloudflare adapter supports this yet. If not, what are the recommended ways to define durable objects within a sveltekit project and have it deploy along with the built worker script?
Until a robust solution is implemented, I think I've managed to find a way to shim in my durable objects alongside the worker script without too much fuss using @sveltejs/adapter-cloudflare-workers. I'm using wrangler3 which includes basic module support.
/wrangler.toml (the adapter uses these as output paths)
main = "./.cloudflare/worker.js"
site.bucket = "./.cloudflare/public"
/worker.js
export { default } from './.cloudflare/worker.js';
export { MyDurableObject } from './src/mydo.ts';
Then vite build
to generate the worker and public assets, followed by wrangler deploy worker.js
to override the main
defined in the wrangler.toml and instead use the worker.js wrapper.
Hopefully this saves someone else a few hours of cussing :wink:
@Klowner but do you still have Cloudflare's CI/CD on git push? So to clarify, you are not using @sveltejs/adapter-cloudflare at all? But still using SvelteKit? Moreover, how would you get TS/typings right when using the DO class? Edit: I guesss importing the type and declaring it to platform.env.YOUR_DO, right?
@205g0 correct, I'm only using @sveltejs/adapter-cloudflare-workers, no @sveltejs/adapter-cloudflare. For CI/CD I'm just using the Github Action.
In my individual DO files I include:
type Bindings = Required<App.Platform>['env']
export class MyDurableObject implements DurableObject {
constructor (readonly state: DurableObjectState, readonly env: Bindings) { }
// ...
}
I managed to get this working with cloudflare-adapter
and a shell script:
In ./scripts/include-durable-objects.sh
:
#!/usr/bin/env bash
#
# This script replaces the default _worker.js file with a new one that includes
# durable objects exported from src/lib/durable-objects/index.ts
dir=.svelte-kit/cloudflare
./node_modules/.bin/esbuild src/lib/durable-objects/index.ts \
--bundle \
--format=esm \
--sourcemap \
--target=esnext \
--outfile=$dir/_durable-objects.js \
mv $dir/_worker.js $dir/_sveltekit_worker.js
mv $dir/_worker.js.map $dir/_sveltekit_worker.js.map
echo "\
export { default } from './_sveltekit_worker.js';
export * from './_durable-objects.js';
" > $dir/_worker.js
In ./src/lib/durable-objects/index.ts
:
export { MyDurable } from './MyDurable';
// Add more here
In package.json
:
{
"scripts": {
"dev": "wrangler pages dev .svelte-kit/cloudflare",
"build": "vite build && ./scripts/include-durable-objects.sh"
}
}
in wrangler.toml
:
build.command = "npm run build"
[durable_objects]
bindings = [
{ name = "mydurable", class_name = "MyDurable },
]
[[migrations]]
tag = "v1"
new_classes = ["MyDurable"]
It works with cloudflare-adapter-workers
too, but you have to change the dir
variable and use worker.js
instead of _worker.js
.
I've written the simple solution (or workaround) here. I hope it will be helpful to you.
https://github.com/cloudflare/cloudflare-docs/issues/13062#issuecomment-1986928294
Sorry if this is a necro, but are we saying to use DO in SvelteKit we need to deploy the worker separately and then bind through SvelteKit? There's currently no "native" way to do this within SvelteKit as such?
@jameswoodley Yes for now, unfortunally.
Is your feature request related to a problem? Please describe. Cloudflare recently introduced the ability to maintain websocket connections with workers, in addition to a stateful storage solution called 'Durable Objects'. For workers, these objects are accessible through a
env
argument passed in during thefetch
invocation. More information about durable objects and their utilization can be found here. https://developers.cloudflare.com/workers/learning/using-durable-objectsDescribe the solution you'd like In my opinion, the cloudflare worker adapter should expose a way to mutate and interact with this
env
object passed in by cloudflare during a function invocation.Describe alternatives you've considered I have not found any alternate ways of using durable objects without modifying the adapter.
How important is this feature to you? This feature is fairly important to me personally, since it would dramatically simplify my stack if my state coordination can be done in the same worker.