Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Alternate Vite Environment API design #18415

bluwy started this conversation in Ideas
Oct 21, 2024 · 6 comments · 31 replies
Discussion options

Exploration of a different API design for Vite's environment API and different tradeoffs. In typical fashion, I submit ideas usually too late 😅 So I don't expect that we're integrating or be considering any of these soon.

Goals

  • Decrease API surface layer compared to current Environment API
  • Additional environment configuration are additive
  • Allow better typings for runtime/plugin authors

API design

Vite config

Click to expand

Most Vite users are spending most of the time interacting with the Vite config. It should be additive, approachable, and simple to configure the options.

Vite stays browser-focused, with all top-level options reflecting for browsers first. Some options may be re-used for other environments (like SSR), and some may not make sense for them, which is fine. The environment specific options should be kept under the environment key.

Here's an example without environment API yet (as an exercise to clean up the API to make way for environment API later):

// vite.config.ts
export default {
 build: {
 modulePreload: false,
 outDir: 'dist/client',
 // ssr* options here are all deprecated, moved to `ssr.build.*`
 },
 ssr: {
 // Environment-specific options
 external: [/^node:/],
 noExternal: true,
 // Partial options from top-level options. Not all top-level options
 // can be configured here unless they make sense for SSR. SSR-specific
 // options can also extend the top-level options, like `build.emitAssets`.
 resolve: {},
 build: {
 outDir: 'dist/server',
 emitAssets: false,
 },
 },
}

Vite config with environment API

Click to expand
// vite.config.ts
import { node } from 'vite/environments'
import { workerd } from 'vite-environment-workerd'
export default {
 // Add additional environments to Vite, usually set by plugins.
 // `client` and `ssr` keys are not allowed. Functions are discouraged
 // from accepting parameters, they should receive the config from the
 // top level `rsc` or `workerd` config below. Unless for other reasons
 // like init-only options or fallback config values.
 environments: {
 rsc: node(),
 workerd: workerd(),
 },
 ssr: {
 // ...
 },
 // These environments can be configured here, the types are explained
 // in the next section.
 rsc: {},
 workerd: {
 // workerd-specific options
 polyfillNode: true,
 // Environment-specific options
 external: [/^node:/],
 noExternal: true,
 // Partial options from top-level options. Similarly as explained above.
 // However the environment instance can decide what to allow users to configure.
 optimizeDeps: { include: ['@workerd/workerd'] },
 },
}

Vite config & environment API types

Click to expand

Environment API libraries (from runtime authors) can return a config type that consumers can use. Consumers like framework or plugin authors can add something like this:

import type { NodeEnvironmentOptions } from 'vite/environments'
import type { WorkerdEnvironmentOptions } from 'vite-environment-workerd'
declare module 'vite' {
 interface ViteEnvironments {
 rsc: {
 type: 'runnable'
 config: NodeEnvironmentOptions
 resolvedConfig: ResolvedNodeEnvironmentOptions
 }
 worker: {
 type: 'fetchable'
 config: WorkerdEnvironmentOptions
 resolvedConfig: ResolvedWorkerdEnvironmentOptions
 }
 }
}
// 1. Vite's `UserConfig` will auto-augmented with `rsc: NodeEnvironmentOptions` option.
// (similar to resolved config)
// 2. `this.environment.rsc` will have stricter types defined here.
// workerd options types could look something like this
interface WorkerdEnvironmentOptions
 extends Omit<EnvironmentOptions, 'consumer' | 'webCompatible'> {
 polyfillNode: boolean
}
interface EnvironmentOptions {
 external?: string[]
 noExternal?: string[] | boolean
 resolve?: ResolvedResolveOptions
 consumer?: 'client' | 'server'
 webCompatible?: boolean
 build?: BaseEnvironmentBuildOptions
 // other allowed configuration
}

The benefit with this approach is that framework or plugin authors can define what they expect to exist and define it themselves. They also get stricter types ootb, for example:

  1. They don't have to type-guard whether an environment is runnable or fetchable.
  2. They are free to extend or limit certain options from being edited (for framework configs).
  3. Environment specific options, e.g. worker polyfillNode, can also be defined in the config.

Create an environment (runtime authors)

Click to expand

An environment instance looks like a plugin with certain properties that act like hooks.

NOTE: I'm not very familiar with the environment requirements, so I might be missing something. As such, the below is also a rough sketch and some hooks may be redundant.

interface Environment<Config = Record<string, any>, ResolvedConfig = Config> {
 name: string
 // Similar to plugin hooks, environments can track when the server/build started and
 // ends. This is useful for caching or cleanups. Not all plugin hooks are inherited.
 buildStart: () => void
 buildEnd: () => void
 // Custom hook to resolve its own config, e.g. like Vite, adding default values, making properties
 // non-optional etc. By default it'll return `config` like a no-op. The resolved config can
 // be access from e.g. `ViteResolvedConfig['workerd']` or `this.environment.workerd.config`.
 configResolve: (
 config: Config,
 viteConfig: ResolvedViteConfig
 ) => ResolvedConfig
 configureServer: (server: ViteDevServer) => void
 configureEnvironment: () => BaseEnvironmentOptions // or `configureBehaviour`?
 fetchModule: (node: EnvironmentModuleNode) => FetchResult
}
// `WorkerdEnvironmentOptions` defined in code example above
function workerd(): Environment<WorkerdEnvironmentOptions> {
 let resolvedConfig: WorkerdEnvironmentOptions
 let miniflare: any
 return {
 name: 'workerd',
 configResolve(config: WorkerdEnvironmentConfig) {
 const resolved = {
 ...config,
 polyfillNode: config.polyfillNode ?? true,
 }
 resolvedConfig = resolved
 return resolved
 },
 configureEnvironment() {
 miniflare = new Miniflare({})
 return {
 external: resolvedConfig.external,
 noExternal: resolvedConfig.noExternal,
 resolve: resolvedConfig.resolve,
 build: resolvedConfig.build,
 }
 },
 fetchModule(node) {
 return miniflare.fetch(node.url)
 },
 }
}

Using en environment (framework & plugin authors)

Click to expand

As showed in Vite config with environment API section, additional environments can be configured by framework or plugin authors by adding to the environments key. If an existing one exists during config merging, it'll error.

In most cases, framework can access via server.environments.ssr.import('...') like before.

For plugin authors, I quite like the current this.environment.<env-name> design. The discussions around per-environment plugin should also be unaffected by this proposal. The only change with this proposal is that configEnvironment is likely not needed anymore as environment instances have the configResolve hook.

Summary

  1. Stricter types. Only allow specific environment options to be configured so there's less "generalized options" that we have to introduce, and less commitment to make all work across environments.
  2. Environment-specific options are tucked away one-level deep and can be access when needed. If Vite eventually wants to make a pattern first class, the environment key will already be in place similar to ssr and worker options.
  3. There should be less abstraction that authors need to understand. If there's common patterns that are required to interact with the API, we can export utility functions.

Caveats or uncovered topics

  1. An obvious pitfall is that extended environments occupy top-level config keys. We could namespace them, but I think it defeats the purpose and ease of use.

    I personally don't think it's a big issue as we don't add new top-level options often. We can mark this not covered by semver, but still check the ecosystem if there's certain keys in use that're popular if we need to add a new option.

  2. Compatibility with Vitest. Since I worked on frameworks more, my bias here probably won't fit well with how Vitest will integrate. I'm open to extending certain parts of the API to make it work better with Vitest.

    However, I think we should consider frameworks as our main target audience given the ecosystem, and if Vitest need to apply specific hacks or workaround, I think it should be acceptable.

You must be logged in to vote

Replies: 6 comments 31 replies

Comment options

Thanks for the writing down the proposal @bluwy! It would be great for others to check things out, maybe even try to implement some of this in a POC as I made deeps mental cliffs now with the other design and it is a bit hard to get out of them and check it as a blank slate. I think there are some things that we seems to agree on here that is good to enumerate again as this is the bulk of what we are introducing in Vite 6 as experimental:

  • The flexibility to create as many environments as needed for a single Vite dev server
  • The possibility to run the code in a different process through ModuleRunner
  • Each environment should be represented as an instance that controls its own module graph and plugins container
  • Plugins can access this instance through this.environment

So to guide others, this proposal affects how to configure the environments and an alternative for API for environment instances.

You must be logged in to vote
0 replies
Comment options

About:

An obvious pitfall is that extended environments occupy top-level config keys. We could namespace them, but I think it defeats the purpose and ease of use.

We evaluated this idea and decided that having the environments key to scope new environments was a requirement, mainly because of this pitfall. Making every new top level key a breaking change doesn't sound like a good idea in the long term. This is something that we can discuss independent of the whole proposal (maybe taking bits of the whole idea) and applying them first could be a good way to see where we end up before we need to try to rework the whole thing.

One of the ideas we checked out was using top level keys with a namespace for environments. For example, Nuxt uses top level $environment keys to do environment overrides. If environments.server is really difficult to grok (and I understand that the extra level makes it a hard sell), we could move to:

export default defineConfig({
 $client: { ... },
 $ssr: { ... },
 $edge: { ... },
})

One of the ideas for environments was that the API feels more consistent. You have config.environments.edge and server.environments.edge. If we do this change, maybe we could still have this consistency and have config.$edge/server.$edge but keep server.environments as a way to iterate them.

Note that this could also apply to your proposal. Given a good way to redefine how the current ssr key works.

You must be logged in to vote
9 replies
Comment options

bluwy Oct 21, 2024
Maintainer Author

I don't really have an opinion if we have to change them as a list or not. Accessing the list of environments is not common I think, usually framework or plugin authors already know what environment they want to work on, so as long as we keep or improve the DX of accessing environments (rather than iterating them), I'm fine with it.

Comment options

I think the $ convention makes it easier to work with environment configs; I never liked the deep structure we have right now (root->environments->name->resolve->alias).

I don't have a strong opinion on whether we should switch .environments to an array if we go with a $ convention. Personally, I don't see a lot of benefit in that. Objects can be iterated.

Comment options

Making every new top level key a breaking change doesn't sound like a good idea in the long term.

I agree with this. Also I think having environments in top-level is kind a confusing.

export default defineConfig({
 server: {},
 build: {},
 ssr: {},
 edge: {}
})

This is requires the users to understand which key is a not an environment to know what option can be set (you can use your IDE's help but there's a mental overhead IHMO).

Additionally, I think the types are difficult to write.

interface UserConfig {
 [key: string]: { resolve?: { alias?: Record<string, string> } }
 root?: string
}

This declaration errors. We need to keep UserConfig as an interface for vitest, so I don't know if we can solve this.

I like the $environment pattern. It feels cleaner than environments.environment. You can understand that the key is an environment easily and the types can be written easily. The following declaration does not error.

interface UserConfig {
 [key: `$${string}`]: { resolve?: { alias?: Record<string, string> } }
 root?: string
}
Comment options

bluwy Oct 22, 2024
Maintainer Author

This is requires the users to understand which key is a not an environment to know what option can be set (you can use your IDE's help but there's a mental overhead IHMO).

Good point. I think I'm persuaded more to use $ to denote an environment now.

Additionally, I think the types are difficult to write.

I don't really expect us to type out the environments ourselves. I'd expect plugin or framework authors to know that they're working on a specific environment to extend the types manually like the Vitest example (but probably different like showed in the proposal's Vite config & environment API types section).

Comment options

The proposal from this thread is implemented here

Comment options

About having the top level keys refer to the client environment. We first implemented a similar design. I can't find where we did the switch now in the sea of commits.

I think it is confusing that some top level options are client only, and some of them affect all environments acting as a default. We currently also have a mix of these though, and people are fine with it. If we think that we should have certain top level options configure the client environment only (a good case may be dev.optimizeDeps), we can check this out (again, independent of the rest of the proposal).

We would need to document for each config option if it is client-only or a default for all environments. And this would be orthogonal to the option being shared or per-environment. We also would need to treat these client-only options in a different way during config resolution. This may be good to explore in a PR maybe.

That being said, this is possible with the current design, it is just another concept users needs to be aware of. If we end up going with something like $client, I would argue that using top level for defaults consistently is better.

You must be logged in to vote
10 replies
Comment options

Let's see a concrete case. You introduced the warmup feature and went with this API:

server: { warmup: { clientFiles: [], ssrFiles: [] }}

You would like to change this to (let's discuss only the defaults vs mixed here):

dev: { warmup: { files: [] }}, // client-only
$ssr: { dev: { warmup: { files: [] }},

I agree this looks good. I actually did this at first for all config options. Every environment specific option on the top level was client only. We had to revert this and go with the current design because resolve, build, and almost every top level key is shared. So this was a breaking change. So, one part of our disagreement is that I'm putting more value on consistency. It is hard to accept for me that we want to have mixed defaults and client-only top level keys when we would only want to make client-only 2-3 of all the config options in Vite (that are a lot). If we would be talking about half the keys, I would understand, but this is a tiny fraction of them.

About not having two ways of setting things for the client. For every top level key that act as a default (most of them), if a plugin or user wants to be sure that it only affects the client, being able to use $client is a great feature that we currently don't have (or we have during build if they use !isSsrBuild). If we don't offer this, then every time they need to set something for the client only, they will need to set the default value and then force a different value for all other environments. It may not even be possible to do it right. For users, they don't know all the environments. For plugins, they may not know at that point. And even if you know, what value do you use? Should you set it to the default? Do we need something like the 'unset' CSS value?

As a note: Every time we decide that a shared top level key will now be able to be configured per-environment (for example, resolve.alias), with the current design, we can just move it to EnvironmentOptions and we don't break anything. It was already working like if it was a default for all environments. The only diff is that now you can override it per environment. This also works if we have mixed, but we would need to move to it as a default and not client-only.

Comment options

I think I'm lost in the discussion. Is your opinion "the currrent behavior of deriving (mostly) all the values of the root options to all the environment is not good from a gradual adoption point of view"? In that case, would you expand on that part? Is it the point that when migrating to CSR+SSR from CSR only, that users need to move all options under $client?

Comment options

As a summary of what I understand from this thread, in case it is useful. Bjorn's proposal:

  • client is configured at top level (there is no $client, you can only set its config through top level)
  • there is a special ssr top key.
  • you configure the other environments through $env.
  • for each non-client environment, some props from top level are taken as default, some are not.

Current design (if we use the $ prefix):

  • defaults are configured at top level
  • the special ssr top key is going to be removed
  • you configure environments using $env (and there is a $client env by default)
  • for all environments, all top level props are taken as default
Comment options

bluwy Oct 22, 2024
Maintainer Author

Yeah @patak-dev is spot on with the summary 👍

For every top level key that act as a default (most of them), if a plugin or user wants to be sure that it only affects the client, being able to use $client is a great feature that we currently don't have

I agree with this. I feel like there's a tradeoff between "DX for users who only edit vite.config.js" and "DX for plugin/framework authors" between both our discussions. I don't think there's a right decision here and it depends on who we favor more, which right now is the latter.

I don't really have a solid idea to support my proposal with it at the moment, other than tossing back, do people really need to set a config for the client only? Given how far we've already come without that? 😄

Comment options

@bluwy shared a set of props that will be a footgun if shared between client and server:

  • resolve.conditions
  • dev.optimizeDeps.include, dev.optimizeDeps.exclude (maybe the whole dev.optimizeDeps?)
  • dev.warmup

Even if I pushed back in this thread, if we believe that these props will become a footgun, then we may not have an option but to break the consistency. Maybe one way to see it that would still go with the current design is that we have certain exceptions (well marked in the docs) for props that don't work as default for both client and server environments
We could even still consider them as defaults, but that only apply to consumer: 'client' environments (where they would make sense in general).
Something I dislike about this is that then we have no way to set a default for all server environments for these, but it may be something we need to live with (and if server environments are generally different runtimes, node and workerd for example, then a default for these is not really needed)

Comment options

@bluwy says:

One thing I'm not sure is highlighted well in my proposal, is that environment types can control what environment-specific option is exposed. For example, perhaps workerd don't want anyone else to change config.$edge.resolve.conditions, the type can disallow configuring that, and top-level resolve.conditions isn't really a fallback anymore. The specific behaviour is each documented by the environment.

I didn't get to this yet as it was unrelated to the other bits in the proposal. I don't think an environment should stop users from configuring things like resolve.conditions. But with the current proposal, that is possible, and I think in a cleaner way:

function opinionatedEdgeEnvironment(config: PartialUserConfig) {
 return {
 ...config,
 // some options that can't be overriden
 }
}
export default defineConfig({
 $edge: opinionatedEdgeEnvironment({
 // only some options are available, typing works fine
 })
}

Not only that, it can control additional options to expose. Example 1: workerd might want users to configure config.$edge.polyfillNode: string[] | boolean and it can take part of config merging, I'm not sure the current API allows that. Example 2: we don't have a usecase for client to have build.emitAssets: false, supporting it may be confusing.

This also works fine right now

function opinionatedEdgeEnvironment(config: ExtendedUserConfig) {
 const { a, b, c, ...userConfig } = config
 return {
 ...userConfig,
 }
}

My suggestion is that environments internally can share the same base environment options for Vite to handle processing, but they can configure what to expose and apply different "resolved config" logic that doesn't necessarily need to mean "fallback to top-level options". I hope this trims down the API surface so users only configure options that matter, but I guess you could say the inconsistency at the end causes an even larger API surface. But I think there's value in this still.

Environments are already using the pattern above if I understand correctly, so they see the value at least in extending with their own options.

You must be logged in to vote
3 replies
Comment options

bluwy Oct 22, 2024
Maintainer Author

I didn't get to this yet as it was unrelated to the other bits in the proposal. I don't think an environment should stop users from configuring things like resolve.conditions. But with the current proposal, that is possible, and I think in a cleaner way:

Wouldn't plugins still be able to change the values in config.$edge? (By "current proposal" I assume the current implementation with the $env idea)

This also works fine right now

They don't take part in config merging, or allow plugins to set them though.

Comment options

Ah, yes, this is true. In your proposal, you'll have the same issue if plugins are the ones adding the environments instead of the user. Or do you plan to restrict that environments need to be defined upfront? In the current implementation, because there is a well defined set of per-environment options, the user can still do overrides if needed (similar to how a user can set ssr.xxx values now). Plugins can also know and modify this common set of values.

About extending the environment config. I see the value in plugins being able to change them. Right now, they would be able to do it using configureServer. Maybe we could modify configEnvironment(name, options) so options also include extra options added by the environment? This hook is called once we know the environment type. I don't know if this is possible though.
Another issue I see is that this is promoting polluting the environment options object, and we may end up breaking frameworks and libraries lately. So maybe these options should be in another object or under a custom key.

It would be good to have a concrete use case for a plugin needed to change an option from an environment. One of the guiding principles of the current design is to push for fine grained options so plugins can be environment agnostic. This goes against this idea.

Comment options

bluwy Oct 22, 2024
Maintainer Author

Or do you plan to restrict that environments need to be defined upfront?

They don't need to be defined upfront. The config.environments: {} and config.$env can be separate and could be validated to match after resolved.

Another issue I see is that this is promoting polluting the environment options object, and we may end up breaking frameworks and libraries lately. So maybe these options should be in another object or under a custom key.

Yeah it might be better to encourage as a separate object instead. The idea of the proposal is that the environment libraries define what the config shape should look like, so they can construct any option shape they like. Extending the environment options would be a risk if they want to do so.

It would be good to have a concrete use case for a plugin needed to change an option from an environment.

I guess I don't have a real-world usecase, but was thinking that environment libraries can take part and extend the Vite config (e.g. through config.$edge) and fit nicely with how the entire Vite config generally works. They don't have to all be the same set of environment options.

Comment options

They don't have to type-guard whether an environment is runnable or fetchable.

What if it's both? The proposal is to have dispatchFetch even on the ssr environment alongside the import method. Maybe it's better to expose an actual Environment type there instead of using a string? This will also make it possible to see any custom methods on server.$env. In both cases, the ViteEnvironments type should also actually be a ViteDevEnvironments type.

declare module 'vite' {
 interface ViteDevEnvironments {
 rsc: {
 type: RunnableDevEnvironment
 config: NodeEnvironmentOptions
 resolvedConfig: ResolvedNodeEnvironmentOptions
 }
 worker: {
 type: FetchableDevEnvironment
 config: WorkerdEnvironmentOptions
 resolvedConfig: ResolvedWorkerdEnvironmentOptions
 }
 }
}

Another thing is this.environment - how do you type it? It's available in both dev and build, so you still need a type guard. I am also not sure if frameworks actually access environments directly on the server. Wouldn't you use some kind of generic adapter that doesn't care about this?

Compatibility with Vitest

I don't think this should be a topic of discussion around the Environment API. Vitest can adapt to any APIs as long as they provide module graphs.

In most cases, framework can access via server.environments.ssr.import('...') like before.

With the $env proposal, can you access server.$ssr/server.$client environments?


Generally speaking, I like the goals that were set in this proposal. I like separating the environment options into a top-level property. I also like moving the environment creating specific properties (like createEnvironment) into a separate object that regular users won't touch in normal circumstances.

Functions are discouraged from accepting parameters, they should receive the config from top level rsc or workerd config below.

This doesn't make a lot of sense to me. If plugin authors populate the environments option, why would they even use any functions for that? Wouldn't they set options directly in the config hook of the plugin?

export default {
 plugins: [miniflare()] // sets `create` in the `config` hook
}

An environment instance looks like a plugin with certain properties that act like hooks

The "environments" plugin-like API feels excessive to me, personally. It just duplicates plugin hooks. Vite already has plugins, and I am satisfied with how the environment is created right now. Moving around properties might be required for better TS support, but that's it. Proposed life-cycle separation seems harder to work with than a single create function. We explored a lot of different APIs starting in February, and I think the current iteration satisfies every constraint that we faced during that time (maybe we should've documented every change to the API).

You must be logged in to vote
3 replies
Comment options

bluwy Oct 22, 2024
Maintainer Author

What if it's both? The proposal is to have dispatchFetch even on the ssr environment alongside the import method. Maybe it's better to expose an actual Environment type there instead of using a string?

If it's both it could use a | union perhaps, but I feel that this isn't common in practice. Exposing an Environment type is fine by me too. The string is mostly a placeholder and we could extend it out.

In both cases, the ViteEnvironments type should also actually be a ViteDevEnvironments type.

I don't think we need to narrow that down though. Isn't the config or resolvedConfig not part of dev? The types could possibly extend for more than only dev.

Another thing is this.environment - how do you type it?

Good point. We still need a type guard, but perhaps combining with the ambient types above, they can typeguard to a more stricter type?

I am also not sure if frameworks actually access environments directly on the server. Wouldn't you use some kind of generic adapter that doesn't care about this?

This is something I've wondered about and I feel like the push for plugins to check "this is a general server environment" rather than "this is an ssr-node environment" isn't going to work out at the end. There will be a lot of permutations of general server environments, not only between environment configurations but also different framework's intention of "what this environment is used for", that eventually it'll have to lead to hardcoding a specific environment.

AFAIU we're adding properties like consumer and webCompatible to define these characteristics, but I don't know if it'll scale well.

With the $env proposal, can you access server.$ssr/server.$client environments?

I guess you can! That's mostly @patak-dev's idea though which I like

This doesn't make a lot of sense to me. If plugin authors populate the environments option, why would they even use any functions for that? Wouldn't they set options directly in the config hook of the plugin?

The "environments" plugin-like API feels excessive to me

I think these two are related for my answer. I don't think my proposed environment "plugin-API-ish" design is perfect, but I want it to reflect close to the plugin API so it feels similar. Right now it feels like a completely different API.

  1. The class-based design, I'm not sure if fits well with the other Vite APIs now. We have classes like ModuleGraph exposed but we never have an API with new Something() (CMIIW).
  2. The API is plugging multiple abstractions into one. In DevEnvironmentContext, hot, remoteRunner.transport, and depsOptimizer all looks like abstractions rather than config options. I'm also not sure splitting an environment as DevEnvironment and BuildEnvironment is ideal too, it feels (at least on surface) more abstractions.

My idea is that there's no longer createEnvironment, so you'd create environment instances like Vite plugins, which helps users to familiarise with it, and add them to config.environments: { ... } like plugins. I'm sure there's good reasons why the design is the way it is today and I'd love to hear more certain exploration/challenges you had. If someone asks me why environments look so different and tricky to set up, I'd like to be able to give a good explanation 😅

Comment options

I am also not sure if frameworks actually access environments directly on the server. Wouldn't you use some kind of generic adapter that doesn't care about this?

This is something I've wondered about and I feel like the push for plugins to check "this is a general server environment" rather than "this is an ssr-node environment" isn't going to work out at the end. There will be a lot of permutations of general server environments, not only between environment configurations but also different framework's intention of "what this environment is used for", that eventually it'll have to lead to hardcoding a specific environment.

AFAIU we're adding properties like consumer and webCompatible to define these characteristics, but I don't know if it'll scale well.

If the framework wants to allow node or workerd environment, they will declare it like:

declare module 'vite' {
 interface ViteEnvironments {
 worker: {
 type: 'fetchable'
 config: NodeEnvironmentOptions | WorkerdEnvironmentOptions
 resolvedConfig: ResolvedNodeEnvironmentOptions | ResolvedWorkerdEnvironmentOptions
 }
 }
}

Is this what you have in mind?

Comment options

bluwy Oct 23, 2024
Maintainer Author

Yup, and my replying comment at #18415 (reply in thread) elaborates a bit more on it. Where this declaration can happen is entirely up to the consumer, e.g. the framework/library author. Runtime provider authors don't do this as they don't know the env names ahead of time, but they'll export the types primitive to construct this. (Maybe in a less verbose way than what we have in the proposal now)

Comment options

First of all, I think an idea is invaluable, no matter when it is submitted, so thanks for writing!


Vite config & environment API types

I guess it's difficult to type workerd property in the code example of Vite config with environment API.
Because which runtime the environment will use is chosen by the end-user, workerd property type needs to be inferred from the environments.workerd.

Vite config & environment API types

I think the declare module 'vite' in the code example of Vite config & environment API types cannot be done for the same reason. That ambient module declaration will limit worker environment to be an environment made from workerd environment factory, but that would make users not possible to swap that with environments made from other environment factories (e.g. fastly edge, vercel edge).

Environment specific options, e.g. worker polyfillNode, can also be defined in the config.

In the current env API, there isn't a way to set an option of environments from frameworks (or other plugins). This is a known issue for plugins as well. This is something I think it'd nice to be solved.

You must be logged in to vote
6 replies
Comment options

I think frameworks/plugins guarding them before accessing it should be fine. Similarly in the current implementation for server.environments.* we don't make it nullable to make it easier to use.

Do you mean the type of config.workerd will be something like Record<string, any> & EnvironmentOptions? (= polyfillNode won't be suggested.)

export default defineConfig({
 environments: { /* */ },
 workerd: {
 polyfillNode: true,
 },
})

Maybe this is related to #18415 (reply in thread)

Comment options

bluwy Oct 23, 2024
Maintainer Author

Not exactly I think, here's an example config I'm thinking about:

// vite.config.ts
import { defineConfig } from 'vite'
import framework from 'framework'
// Import the adapter, and the ambient types for `config.$cloudflare` would be augmented
// so you get typings for fields like `config.$cloudflare.polyfillNode`. If the framework is structured
// differently where this import doesn't happen, the end user could add /// <reference types="..." />
// in this config instead
import cloudflareAdapter from 'framework-adapter-cloudflare'
export default defineConfig({
 plugins: [
 framework({ adapter: cloudflareAdapter() })
 ],
 $cloudflare: {
 polyfillNode: true
 }
})
Comment options

I see. If the framework accepts both vercel and cloudflare, would it be like

export default defineConfig({
 plugins: [
 framework({ adapter: cloudflareAdapter() })
 ],
 $cloudflare: {
 polyfillNode: true
 }
})
// ---
export default defineConfig({
 plugins: [
 framework({ adapter: vercelAdapter() })
 ],
 $vercel: {
 polyfillNode: true
 }
})

or

export default defineConfig({
 plugins: [
 framework({ adapter: cloudflareAdapter() })
 ],
 $edge: {
 polyfillNode: true
 }
})
// ---
export default defineConfig({
 plugins: [
 framework({ adapter: vercelAdapter() })
 ],
 $edge: {
 polyfillNode: true
 }
})

? The latter one is similar to the current design. I'm not sure what the pros and cons is for the former one.

(削除) For the latter one, on the framework side, I guess they will have to do something like below and it's a bit tedious. (削除ここまで)

const framework = ({ adapter }) => {
 return {
 configResolved(config) {
 if (adapter.name === 'cloudflare') {
 const config = (config.$edge as CloudflareOptions)
 // do something
 } else if (adapter.name === 'vercel') {
 const config = (config.$edge as VercelOptions)
 // do something
 }
 }
 }
}

Edit: on second thought, I think this happens with the current design as well, so ignore it.

Comment options

I think I understand your idea now.

Comment options

bluwy Oct 24, 2024
Maintainer Author

👍 Yeah I think either patterns work and is up to how the framework wants to structure it. If there's only ever one adapter, it might make sense to share the same key, but regardless you'd still get fine-grained types depending on the augmented types provided by the adapter library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /