sveltejs / kit

web development, streamlined

Home Page:https://kit.svelte.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Platform context fallbacks

hmnd opened this issue · comments

commented

Describe the problem

  • When developing with svelte-kit dev, the event.platform object is always empty, with no great way to mock it.
  • When building using an adapter like Cloudflare Workers, event.platform is empty for prerendering.

Describe the proposed solution

Ability to provide a platform object in svelte.config.js that is

  • Substituted for event.platform when event.platform is undefined.
  • Merged with event.platform, useful for specifying optional/default platform values.
  • Both?

Alternatives considered

Perhaps allowing a 'transform' function that accepts an event may be better, in case platform needs to change based on inputs?

Importance

would make my life easier

Additional Information

No response

relatated #2304

ideally adapters come with their own dev setup where needed, eg miniflare for cf

Yeah, this is one facet of a much larger topic that also includes #3535. The platform shouldn't be provided by svelte.config.js, it should be provided by adapters (which could expose options for controlling how the adapter constructs the default). But when we get to the specifics (e.g. exposing KV bindings and DO namespaces during development with the Cloudflare adapter) it doesn't actually get us very far by itself.

ideally adapters come with their own dev setup where needed, eg miniflare for cf

what do you think if developer specify the contents of platform?

for example I need a database for my project. for Deno I can set database in platform object that can use Mongodb driver from deno.land like below:

// adapter.deno.js

// PRODUCTION mode 
// we have access to Deno,WebSocket,crypto.... in deno

const platform = {
  db: {
    get(id) {...},
    insert(data) {...}
    update(id, data) {...},
    remove(id) {...}
  }
}

export default platform;

then I can import and use this file in server.js

and for dev-mode I can simulate this functionality in handle (hooks.js) using in-memory or filesystem based database.

// hooks.js

export async function handle ({event, resolve }) {
  if(!platform.db) { // DEV mode
    platform.db = {
      get(id) {...},
      insert(data) {...},
      update(id, data) {...},
      remove(id) {...}
    }
  }
  ....

}

this way our sveltekit project is not dependent to cloudflare/deno and we can always move from one provider to another because the developer is the creator of this abstraction.

// adapter.cloudflare.js

const platform = {
  db: {/*TODO: same abstraction using cloudflare's KV or DO.*/}
}

export default platform;

Ability to provide a platform object in svelte.config.js

yes, this way we can manage platform object from svelte.config.js instead of hooks.js and we can provide different implementations for different adapters

My proposal would be, having adapters optionally provide an adapt_dev function similar to adapt where adapter author provides a mocked platform before request is sent for render. Setting up mocked platform should be adapter specific though.

I have managed fix this issue when using adapter-cloudflare-workers. it is a great workaround that served me well so far.

in hooks I use:

//src/hooks/index.ts
export const interceptPlatform: Handle = async ({ event, resolve }) => {
  event.platform = await cloudflareAdapterPlatform(event.platform)
  return resolve(event)
}
...

export const handle: Handle = sequence(interceptPlatform, ...)

Every request is intercepted by following code:

//src/cloudflareAdapterPlatform.ts

import { dev } from '$app/env';
import type { CFENV } from '../../app';

let context:ExecutionContext
const exposeCFGlobals = (globalObjects:object,ctx:ExecutionContext)=>{
    Object.entries(globalObjects).forEach(([key,val])=>{
         global[key]=val;
    })
    context = ctx;
}
const fn = (ctx:ExecutionContext) => {
        exposeCFGlobals({crypto},ctx)
        return;
}
export default async (_platform:App.Platform) => {
        if(!dev){
            return _platform;
        }
        if(_platform){
            return _platform;
        }
        
        console.log("!!!!!INITIALIZED!!!!!")
        const dotenv = await import("dotenv");
        const esbuild = await import("esbuild")
        const path = await import("path")
        const toml = await import("toml")
        const fs = await import("fs");
        const sourcefile = path.join(process.cwd(),"/durable_objects/src/objects.ts")
        const tsconfpath = path.join(process.cwd(),"/tsconfig.json")
        const wranglerPath = path.join(process.cwd(),"/wrangler.toml")
        const sourceCode = fs.readFileSync(sourcefile).toString('utf8')
        const tsconfigRaw = fs.readFileSync(tsconfpath).toString('utf8')
        const wranglerConfigRaw = fs.readFileSync(wranglerPath).toString('utf8')
        const wranglerConfig = toml.parse(wranglerConfigRaw)
        const bindings = wranglerConfig?.vars ??{}
        const durableObjects = (wranglerConfig?.durable_objects?.bindings ?? []).reduce((p,{name,class_name})=>{
            p[name] = class_name;
            return  p;
        },{})
        
        const {code } =  esbuild.transformSync(
            `
            const fn =  ${fn.toString()};
            export default {
                fetch: async (request, env2, ctx2) => {
                    fn(ctx2);
                    return new Response("Hello Miniflare!");
                }
            };
            ${sourceCode}
            `
            ,{
            loader: 'ts',
            sourcefile,
            tsconfigRaw,
            sourcemap:"inline"
        });
        const {parsed} =  dotenv.config()
        const miniflare = await (await import("miniflare")).Miniflare;
        const mf = new miniflare({
            modules:true,
            durableObjectsPersist:true,
            wranglerConfigPath:false,
            envPath:path.join(process.cwd(),"/.env"),
            script: code,
            durableObjects,
            bindings,
            globals:{exposeCFGlobals}
            })
            await mf.dispatchFetch("https://host.tld")
            const env = {...parsed, ...bindings} as unknown as CFENV;
            for await (const [k,_] of Object.entries(durableObjects)){
                env[k] = await mf.getDurableObjectNamespace(k) as DurableObjectNamespace;
            }  
            
        
        const platform:App.Platform = {env,context}
        return platform;
        
    }

In brief cloudflareAdapterPlatform.ts:

  • checks if it is called in dev mode if not exits without any action
  • does platform specific logic.
    • spin up miniflare.
    • load Durable Objects
    • expose runtime specific modules back to node such as Crypto
    • load environment variables defined in wrangler.toml
    • etc..
  • attach mocked platform to event

This logic should be part of adapter and every adapter should fulfil its platform specific requirements. I understand that there is a desire for keeping codebase free from adapter specific logic. However, I don't see this happening when endpoint events expose adapter specific platform.

Hi @denizkenan, your looks great! Would you by any chance have an public repo with this implementation available?
I am trying to figure out how you are piecing everything together but I can't figure it out.

Hi @denizkenan, your looks great! Would you by any chance have an public repo with this implementation available? I am trying to figure out how you are piecing everything together but I can't figure it out.

It's not perfect but here's what I've been using for my base repo:
https://github.com/tyvdh/test-kit

Hi @denizkenan, your looks great! Would you by any chance have an public repo with this implementation available? I am trying to figure out how you are piecing everything together but I can't figure it out.

It's not perfect but here's what I've been using for my base repo: https://github.com/tyvdh/test-kit

Ah nice, I see that you have wrapped it as boilerplate(also added cache).

👍🏼
yes would be amazing to have, when I played with SolidStart couple weeks ago I was amazed that dev already uses miniflare under the hood which makes it much simpler to develop code depending on R2 etc