Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vite Environment API - Cloudflare environment support #12637

Draft
wants to merge 42 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9a8587f
Created vite-node-vm-environment playground
Aug 6, 2024
8edc12a
Created vite-workerd-environment playground
Aug 20, 2024
558f42b
Returned response from workerd environment
Aug 20, 2024
aa1679e
Added node module runner and entrypoint
Aug 22, 2024
dde5922
Initial POC for running inside module runner
Aug 23, 2024
87b7f9f
Revised manifest routes
Aug 27, 2024
e9e61e1
Revised manifest nodes
Aug 27, 2024
5889410
Used pathe for path resolution
Aug 28, 2024
0e61853
Fixed support for endpoints
Aug 28, 2024
6313516
Added links to playground example
Aug 28, 2024
04efeec
Removed commented code
Aug 28, 2024
47d0379
Used Cloudflare plugin to run in workerd environment
Aug 28, 2024
7dd8ced
Added Cloudflare platform context
Aug 28, 2024
483d137
Added cloudflare properties route to workerd playground
Aug 28, 2024
97489b2
Added KV example
Aug 28, 2024
e50f543
Added matchers example to workerd playground
Aug 28, 2024
c7484d0
Added layout and improved playground example
Aug 29, 2024
a356404
Commented out unused code and added invalidation for environment_cont…
Aug 29, 2024
63c51f3
Added cloudflare types
Aug 29, 2024
ceb2a09
Added additional types
Aug 29, 2024
8bca02f
Added kit.ssrEnvironment property to svelte config and created defaul…
Aug 29, 2024
665fb76
Used runtime_base to resolve import in virtual module
Aug 30, 2024
2ebda30
Simplified Cloudflare type imports
Aug 30, 2024
954ad1b
Merge upstream main
Aug 30, 2024
b7b36e1
Resolved typescript errors
Aug 30, 2024
d00737b
Asserted platform types in playground
Aug 30, 2024
c0a478c
Updated vite to alpha.23
Aug 30, 2024
2438651
Changed to environments option in svelte config
Aug 30, 2024
ece5364
Started adding original implementation back in
Aug 30, 2024
113418a
Fixed error and improved node playground
Aug 30, 2024
0c5c6c3
Updated config test
Aug 31, 2024
699aa8b
Export Server class to use in entrypoint
Aug 31, 2024
d087bbf
Tidied up virtual module code
Aug 31, 2024
afdbcdc
Imported default environment in vite-node-environment playground
Aug 31, 2024
b5a6c00
Reverted exporting Server
Aug 31, 2024
be9d6a0
Added comments
Sep 1, 2024
5f66708
Replaced cloudflare environment provider dependency
Sep 2, 2024
a1b19cb
Renamed vite-workerd-environment to vite-cloudflare-environment
Sep 2, 2024
71145cb
Changed playgrounds for TS to JS
Sep 2, 2024
9fa12bf
merge main
Rich-Harris Sep 20, 2024
7d011ca
Add missing return
jamesopstad Sep 20, 2024
bbe7b42
merge main
Rich-Harris Sep 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
"playwright": "^1.44.1",
"typescript-eslint": "^8.0.0"
},
"pnpm": {
"overrides": {
"vite": "6.0.0-alpha.23"
}
},
"packageManager": "pnpm@9.10.0",
"engines": {
"pnpm": "^9.0.0"
Expand Down
1 change: 1 addition & 0 deletions packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"kleur": "^4.1.5",
"magic-string": "^0.30.5",
"mrmime": "^2.0.0",
"pathe": "^1.1.2",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try really hard not to add new dependencies. Let's find a way to do this without adding one

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to use in place of node:path for compatibility with other runtimes. We're only using the resolve function though so we could probably create our own with the same behaviour.

"sade": "^1.8.1",
"set-cookie-parser": "^2.6.0",
"sirv": "^2.0.4",
Expand Down
3 changes: 3 additions & 0 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ const get_defaults = (prefix = '') => ({
publicPrefix: 'PUBLIC_',
privatePrefix: ''
},
environments: {
ssr: null
},
files: {
assets: join(prefix, 'static'),
hooks: {
Expand Down
7 changes: 7 additions & 0 deletions packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ const options = object(
privatePrefix: string('')
}),

// New environments option. The fallback for each environment could eventually be a default Node environment.
environments: object({
ssr: validate(null, (input) => {
return input;
})
}),

files: object({
assets: string('static'),
hooks: object({
Expand Down
7 changes: 7 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'svelte'; // pick up `declare module "*.svelte"`
import 'vite/client'; // pick up `declare module "*.jpg"`, etc.
import '../types/ambient.js';
import { Plugin } from 'vite';

import { CompileOptions } from 'svelte/compiler';
import {
Expand Down Expand Up @@ -407,6 +408,12 @@ export interface KitConfig {
*/
privatePrefix?: string;
};
/**
* The new `environments` option. The user provides a factory function that is then used to create the plugin.
*/
environments?: {
ssr?: (environmentName: string, options?: any) => Plugin[];
};
/**
* Where to find various files within your project.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/exports/vite/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SSR_ENVIRONMENT_NAME = '__ssr_environment__';
33 changes: 33 additions & 0 deletions packages/kit/src/exports/vite/dev/cloudflare_entrypoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// This should be exported from @sveltejs/kit so that the path isn't relative
import { Server } from '../../../runtime/server/index.js';

export default {
/**
* This fetch handler is the entrypoint for the environment.
* @param {Request & { cf: any }} request
* @param {any} env
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'd be nice to avoid the any

* @param {any} context
*/
fetch: async (request, env, context) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if fetch is the name I would use for this. It makes me think it's a per-environment implementation of the fetch API, but it's not really since the request signature differs. Maybe something like handleRequest would be better

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the standard format for a Cloudflare Worker entrypoint (https://developers.cloudflare.com/workers/get-started/guide/#3-write-code).

const environment_context = await import('__sveltekit/environment_context');
const server = new Server(environment_context.manifest);

await server.init({
env: environment_context.env
});

return server.respond(request, {
getClientAddress: () => {
if (environment_context.remote_address) return environment_context.remote_address;
throw new Error('Could not determine clientAddress');
},
// We can provide the platform properties directly as the code is executed in a workerd environment.
platform: {
env,
cf: request.cf,
context,
caches
}
});
}
};
27 changes: 27 additions & 0 deletions packages/kit/src/exports/vite/dev/default_environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createNodeDevEnvironment } from 'vite';

/**
* A default Node environment to pass to kit.environments.ssr in the Svelte config. This could eventually be used as the fallback for this option.
* @returns {(environment_name: string) => import('vite').Plugin[]}
*/
export function node() {
// @ts-ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove the ts-ignore or add a comment saying why the ts-ignore is required?

return function default_environment(environment_name) {
return [
{
name: 'vite-plugin-sveltekit-default-environment',
config: () => {
return {
environments: {
[environment_name]: {
dev: {
createEnvironment: createNodeDevEnvironment
}
}
}
};
}
}
];
};
}
87 changes: 72 additions & 15 deletions packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { URL } from 'node:url';
import { fileURLToPath, URL } from 'node:url';
import { AsyncLocalStorage } from 'node:async_hooks';
import colors from 'kleur';
import sirv from 'sirv';
import { isCSSRequest, loadEnv, buildErrorMessage } from 'vite';
import { isCSSRequest, loadEnv, buildErrorMessage, createServerModuleRunner } from 'vite';
import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js';
import { installPolyfills } from '../../../exports/node/polyfills.js';
import { coalesce_to_error } from '../../../utils/error.js';
Expand All @@ -18,16 +18,19 @@ import { compact } from '../../../utils/array.js';
import { not_found } from '../utils.js';
import { SCHEME } from '../../../utils/url.js';
import { check_feature } from '../../../utils/features.js';
import { sveltekit_environment_context } from '../module_ids.js';
import { SSR_ENVIRONMENT_NAME } from '../constants.js';

const cwd = process.cwd();

/**
* @param {import('vite').ViteDevServer} vite
* @param {import('vite').ResolvedConfig} vite_config
* @param {import('types').ValidatedConfig} svelte_config
* @param {import('types').EnvironmentContext} environment_context
* @return {Promise<Promise<() => void>>}
*/
export async function dev(vite, vite_config, svelte_config) {
export async function dev(vite, vite_config, svelte_config, environment_context) {
installPolyfills();

const async_local_storage = new AsyncLocalStorage();
Expand Down Expand Up @@ -98,10 +101,30 @@ export async function dev(vite, vite_config, svelte_config) {
return { module, module_node, url };
}

/**
* Used to invalidate the `sveltekit_environment_context` module when the manifest is updated.
*/
function invalidate_environment_context_module() {
for (const environment in vite.environments) {
const module = vite.environments[environment].moduleGraph.getModuleById(
sveltekit_environment_context
);

if (module) {
vite.environments[environment].moduleGraph.invalidateModule(module);
}
}
}

function update_manifest() {
try {
({ manifest_data } = sync.create(svelte_config));

// Update the `manifest_data` used in the `sveltekit_environment_context` virtual module.
environment_context.manifest_data = manifest_data;
// Invalidate the virtual module.
invalidate_environment_context_module();

if (manifest_error) {
manifest_error = null;
vite.ws.send({ type: 'full-reload' });
Expand Down Expand Up @@ -420,8 +443,36 @@ export async function dev(vite, vite_config, svelte_config) {
});

const env = loadEnv(vite_config.mode, svelte_config.kit.env.dir, '');
// Update the `env` used in the `sveltekit_environment_context` virtual module.
environment_context.env = env;
const emulator = await svelte_config.kit.adapter?.emulate?.();

/**
* The environment that was provided to `kit.environments.ssr` in the Svelte config.
* @type { ((import('vite').DevEnvironment & { api?: { getHandler: (opts: { entrypoint: string }) => Promise<(req: Request) => Promise<Response>> }})) | undefined }
*/
const devEnv = vite.environments[SSR_ENVIRONMENT_NAME];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know our naming scheme is unusual. sorry...

Suggested change
const devEnv = vite.environments[SSR_ENVIRONMENT_NAME];
const dev_env = vite.environments[SSR_ENVIRONMENT_NAME];


const __dirname = fileURLToPath(new URL('.', import.meta.url));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could live at the top of the file


/** @type {((req: Request) => Promise<Response>) | undefined} */
let handler;

// Create the handler for the Cloudflare or Node environment if it exists.
if (devEnv) {
if (devEnv.api) {
handler = await devEnv.api.getHandler({
entrypoint: path.join(__dirname, 'cloudflare_entrypoint.js')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's move anything cloudflare-specific to the cloudflare adapter

});
console.log('Running in Cloudflare environment');
} else {
const module_runner = createServerModuleRunner(vite.environments[SSR_ENVIRONMENT_NAME]);
const entrypoint = await module_runner.import(path.join(__dirname, 'node_entrypoint.js'));
handler = entrypoint.default.fetch;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps this path should also be called if !devEnv? I think it would simplify things to always have a handler defined and the default should probably be to use the Node handler

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's definitely the best way to go. That's what I meant by this comment:

This could eventually be used as the default environment when kit.ssrEnvironment is not provided. This would mean that a single approach could be used, based on the entrypoint and virtual module convention, rather than maintaining two separate code paths.

It requires going all in on this approach though so I need to fill in any missing functionality first.

console.log('Running in Node environment');
}
}

return () => {
const serve_static_middleware = vite.middlewares.stack.find(
(middleware) =>
Expand All @@ -433,6 +484,9 @@ export async function dev(vite, vite_config, svelte_config) {
remove_static_middlewares(vite.middlewares);

vite.middlewares.use(async (req, res) => {
// Update the `remote_address` used in the `sveltekit_environment_context` virtual module.
environment_context.remote_address = req.socket.remoteAddress;

// Vite's base middleware strips out the base path. Restore it
const original_url = req.url;
req.url = req.originalUrl;
Expand Down Expand Up @@ -522,18 +576,21 @@ export async function dev(vite, vite_config, svelte_config) {
return;
}

const rendered = await server.respond(request, {
getClientAddress: () => {
const { remoteAddress } = req.socket;
if (remoteAddress) return remoteAddress;
throw new Error('Could not determine clientAddress');
},
read: (file) => fs.readFileSync(path.join(svelte_config.kit.files.assets, file)),
before_handle: (event, config, prerender) => {
async_local_storage.enterWith({ event, config, prerender });
},
emulator
});
// Render using the environment handler if it has been created. Else, fallback to the default behaviour.
const rendered = handler
? await handler(request)
: await server.respond(request, {
getClientAddress: () => {
const { remoteAddress } = req.socket;
if (remoteAddress) return remoteAddress;
throw new Error('Could not determine clientAddress');
},
read: (file) => fs.readFileSync(path.join(svelte_config.kit.files.assets, file)),
before_handle: (event, config, prerender) => {
async_local_storage.enterWith({ event, config, prerender });
},
emulator
});

if (rendered.status === 404) {
// @ts-expect-error
Expand Down
24 changes: 24 additions & 0 deletions packages/kit/src/exports/vite/dev/node_entrypoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// This should be exported from @sveltejs/kit so that the path isn't relative
import { Server } from '../../../runtime/server/index.js';

export default {
/**
* This fetch handler is the entrypoint for the environment.
* @param {Request} request
*/
fetch: async (request) => {
const environment_context = await import('__sveltekit/environment_context');
const server = new Server(environment_context.manifest);

await server.init({
env: environment_context.env
});

return server.respond(request, {
getClientAddress: () => {
if (environment_context.remote_address) return environment_context.remote_address;
throw new Error('Could not determine clientAddress');
}
});
}
};
Loading
Loading