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

docs: environment API proposal #16089

Merged
merged 45 commits into from Apr 19, 2024
Merged

Conversation

patak-dev
Copy link
Member

@patak-dev patak-dev commented Mar 4, 2024

Description

Important

The Environment API work has been consolidated into #16471

Vite Environment API proposal docs.

For the proposal implementation, see:

Deployed docs for this PR

This API was discussed between @sheremet-va, @antfu, and me, and validated with Pooya Parsa, Ryan Carniato, Evan You, Daniel Roe, and Dominik G (we'll ping them once this PR is ready to be reviewed). Feedback from all of them was positive, and they think this would unlock a lot of use cases in Nitro and the frameworks they work on. It was later on discussed and refined with other members of the Vite team and maintainers in the ecosystem.

This proposal merges the Vite Runtime API and the separate module graph proposal into a new unified API. Discussions were triggered by the experimentation done by sapphi-red, hi-ogawa, the Remix and Hydrogen teams, and the feedback received from the Cloudflare team.

Related PRs:

There is still a missing section about the new separate module graphs API, that I'll add later.

vite-environments

Copy link

stackblitz bot commented Mar 4, 2024

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@patak-dev patak-dev changed the title docs: Vite Environment API docs: Vite Environment API proposal Mar 4, 2024
@patak-dev patak-dev changed the title docs: Vite Environment API proposal docs: Environment API proposal Mar 4, 2024
@patak-dev patak-dev changed the title docs: Environment API proposal docs: Vite Environment guide Mar 4, 2024
@patak-dev patak-dev changed the title docs: Vite Environment guide docs: revised Runtime API Mar 4, 2024
@patak-dev patak-dev added feat: runtime Vite Runtime API feat: dev dev server feat: hmr p3-significant High priority enhancement (priority) labels Mar 4, 2024
Copy link
Member

@sapphi-red sapphi-red left a comment

Choose a reason for hiding this comment

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

I am still trying to figure out the whole picture, but I have some questions.

Comment on lines 264 to 266
### Accessing the current environment in hooks

The `environment` name is passed as a parameter to the `options` of `resolveId`, `load`, and `transform`.
Copy link
Member

Choose a reason for hiding this comment

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

If we want to conditionally return a virtual file for worker-like runtimes such as Cloudflare workerd and Fastly Compute, is there a way to do it at once?

I guess it is required to write it like:

const p = {
  name: 'virtual',
  load(id, opts) {
    // it requires to write all the environment names
    if (opts.environment === 'workerd' || opts.environment === 'fastly-compute') {
      return 'a code for worker-like runtimes'
    }
    return 'a code'
  }
}

But to migrate from config.ssr.target === 'webworker', I think it'd nice to write it something like:

const p = {
  name: 'virtual',
  load(id, opts) {
    const e = server.environment(environment)
    if (e.type === 'worker') {
      return 'a code for worker-like runtimes'
    }
    return 'a code'
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a great point. We discussed two weeks ago a bit about this with @sheremet-va. In Vite core plugins, we have several conditions that depend on the ssr flag and other that depend on ssr.target: node | webworker as you explain (other ssr config options are already accounted for in the current state of the proposal).

If we want to remove the ssr flag, we need something like you propose here. I think we should move away from conditionals based on the environment name to conditionals based on what the environment supports. So, totally agree with you. I don't think it should be called type as it is too generic, but we need to identify what are the conditions not covered by resolve.conditions et al and add them as part of the environment config.

Maybe something like:

type runtimeType = 'browser' | 'node' | 'worker' | 'edge' | ?

The problem with this is that there will be differences between edge environments and node environments. So it may be worth checking all conditionals in core and other projects and see if we can be even more fine-grained. Maybe it is a more generic option we should add to resolve for example.

If we end up using only fine-grained options in core, I still think that having a runtimeType is a good idea, even if it is only to help with debugging.

Copy link
Member

Choose a reason for hiding this comment

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

Can you show an example when this is useful? I think having fine-grained config should be enough.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was thinking on the runtimeType also as a way for community plugins to be able to ship features before we add the proper options in Vite core. If we are missing a fine-grained config, it takes time for it to end up in a release. So it may be useful to know the runtime type to do some conditionals until they can switch to fine-grained options. I'm good avoiding this until we have a good use case to justify it.

Copy link
Member

Choose a reason for hiding this comment

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

I assumed that we have many ssr.target conditions in our codebase, but it turns out those can be rewrote with a fine-grained option. Yeah, it seems we don't need something like this.
I was thinking of returning a virtual module conditionally, but I noticed that it can be achieved by using an exports field with wintercg condition.

* Trigger the execution of a module using the associated module runner
* in the target runtime.
*/
run: ModuleRunFunction
Copy link
Member

Choose a reason for hiding this comment

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

What is the type of ModuleRunFunction? Is it (...args: any) => any or () => void?

Copy link
Member Author

Choose a reason for hiding this comment

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

So far, the proposal assumed it to be (url: string) => void. But given your questions, and a discussion between @sheremet-va and @bluwy yesterday, I think it may be better for it to be (url: string, options?: any) => any and let each environment define the options and return type so code like you proposed here will work:

// the input and the output depends on the environment
const response = await server.environment('workerd').run(nodeReqToNativeReq(req))
const html = getResponseContent(response)

Copy link
Member

Choose a reason for hiding this comment

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

So far, the proposal assumed it to be (url: string) => void.

The proposal was (url, req?) => void

Copy link
Member Author

Choose a reason for hiding this comment

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

I forgot we wanted to allow for passing the request too. I think it could go inside options for environments that would need that, no?

Copy link
Member

Choose a reason for hiding this comment

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

Options are too environment-specific, we need a general interface that frameworks can just use without knowing what environment it is. I would expect most environments to throw an error if request is not passed

Copy link
Member

Choose a reason for hiding this comment

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

we need a general interface that frameworks can just use without knowing what environment it is.

To achieve this, I guess we need to specify the return type. For example, limiting to a Response object.

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 think we can limit the return type to be honest. I would make it unknown so you have to check the return, but you can always call it with same parameters at least.

Copy link
Collaborator

@hi-ogawa hi-ogawa Mar 7, 2024

Choose a reason for hiding this comment

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

I'm a bit confused about this too. It looks like this is the only interface for "environment implementer" (e.g. cloudflare workerd) to provide a unique feature to "environment users" (i.e. framework authors).

I feel this interface is somewhat analogical to what @sapphi-red's suggested to use plugin.api in
https://github.com/sapphi-red/vite-envs/blob/7f76892b7d28f0da06826f43953cedb5b2f042c5/packages/example-framework/src/index.ts#L16-L18C19

So, how about Vite to only define this interface with no constraint like this?

interface ModuleExecutionEnvironment {
  api?: unkonwn
  ...
}
some hypotetical code

Then, environment implementer side might provide:

// in vite-environment-workerd package
export interface WorkerdEnvironmentApi {
  dispatchFetch(req: Request): Promise<Response>
}

and framework author side would use it in a similar way as sapphi-red's plugin.api (or also Cloudflare team's POC, which is likely inspired by sapphi-red's api https://github.com/dario-piotrowicz/vite-runtime-5.1-experimentations/blob/2fa5ecead1fc60b30a4952f8bd7fdf4bf22aba4f/examples/example-framework/vite.config.js#L9-L15)

import type { WorkerdEnvironmentApi } from "vite-environment-workerd"

const frameworkPlugin = {
  configureServer(server) {
    const workerdEnv = viteServer.getEnvironment("workerd");
    const workerdApi = workerdEnv.api as WorkerdEnvironmentApi;
    return () => {
      server.middlewares.use(async (req, res) => {
        const webRes = await workerdApi.dispatchFetch(toWebRequest(req));
        fromWebRequest(webRes, res)
      }
    }
  }
}

Ah, I totally ignored run(url: stirng) part, but from the yesterday's discussion, I feel there's always serialization issue and we cannot assume anything universally for all environments.

Also, for Workerd use case, entry file is probably assumed to be a part of environment initialization https://github.com/dario-piotrowicz/vite-runtime-5.1-experimentations/blob/2fa5ecead1fc60b30a4952f8bd7fdf4bf22aba4f/examples/example-framework/vite.config.js#L53, so run(url: string) might not make sense either.

Copy link
Member Author

Choose a reason for hiding this comment

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

For reference, we are also discussing with @IgorMinar in this thread about run: #16089 (comment). Commented about the serialization part there.

Would you expand on the environment initialization issue?

I think how run (or something like your api) proposal will pan out is the biggest unknown to be defined right now in the proposal.

docs/guide/api-vite-environment.md Outdated Show resolved Hide resolved
export default {
builder: {
runBuildTasks: asnyc (builder, buildTasks) => {
return Promise.all(buildTasks.map( task => task.run() ))
Copy link
Member

Choose a reason for hiding this comment

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

Are we going to reuse plugins? I guess that would have the same problem with #11218. (In dev, it won't be a big problem because buildStart hook will only be called once)
We can load the config for each build task, but that will make it difficult to share state among build tasks.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right now, the implementation resolves the config for each build generating a separate pipeline. I did it this way so we can parallelize without issues given the current state of plugins in the ecosystem.

@antfu proposed that we should take the opportunity and try to align with dev, and have a single plugins pipeline during build so it is easier to share information during build time too. We're going to push everyone to use something like this.environment.cache if they need to store things while processing (maybe we need a this.environment.api for comms?), if the plugin isn't environment agnostic. In dev, this isn't a stretch as people are already used to do the same with the ssr flag (probably there are a lot of bugs due to improper isolation because things aren't as clear as having a proper environment abstraction). During build, we could do the same and ask everyone to be aware of the environment that is being built. Running the plugins in parallel, even for workers (that we may rethink as environments too) becomes possible.

I'm worried about this idea though. I think it is an interesting goal to aim for, but a lot of plugins will be broken for vite build --all if we do this. I was proposing that we have a builder.isolatedPlugins flag that we make opt-in for a major or two and let users set it if all their plugins work well in this new world.

Copy link
Member Author

Choose a reason for hiding this comment

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

About buildStart, we actually would like to go the other way around and call buildStart during dev once per environment and make plugins use that to reset the state of each environment. This could be very interesting if at one point we would like to implement a "restart this environment" if a config changes for example without the need for a full server restart.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a way for plugin to know the start or end of certain environment build?
For example, currently it doesn't look like environment is accessible from buildStart, buildEnd, writeBundle etc...

Copy link
Member Author

Choose a reason for hiding this comment

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

We need to implement that, this.environment should be available for all hooks. So far we only injected it in resolveId, load, and transform as it was done before.
I wonder if Rollup could give us a better way to inject the environment so we don't need to wrap every hook, that feels quite inefficient

docs/guide/api-vite-environment.md Outdated Show resolved Hide resolved
docs/guide/api-vite-environment.md Outdated Show resolved Hide resolved
docs/guide/api-vite-environment.md Outdated Show resolved Hide resolved
docs/guide/api-vite-environment.md Outdated Show resolved Hide resolved
Comment on lines +463 to +470
While the `config` hook is running, the complete list of environments isn't yet known and the environments can be affected by both the default values from the root level environment config or explicitely through the `config.environments` record.
Plugins should set default values using the `config` hook. To configure each environment, they can use the new `configEnvironment` hook. This hook is called for each environment with its partially resolved config including resolution of final defaults.

```ts
configEnvironment(name: string, config: EnvironmentConfig) {
if (name === 'rsc') {
config.resolve.conditions = // ...
```
Copy link
Member

Choose a reason for hiding this comment

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

Is it impossible to use configResolved hook?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think we should promote modifying the config in configResolved. When this hook is called, it should be safe to assume that the config is settled and plugins can read it and cache/store what they need. If we want to do this config in another hook, it should be in the config hook. And we could do that without a problem. The only reason why the configEnvironment hook exists is to help plugins deal with the complexity of environment overrides. I'm ok removing it if we think it is redundant, but at this point I feel that it will be very helpful to avoid subtle bugs in plugins.

Copy link
Collaborator

@hi-ogawa hi-ogawa Mar 29, 2024

Choose a reason for hiding this comment

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

Is this configEnvironment only for custom environments or do you also want to move people to configure name = "client" and name = "ssr" cases here as well? For example,

sample code
// from this
config(config, env) {
  return {
    optimizeDeps: { ... },
    ssr: { optimizeDeps: { ...} }
  }
}

// to this?
configEnvironment(name, config) {
  if (name === "client") {
    config.dev.optimizeDeps = { ... }
  }
  if (name === "ssr") {
    config.dev.optimizeDeps = { ... }
  }
}

// or this?
config(config, env) {
  config.environments.client.dev.optimizeDeps = { ... }
  config.environments.ssr.dev.optimizeDeps = { ... }
}

// or this?
config(config, env) {
  return {
    environments: {
      client: {
        dev: {
          optimizeDeps: { ... }
        }
      }
      ssr: { ... }
    }
  }
}

I thought it will be mostly redundant, but after writing this sample code, maybe configEnvironment looks explicit and nicer even for builtin ones. But maybe this assignment form of config needs to take care config mergining inside the hook itself?

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I forgot that ResolvedConfig is a readonly. 😅

At this point I feel that it will be very helpful to avoid subtle bugs in plugins.

What kind of bug do you have in mind?

Copy link
Member Author

@patak-dev patak-dev Mar 29, 2024

Choose a reason for hiding this comment

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

Is this configEnvironment only for custom environments or do you also want to move people to configure name = "client" and name = "ssr" cases here as well?

Yes, configEnvironment will be called once per environment (including default ones). It should also work like config regarding being able to modify inplace or return an object to be merged (or we have a bug, or it isn't implemented yet)

Copy link
Member Author

Choose a reason for hiding this comment

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

At this point I feel that it will be very helpful to avoid subtle bugs in plugins.

What kind of bug do you have in mind?

When you configure environments in the config hook you should first do things like: config.environments.custom ??= {}. You also have two ways to configure an environment:

  • set it as a default affecting all environments
  • set it explicitly for a given environment

If the default is already set, maybe it is better to avoid setting it explicitly? It may be a valid question. You also don't know from a plugin what are all the configured environments (because other plugins after you may add new ones), and maybe you want to do something with all of them.

In configEnvironment, all environments are already known, and defaults have already been applied. So you see a partially resolved environment options.

What I'm thinking we could suggest to plugin authors is:

  • Add environments and set defaults in config
  • Configure specific environments at configEnvironment

We renamed EnvironmentConfig / environment.config to EnvironmentOptions / environment.options here c7e4da2

Rationale: I need to reach out for the root or some other global config from the environment. We currently can do that for a DevEnvironment with environment.server.config that is bad because you need a dependency on the server to get the config that doesn't depend on it. Currently there is no way to get the global config in a BuildEnvironment. Or I think you could do it through environment.builder.config that reads well but doesn't allow for generic dev/build code. If you only need the root, it would be better to avoid going through the server or builder. We also now have environment.config be a EnvironmentConfig. This also reads well, but given that we use const { config } = server in tons of places, it is very confusing now to see config and not know if it is the global config or the environment config. I think we should change environment.config to be the global ResolvedConfig. It still makes sense, this is the config for the environment, root is shared but is also part of the environment config. This will make config easier to understand everywhere.
I still think we need a way to have the EnvironmentConfig from the environment. So I'm thinking that I will rename it to EnvironmentOptions (aligning it with BuildOptions and DevOptions). And then you can access these as environment.options

We should also rename configEnvironment to environmentOptions aligning with options from rollup.

Copy link
Collaborator

Choose a reason for hiding this comment

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

But maybe this assignment form of config needs to take care config merging inside the hook itself?

In my comment above, I was worrying about how merging works in configEnvironment, but I just realized it can return partial options and merged automatically.

    configEnvironment(name, _config, _env) {
      if (name === "ssr") {
        return {
          build: {
            rollupOptions: {
              input: {
                index: entry,
              },
            },
          },
        };
      }
    },

docs/guide/api-vite-environment.md Outdated Show resolved Hide resolved
Comment on lines +463 to +470
While the `config` hook is running, the complete list of environments isn't yet known and the environments can be affected by both the default values from the root level environment config or explicitely through the `config.environments` record.
Plugins should set default values using the `config` hook. To configure each environment, they can use the new `configEnvironment` hook. This hook is called for each environment with its partially resolved config including resolution of final defaults.

```ts
configEnvironment(name: string, config: EnvironmentConfig) {
if (name === 'rsc') {
config.resolve.conditions = // ...
```
Copy link
Member

Choose a reason for hiding this comment

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

Ah, I forgot that ResolvedConfig is a readonly. 😅

At this point I feel that it will be very helpful to avoid subtle bugs in plugins.

What kind of bug do you have in mind?

Co-authored-by: 翠 / green <green@sapphi.red>
Comment on lines +486 to +496
```ts
interface HotContext {
file: string
timestamp: number
modules: Array<ModuleNode>
read: () => string | Promise<string>
server: ViteDevServer
}
```

- `this.environment` is the module execution environment where a file update is currently being processed.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Currently it's implemented as HotContext.environment instead of this.environment.

@patak-dev patak-dev changed the title docs: revised Runtime API docs: Environment API proposal Mar 31, 2024
@patak-dev patak-dev changed the title docs: Environment API proposal docs: environment API proposal Mar 31, 2024
Co-authored-by: Clément <clemvnt@gmail.com>
@patak-dev patak-dev changed the base branch from main to v6/environment-api April 19, 2024 12:43
@patak-dev patak-dev marked this pull request as ready for review April 19, 2024 12:45
@patak-dev patak-dev merged commit 777967d into v6/environment-api Apr 19, 2024
6 checks passed
@patak-dev patak-dev deleted the docs/vite-environments-guide branch April 19, 2024 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat: dev dev server feat: hmr feat: runtime Vite Runtime API p3-significant High priority enhancement (priority)
Projects
Status: Has plan
Development

Successfully merging this pull request may close these issues.

None yet