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

Route-level deployment config #8383

Closed
Rich-Harris opened this issue Jan 7, 2023 · 2 comments · Fixed by #8740
Closed

Route-level deployment config #8383

Rich-Harris opened this issue Jan 7, 2023 · 2 comments · Fixed by #8740
Labels
adapters - general Support for functionality general to all adapters feature request New feature or request
Milestone

Comments

@Rich-Harris
Copy link
Member

Describe the problem

We've talked about this obliquely in #7716 and #7845 and elsewhere, but haven't really fleshed it out.

#8310 articulates the idea well: it's often the case that some parts of your app are suitable for edge runtimes, while others require Node modules; some things make sense to deploy to the edge, other things belong physically close to your database. At present, deployment configuration is per-app — if you're using adapter-vercel (for example) you can use edge functions or lambdas, but you can't use both in one app.

Describe the proposed solution

In #7716 I proposed this:

// src/routes/+page.server.js
export const config = {
  region: 'edge'
};

export function load() {...}

Here, config is whatever shape makes sense for the adapter you're using. After the app is built, SvelteKit would read the config from each entry point (by actually importing the module, not by static analysis which would prevent the use of things like environment variables or re-exports from $lib/config or whatever) and associate it with the route. This config would then be accessible to the adapter, which would use it to configure the deployment. (Note that reading the config happens in a separate process, which means it must be serializable using devalue or whatever.)

Layout config

As with other page options, export const config from a +layout.js or +layout.server.js would apply to all child pages unless it was overridden.

Overriding could take one of two forms — merging or replacing. In other words this...

// src/routes/+layout.js
export const config = {
  runtime: 'edge',
  region: 'edge'
};
// src/routes/whatever/+page.js
export const config = {
  region: 'iad1'
};

...could result in this for the /whatever route (where 'runtime' is undefined and therefore whatever the default value is)...

// replaced
{
  region: 'iad1'
}

...or this:

// merged
{
  runtime: 'edge',
  region: 'iad1'
}

I suspect merging would be more helpful in practice.

One thing to note is that in the export const prerender case, API routes do not inherit from layouts. For consistency, we'd probably want to do the same for export const config. In the case described in #8310, where everything in /api needs a Node runtime, this might be unhelpful. Possible solutions:

  • API routes do inherit from layouts (might feel a bit weird)
  • Default can be specified in adapter options (this would apply to all API routes, but could be overridden for all pages in the root layout)
  • Something other than export const config altogether (see 'Alternatives considered')

Types

We could add a new App.RouteConfig interface. This could be declared inside the adapter's types, and would be automatically picked up.

/** @type {App.RouteConfig} */
export const config = {...};

In the glorious future where we somehow type exports automatically (either microsoft/TypeScript#38511 or a TypeScript plugin, even this would be unnecessary for type-safe configuration.

Dev

One important consideration is how this interacts with #3535. The tl;dr is that we'd like to actively prevent you from using Node APIs/modules like Buffer or node:fs in a route that will be deployed to the edge; conversely it would be cool if you could use platform-specific stuff that wouldn't normally be available in Node in applicable functions.

Simply running the dev server inside a simulation of the production runtime (well, not simply — that would probably be quite hard) doesn't solve the problem, because it's quite possible to use node:fs in a prerendered page in an otherwise edge-deployed app.

Two basic approaches spring to mind: SvelteKit itself is somehow aware of the different environments, and config.runtime is a common property with a restricted set of values, or each adapter needs to somehow augment the dev server environment with additional APIs and modules and provide a hook for detecting the use of forbidden APIs and modules. I suspect the adapter approach is preferable (it allows adapter config such as edge: true to be taken into account, and is probably more future-proof), and it gives us a bit more flexibility in designing this feature since we don't need to solve both things at once, but it would be good to have some idea of what the implementation would look like so that we don't accidentally paint ourselves into a corner.

Conventions

While the shape of App.RouteConfig would be adapter-specific, it's likely that certain conventions would emerge. For example the Vercel and Netlify adapters could both use runtime to configure edge vs lambda, and possibly group to configure splitting behaviour if someone wanted manual control. I'm not sure if Netlify allows you to configure multiple regions per deployment, but if so both could also use a region property.

Other adapters would doubtless have overlapping uses. While the shape of the config wouldn't be enforced anywhere, it would be nice if adapters organically adopted the same conventions.

Alternatives considered

The alternative to route config is to do everything inside the adapter, perhaps by having a function that took a route ID and returned the relevant config.

In general I like this less than the declarative approach, but it might be nicer in certain circumstances, like switching behaviour based on whether a route is a page or an API route as in the example above.

Importance

would make my life easier

Additional Information

No response

@dummdidumm dummdidumm added adapters - general Support for functionality general to all adapters feature request New feature or request labels Jan 7, 2023
@madeleineostoja
Copy link

madeleineostoja commented Jan 7, 2023

This sounds like a good solution, including for the case I raised in #8310. Re: api route inheritance, I think having a default config in the adapter that can be overridden in a particular route/tree makes sense, since global config will be the majority use-case. I agree having api routes inherit this config from routes but not others would lead to confusion. In the case I outlined you can then just leave the default and opt-in to the edge in the root layout, which api routes won’t inherit. Feels right to my brain.

I definitely think merging config objects in overrides makes a lot more sense, especially since there’s no way to access the parent object to merge stuff yourself, otherwise you’ll have a lot of duplication to keep track of.

And finally the only reason I kind of like the alternative of keeping everything in adapter config is an irrational one — adapters don’t really feel like part of Sveltekit ‘core’, but something you can swap and change at any time for different environments, so it feels cleaner to have their config seperate rather than littered in your app code. Again though in practice this has no real practical downside, just the vibe of the thing

@Smirow
Copy link

Smirow commented Jan 19, 2023

Coming from #8458. I really like the group idea to handle the splitting behaviour, it can be a nice way to prevent some cold starts for node runtime and really fits the original idea that only a part of the app is suitable for the edge runtime, and neither the edge or node routes needs to be split.

Maybe outside the scope of this issue, but we may need a way to properly handle 404 or even whatever is handled in hooks when we don't match a route, like redirects, rate-limiter, or logging for instance when splitting functions. I've been using a fallback function that have the same behavior as the render function when splitting is disabled that gets hit when the request don't match a route, but maybe there is a nicer way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
adapters - general Support for functionality general to all adapters feature request New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants