diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 440a14d3aea2a2..5eddfc8e042efe 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -265,6 +265,10 @@ export default defineConfig({ text: 'Migration from v4', link: '/guide/migration', }, + { + text: 'Deprecations Guide', + link: '/deprecations/', + }, ], }, { @@ -283,8 +287,8 @@ export default defineConfig({ link: '/guide/api-javascript', }, { - text: 'Vite Runtime API', - link: '/guide/api-vite-runtime', + text: 'Vite Environment API', + link: '/guide/api-vite-environment', }, { text: 'Config Reference', @@ -332,6 +336,33 @@ export default defineConfig({ ], }, ], + '/deprecations/': [ + { + text: 'Deprecations Guide', + link: '/deprecations/', + }, + { + text: 'Deprecations List', + items: [ + { + text: 'VD001 - handleHotUpdate()', + link: '/deprecations/vd001', + }, + { + text: 'VD002 - options.ssr', + link: '/deprecations/vd002', + }, + { + text: 'VD003 - Dev Server APIs', + link: '/deprecations/vd003', + }, + { + text: 'VD004 - ssrLoadModule', + link: '/deprecations/vd004', + }, + ], + }, + ], }, outline: { diff --git a/docs/blog/announcing-vite5-1.md b/docs/blog/announcing-vite5-1.md index 35ee61c172870e..5b22abe800aacd 100644 --- a/docs/blog/announcing-vite5-1.md +++ b/docs/blog/announcing-vite5-1.md @@ -56,8 +56,6 @@ The new API brings many benefits: The initial idea [was proposed by Pooya Parsa](https://github.com/nuxt/vite/pull/201) and implemented by [Anthony Fu](https://github.com/antfu) as the [vite-node](https://github.com/vitest-dev/vitest/tree/main/packages/vite-node#readme) package to [power Nuxt 3 Dev SSR](https://antfu.me/posts/dev-ssr-on-nuxt) and later also used as the base for [Vitest](https://vitest.dev). So the general idea of vite-node has been battle-tested for quite some time now. This is a new iteration of the API by [Vladimir Sheremet](https://github.com/sheremet-va), who had already re-implemented vite-node in Vitest and took the learnings to make the API even more powerful and flexible when adding it to Vite Core. The PR was one year in the makings, you can see the evolution and discussions with ecosystem maintainers [here](https://github.com/vitejs/vite/issues/12165). -Read more in the [Vite Runtime API guide](/guide/api-vite-runtime) and [give us feedback](https://github.com/vitejs/vite/discussions/15774). - ## Features ### Improved support for `.css?url` diff --git a/docs/config/build-options.md b/docs/config/build-options.md index 3713daf534b349..02df0202b8319e 100644 --- a/docs/config/build-options.md +++ b/docs/config/build-options.md @@ -191,10 +191,18 @@ When set to `true`, the build will also generate an SSR manifest for determining Produce SSR-oriented build. The value can be a string to directly specify the SSR entry, or `true`, which requires specifying the SSR entry via `rollupOptions.input`. +## build.emitAssets + +- **Type:** `boolean` +- **Default:** `false` + +During non-client builds, static assets aren't emitted as it is assumed they would be emitted as part of the client build. This option allows frameworks to force emitting them in other environments build. It is responsibility of the framework to merge the assets with a post build step. + ## build.ssrEmitAssets - **Type:** `boolean` - **Default:** `false` +- **Deprecated:** use `build.emitAssets` During the SSR build, static assets aren't emitted as it is assumed they would be emitted as part of the client build. This option allows frameworks to force emitting them in both the client and SSR build. It is responsibility of the framework to merge the assets with a post build step. diff --git a/docs/deprecations/index.md b/docs/deprecations/index.md new file mode 100644 index 00000000000000..f9c1d8025f5c8d --- /dev/null +++ b/docs/deprecations/index.md @@ -0,0 +1,5 @@ +# Deprecations Guide + +A list of guides for planned future deprecations/removals in Vite. + +// TODO: diff --git a/docs/deprecations/vd001.md b/docs/deprecations/vd001.md new file mode 100644 index 00000000000000..e1f9929a3cefe9 --- /dev/null +++ b/docs/deprecations/vd001.md @@ -0,0 +1,23 @@ +# VD001 - Plugin Hook `handleHotUpdate` + +Deprecate plugin hook `handleHotUpdate` in favor of [`hotUpdate` hook](/guide/api-vite-environment#the-hotupdate-hook) to be Environment API aware, and handle additional watch events with `create` and `delete`. + +::: tip Future Deprecation +The deprecation is plannde in the future, where you could start migrating your plugin to use the new API if you move fast. To identify your usage, set `future.deprecationWarnings.pluginHookHandleHotUpdate` to `true` in your vite config. +::: + +Affect scope: `Vite Plugin Authors` + +| Stages | Version | +| ---------------- | ---------------------------------------- | +| First Introduced | `v6.0.0` | +| Deprecation | (planned in `v7.0.0`) | +| Feature Removal | (currently no plan to remove completely) | + +## Motivation + +// TODO: + +## Migration Guide + +// TODO: diff --git a/docs/deprecations/vd002.md b/docs/deprecations/vd002.md new file mode 100644 index 00000000000000..d721cae211e89f --- /dev/null +++ b/docs/deprecations/vd002.md @@ -0,0 +1,45 @@ +# VD002 - Plugin Hook Argument `options.ssr` + +Deprecate plugin hook argument `options.ssr` in `resolveId`, `load` and `transform` in favor of the `this.environment` plugin context property. + +::: tip Future Deprecation +The deprecation is plannde in the future, where you could start migrating your plugin to use the new API if you move fast. To identify your usage, set `future.deprecationWarnings.pluginHookSsrArgument` to `true` in your vite config. +::: + +Affect scope: `Vite Plugin Authors` + +| Stages | Version | +| ---------------- | ---------------------------------------- | +| First Introduced | `v6.0.0` | +| Deprecation | (planned in `v7.0.0`) | +| Feature Removal | (currently no plan to remove completely) | + +## Motivation + +// TODO: + +## Migration Guide + +For the existing plugin to do a quick migration, replace the `options.ssr` argument with `this.environment.name !== 'client'` in the `resolveId`, `load` and `transform` hooks: + +```ts +import { Plugin } from 'vite' + +export function myPlugin(): Plugin { + return { + name: 'my-plugin', + resolveId(id, importer, options) { + const isSSR = options.ssr // [!CODE --] + const isSSR = this.environment.name !== 'client' // [!CODE ++] + + if (isSSR) { + // SSR specific logic + } else { + // Client specific logic + } + }, + } +} +``` + +For a more robust long term implemtation, plugin should provide handling for [multiple environments](/guide/api-vite-environment.html#accessing-the-current-environment-in-hooks). diff --git a/docs/deprecations/vd003.md b/docs/deprecations/vd003.md new file mode 100644 index 00000000000000..54c91304face0f --- /dev/null +++ b/docs/deprecations/vd003.md @@ -0,0 +1,26 @@ +# VD003 - Dev Server APIs + +Multiple APIs from ViteDevServer related to module graph has replaced with more isolated Environment APIs. + +- `server.moduleGraph` -> [`environment.moduleGraph`](/guide/api-vite-environment#separate-module-graphs) +- `server.transformRequest` -> `environment.transformRequest` + +::: tip Future Deprecation +The deprecation is plannde in the future, where you could start migrating your plugin to use the new API if you move fast. To identify your usage, set `future.deprecationWarnings` in your vite config. +::: + +Affect scope: `Vite Plugin Authors` + +| Stages | Version | +| ---------------- | ---------------------------------------- | +| First Introduced | `v6.0.0` | +| Deprecation | (planned in `v7.0.0`) | +| Feature Removal | (currently no plan to remove completely) | + +## Motivation + +// TODO: + +## Migration Guide + +// TODO: diff --git a/docs/deprecations/vd004.md b/docs/deprecations/vd004.md new file mode 100644 index 00000000000000..5247c5fb93cfe0 --- /dev/null +++ b/docs/deprecations/vd004.md @@ -0,0 +1,23 @@ +# VD004 - `ssrLoadModule` + +`server.ssrLoadModule` has been replaced new [Module Runner](/guide/api-vite-environment#modulerunner). + +::: tip Future Deprecation +The deprecation is plannde in the future, where you could start migrating your plugin to use the new API if you move fast. To identify your usage, set `future.deprecationWarnings.ssrLoadModule` to `true` in your vite config. +::: + +Affect scope: `Vite Plugin Authors` + +| Stages | Version | +| ---------------- | ---------------------------------------- | +| First Introduced | `v6.0.0` | +| Deprecation | (planned in `v7.0.0`) | +| Feature Removal | (currently no plan to remove completely) | + +## Motivation + +// TODO: + +## Migration Guide + +// TODO: diff --git a/docs/guide/api-javascript.md b/docs/guide/api-javascript.md index eda204c2cf951d..22502a03d9f931 100644 --- a/docs/guide/api-javascript.md +++ b/docs/guide/api-javascript.md @@ -135,6 +135,7 @@ interface ViteDevServer { /** * Programmatically resolve, load and transform a URL and get the result * without going through the http request pipeline. + * @deprecated use environment.transformRequest */ transformRequest( url: string, diff --git a/docs/guide/api-vite-environment.md b/docs/guide/api-vite-environment.md new file mode 100644 index 00000000000000..775b681250d9d3 --- /dev/null +++ b/docs/guide/api-vite-environment.md @@ -0,0 +1,915 @@ +# Vite Environment API + +:::warning Low-level API +Initial work for this API was introduced in Vite 5.1 with the name "Vite Runtime API". This guide describes a revised API, renamed to Vite Environment API. This API will be released in Vite 6. You can already test it in the latest `vite@6.0.0-alpha.x` version. + +Resources: + +- [Environment API PR](https://github.com/vitejs/vite/pull/16471) where the new API is implemented and reviewed. +- [Feedback discussion](https://github.com/vitejs/vite/discussions/16358) where we are gathering feedback about the new APIs. + +Feel free to send us PRs against the `v6/environment-api` branch to fix the issues you discover. Please share with us your feedback as you test the proposal. +::: + +Vite 6 formalizes the concept of Environments, introducing new APIs to create and configure them as well as accessing options and context utilities with a consistent API. Since Vite 2, there were two implicit Environments (`client` and `ssr`). Plugin Hooks received a `ssr` boolean in the last options parameter to identify the target environment for each processed module. Several APIs expected an optional last `ssr` parameter to properly associate modules to the correct environment (for example `server.moduleGraph.getModuleByUrl(url, { ssr })`). The `ssr` environment was configured using `config.ssr` that had a partial set of the options present in the client environment. During dev, both `client` and `ssr` environment were running concurrently with a single shared plugin pipeline. During build, each build got a new resolved config instance with a new set of plugins. + +The new Environment API not only makes these two default environment explicit, but allows users to create as many named environments as needed. There is a uniform way to configure environments (using `config.environments`) and the environment options and context utilities associated to a module being processed is accessible in plugin hooks using `this.environment`. APIs that previously expected a `ssr` boolean are now scoped to the proper environment (for example `environment.moduleGraph.getModuleByUrl(url)`). During dev, all environments are run concurrently as before. During build, for backward compatibility each build gets its own resolved config instance. But plugins or users can opt-in into a shared build pipeline. + +Even if there are big changes internally, and new opt-in APIs, there are no breaking changes from Vite 5. The initial goal of Vite 6 will be to move the ecosystem to the new major as smoothly as possible, delaying promoting the adoption of new APIs in plugins until there is enough users ready to consume the new versions of these plugins. + +## Using environments in the Vite server + +A single Vite dev server can be used to interact with different module execution environments concurrently. We'll use the word environment to refer to a configured Vite processing pipeline that can resolve ids, load, and process source code and is connected to a runtime where the code is executed. The transformed source code is called a module, and the relationships between the modules processed in each environment are kept in a module graph. The code for these modules is sent to the runtimes associated with each environment to be executed. When a module is evaluated, the runtime will request its imported modules triggering the processing of a section of the module graph. In a typical Vite app, an environments will be used for the ES modules served to the client and for the server program that does SSR. An app can do SSR in a Node server, but also other JS runtimes like [Cloudflare's workerd](https://github.com/cloudflare/workerd). So we can have different types of environments on the same Vite server: browser environments, node environments, and workerd environments, to name a few. + +A Vite Module Runner allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runner implementation is decoupled from the server. This allows library and framework authors to implement their layer of communication between the Vite server and the runner. The browser communicates with its corresponding environment using the server Web Socket and through HTTP requests. The Node Module runner can directly do function calls to process modules as it is running in the same process. Other environments could run modules connecting to a JS runtime like workerd, or a Worker Thread as Vitest does. + +All these environments share Vite's HTTP server, middlewares, and Web Socket. The resolved config and plugins pipeline are also shared, but plugins can use `apply` so its hooks are only called for certain environments. The environment can also be accessed inside hooks for fine-grained control. + +![Vite Environments](../images/vite-environments.svg) + +A Vite dev server exposes two environments by default: a `client` environment and an `ssr` environment. The client environment is a browser environment by default, and the module runner is implemented by importing the virtual module `/@vite/client` to client apps. The SSR environment runs in the same Node runtime as the Vite server by default and allows application servers to be used to render requests during dev with full HMR support. We'll discuss later how frameworks and users can change the environment types for the default client and SSR environments, or register new environments (for example to have a separate module graph for [RSC](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components)). + +The available environments can be accessed using `server.environments`: + +```js +const environment = server.environments.client + +environment.transformRequest(url) + +console.log(server.environments.ssr.moduleGraph) +``` + +Most of the time, the current `environment` instance will be available as part of the context of the code being run so the need to access them through `server.environments` should be rare. For example, inside plugin hooks the environment is exposed as part of the `PluginContext`, so it can be accessed using `this.environment`. + +A dev environment is an instance of the `DevEnvironment` class: + +```ts +class DevEnvironment { + /** + * Unique identifier for the environment in a Vite server. + * By default Vite exposes 'client' and 'ssr' environments. + */ + name: string + /** + * Communication channel to send and receive messages from the + * associated module runner in the target runtime. + */ + hot: HotChannel | null + /** + * Graph of module nodes, with the imported relationship between + * processed modules and the cached result of the processed code. + */ + moduleGraph: EnvironmentModuleGraph + /** + * Resolved plugins for this environment, including the ones + * created using the per-environment `create` hook + */ + plugins: Plugin[] + /** + * Allows to resolve, load, and transform code through the + * environment plugins pipeline + */ + pluginContainer: EnvironmentPluginContainer + /** + * Resolved config options for this environment. Options at the server + * global scope are taken as defaults for all environments, and can + * be overridden (resolve conditions, external, optimizedDeps) + */ + options: ResolvedDevEnvironmentOptions + + constructor(name, config, { hot, options }: DevEnvironmentSetup) + + /** + * Resolve the URL to an id, load it, and process the code using the + * plugins pipeline. The module graph is also updated. + */ + async transformRequest(url: string): TransformResult + + /** + * Register a request to be processed with low priority. This is useful + * to avoid waterfalls. The Vite server has information about the imported + * modules by other requests, so it can warmup the module graph so the + * modules are already processed when they are requested. + */ + async warmupRequest(url: string): void + + /** + * Fetch information about a module from the module runner without running it. + * Note: This method may not be needed + */ + async fetchModuleInfo(url: string) +} +``` + +With `TransformResult` being: + +```ts +interface TransformResult { + code: string + map: SourceMap | { mappings: '' } | null + etag?: string + deps?: string[] + dynamicDeps?: string[] +} +``` + +An environment instance in the Vite server lets you process a URL using the `environment.transformRequest(url)` method. This function will use the plugin pipeline to resolve the `url` to a module `id`, load it (reading the file from the file system or through a plugin that implements a virtual module), and then transform the code. While transforming the module, imports and other metadata will be recorded in the environment module graph by creating or updating the corresponding module node. When processing is done, the transform result is also stored in the module. + +But the environment instance can't execute the code itself, as the runtime where the module will be run could be different from the one the Vite server is running in. This is the case for the browser environment. When a html is loaded in the browser, its scripts are executed triggering the evaluation of the entire static module graph. Each imported URL generates a request to the Vite server to get the module code, which ends up handled by the Transform Middleware by calling `server.environments.client.transformRequest(url)`. The connection between the environment instance in the server and the module runner in the browser is carried out through HTTP in this case. + +:::info transformRequest naming +We are using `transformRequest(url)` and `warmupRequest(url)` in the current version of this proposal so it is easier to discuss and understand for users used to Vite's current API. Before releasing, we can take the opportunity to review these names too. For example, it could be named `environment.processModule(url)` or `environment.loadModule(url)` taking a page from Rollup's `context.load(id)` in plugin hooks. For the moment, we think keeping the current names and delaying this discussion is better. +::: + +:::info Running a module +The initial proposal had a `run` method that would allow consumers to invoke an import on the runner side by using the `transport` option. During our testing we found out that the API was not unversal enough to start recommending it. We are open to implement a built-in layer for remote SSR implementation based on the frameworks feedback. In the meantime, Vite still exposes a [`RunnerTransport` API](#runnertransport) to hide the complexity of the runner RPC. +::: + +For the `ssr` environment running in Node by default, Vite creates a module runner that implements evaluation using `new AsyncFunction` running in the same JS runtime as the dev server. This runner is an instance of `ModuleRunner` that exposes: + +```ts +class ModuleRunner { + /** + * URL to execute. Accepts file path, server path, or id relative to the root. + * Returns an instantiated module (same as in ssrLoadModule) + */ + public async import(url: string): Promise> + /** + * Other ModuleRunner methods... + */ +``` + +:::info +In the v5.1 Runtime API, there were `executeUrl` and `executeEntryPoint` methods - they are now merged into a single `import` method. If you want to opt-out of the HMR support, create a runner with `hmr: false` flag. +::: + +The default SSR Node module runner is not exposed. You can use `createNodeEnvironment` API with `createServerModuleRunner` together to create a runner that runs code in the same thread, supports HMR and doesn't conflict with the SSR implementation (in case it's been overriden in the config). Given a Vite server configured in middleware mode as described by the [SSR setup guide](/guide/ssr#setting-up-the-dev-server), let's implement the SSR middleware using the environment API. Error handling is omitted. + +```js +import { + createServer, + createServerModuleRunner, + createNodeEnvironment, +} from 'vite' + +const server = await createServer({ + server: { middlewareMode: true }, + appType: 'custom', + environments: { + node: { + dev: { + // Default Vite SSR environment can be overriden in the config, so + // make sure you have a Node environment before the request is received. + createEnvironment: createNodeEnvironment, + }, + }, + }, +}) + +const runner = createServerModuleRunner(server.environments.node) + +app.use('*', async (req, res, next) => { + const url = req.originalUrl + + // 1. Read index.html + let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8') + + // 2. Apply Vite HTML transforms. This injects the Vite HMR client, + // and also applies HTML transforms from Vite plugins, e.g. global + // preambles from @vitejs/plugin-react + template = await server.transformIndexHtml(url, template) + + // 3. Load the server entry. import(url) automatically transforms + // ESM source code to be usable in Node.js! There is no bundling + // required, and provides full HMR support. + const { render } = await runner.import('/src/entry-server.js') + + // 4. render the app HTML. This assumes entry-server.js's exported + // `render` function calls appropriate framework SSR APIs, + // e.g. ReactDOMServer.renderToString() + const appHtml = await render(url) + + // 5. Inject the app-rendered HTML into the template. + const html = template.replace(``, appHtml) + + // 6. Send the rendered HTML back. + res.status(200).set({ 'Content-Type': 'text/html' }).end(html) +}) +``` + +## Environment agnostic SSR + +::: info +It isn't clear yet what APIs Vite should provide to cover the most common SSR use cases. We are thinking on releasing the Environment API without an official way to do environment agnostic SSR to let the ecosystem explore common patterns first. +::: + +## Separate module graphs + +Each environment has an isolated module graph. All module graphs have the same signature, so generic algorithms can be implemented to crawl or query the graph without depending on the environment. `hotUpdate` is a good example. When a file is modified, the module graph of each environment will be used to discover the affected modules and perform HMR for each environment independently. + +::: info +Vite v5 had a mixed Client and SSR module graph. Given an unprocessed or invalidated node, it isn't possible to know if it corresponds to the Client, SSR, or both environments. Module nodes have some properties prefixed, like `clientImportedModules` and `ssrImportedModules` (and `importedModules` that returns the union of both). `importers` contains all importers from both the Client and SSR environment for each module node. A module node also has `transformResult` and `ssrTransformResult`. A backward compatibility layer allows the ecosystem to migrate from the deprecated `server.moduleGraph`. +::: + +Each module is represented by a `EnvironmentModuleNode` instance. Modules may be registered in the graph without yet being processed (`transformResult` would be `null` in that case). `importers` and `importedModules` are also updated after the module is processed. + +```ts +class EnvironmentModuleNode { + environment: string + + url: string + id: string | null = null + file: string | null = null + + type: 'js' | 'css' + + importers = new Set() + importedModules = new Set() + importedBindings: Map> | null = null + + info?: ModuleInfo + meta?: Record + transformResult: TransformResult | null = null + + acceptedHmrDeps = new Set() + acceptedHmrExports: Set | null = null + isSelfAccepting?: boolean + lastHMRTimestamp = 0 + lastInvalidationTimestamp = 0 +} +``` + +`environment.moduleGraph` is an instance of `EnvironmentModuleGraph`: + +```ts +export class EnvironmentModuleGraph { + environment: string + + urlToModuleMap = new Map() + idToModuleMap = new Map() + etagToModuleMap = new Map() + fileToModulesMap = new Map>() + + constructor( + environment: string, + resolveId: (url: string) => Promise, + ) + + async getModuleByUrl(rawUrl: string): Promise + + getModulesByFile(file: string): Set | undefined + + onFileChange(file: string): void + + invalidateModule( + mod: ModuleNode, + seen: Set = new Set(), + timestamp: number = Date.now(), + isHmr: boolean = false, + ): void + + invalidateAll(): void + + async updateModuleInfo( + mod: ModuleNode, + importedModules: Set, + importedBindings: Map> | null, + acceptedModules: Set, + acceptedExports: Set | null, + isSelfAccepting: boolean, + ): Promise | undefined> + + async ensureEntryFromUrl( + rawUrl: string, + setIsSelfAccepting = true, + ): Promise + + createFileOnlyEntry(file: string): ModuleNode + + async resolveUrl(url: string): Promise + + updateModuleTransformResult( + mod: ModuleNode, + result: TransformResult | null, + ): void + + getModuleByEtag(etag: string): ModuleNode | undefined +} +``` + +## Creating new environments + +One of the goals of this feature is to provide a customizable API to process and run code. Users can create new environment types using the exposed primitives. + +```ts +import { DevEnvironment, RemoteEnvironmentTransport } from 'vite' + +function createWorkerdDevEnvironment(name: string, config: ResolvedConfig, options?: DevEnvironmentOptions) { + const hot = /* ... */ + const connection = /* ... */ + const transport = new RemoteEnvironmentTransport({ + send: (data) => connection.send(data), + onMessage: (listener) => connection.on('message', listener), + }) + + const workerdDevEnvironment = new DevEnvironment(name, config, { + options: { + resolve: { conditions: ['custom'] }, + ...options, + }, + hot, + runner: { + transport, + }, + }) + return workerdDevEnvironment +} +``` + +Then users can create a workerd environment to do SSR using: + +```js +const ssrEnvironment = createWorkerdEnvironment('ssr', config) +``` + +## Environment Configuration + +Environments are explicitely configured with the `environments` config option. + +```js +export default { + environments: { + client: { + resolve: { + conditions: [], // configure the Client environment + }, + }, + ssr: { + dev: { + optimizeDeps: {}, // configure the SSR environment + }, + }, + rsc: { + resolve: { + noExternal: true, // configure a custom environment + }, + }, + }, +} +``` + +All environment configs extend from user's root config, allowing users add defaults for all environments at the root level. This is quite useful for the common use case of configuring a Vite client only app, that can be done without going through `environments.client`. + +```js +export default { + resolve: { + conditions: [], // configure a default for all environments + }, +} +``` + +The `EnvironmentOptions` interface exposes all the per-environment options. There are `SharedEnvironmentOptions` that apply to both `build` and `dev`, like `resolve`. And there are `DevEnvironmentOptions` and `BuildEnvironmentOptions` for dev and build specific options (like `dev.optimizeDeps` or `build.outDir`). + +```ts +interface EnvironmentOptions extends SharedEnvironmentOptions { + dev: DevOptions + build: BuildOptions +} +``` + +As we explained, Environment specific options defined at the root level of user config are used for the default client environment (the `UserConfig` interface extends from the `EnvironmentOptions` interface). And environments can be configured explicitly using the `environments` record. The `client` and `ssr` environments are always present during dev, even if an empty object is set to `environments`. This allows backward compatibility with `server.ssrLoadModule(url)` and `server.moduleGraph`. During build, the `client` environment is always present, and the `ssr` environment is only present if it is explicitly configured (using `environments.ssr` or for backward compatibility `build.ssr`). + +```ts +interface UserConfig extends EnvironmentOptions { + environments: Record + // other options +} +``` + +::: info + +The `ssr` top level property has many options in common with `EnvironmentOptions`. This option was created for the same use case as `environments` but only allowed configuration of a small number of options. We're going to deprecate it in favour of a unified way to define environment configuration. + +::: + +## Custom environment instances + +To create custom dev or build environment instances, you can use the `dev.createEnvironment` or `build.createEnvironment` functions. + +```js +export default { + environments: { + rsc: { + dev: { + createEnvironment(name, config) { + // Called with 'rsc' and the resolved config during dev + return createNodeDevEnvironment(name, config) + } + }, + build: { + createEnvironment(name, config) { + // Called with 'rsc' and the resolved config during build + return createNodeBuildEnvironment(name, config) + } + outDir: '/dist/rsc', + }, + }, + }, +} +``` + +The environment will be accessible in middlewares or plugin hooks through `server.environments`. In plugin hooks, the environment instance is passed in the options so they can do conditions depending on the way they are configured. + +Environment providers like Workerd, can expose an environment provider for the most common case of using the same runtime for both dev and build environments. The default environment options can also be set so the user doesn't need to do it. + +```js +function createWorkedEnvironment(userConfig) { + return mergeConfig( + { + resolve: { + conditions: [ + /*...*/ + ], + }, + dev: { + createEnvironment(name, config) { + return createWorkerdDevEnvironment(name, config) + }, + }, + build: { + createEnvironment(name, config) { + return createWorkerdBuildEnvironment(name, config) + }, + }, + }, + userConfig, + ) +} +``` + +Then the config file can be written as + +```js +import { workerdEnvironment } from 'vite-environment-workerd' + +export default { + environments: { + ssr: createWorkerdEnvironment({ + build: { + outDir: '/dist/ssr', + }, + }), + rsc: createWorkerdEnvironment({ + build: { + outDir: '/dist/rsc', + }, + }), + ], +} +``` + +In this case we see how the `ssr` environment can be configured to use workerd as it's runtime. Additionally a new custom RSC environment is also defined, backed by a separate instance of the workerd runtime. + +## Plugins and environments + +### Accessing the current environment in hooks + +The Vite server has a shared plugin pipeline, but when a module is processed it is always done in the context of a given environment. The `environment` instance is available in the plugin context of `resolveId`, `load`, and `transform`. + +A plugin could use the `environment` instance to: + +- Only apply logic for certain environments. +- Change the way they work depending on the configuration for the environment, which can be accessed using `environment.options`. The vite core resolve plugin modifies the way it resolves ids based on `environment.options.resolve.conditions` for example. + +```ts + transform(code, id) { + console.log(this.enviroment.options.resolve.conditions) + } +``` + +### Registering new environments using hooks + +Plugins can add new environments in the `config` hook: + +```ts + config(config: UserConfig) { + config.environments.rsc ??= {} + } +``` + +An empty object is enough to register the environment, default values from the root level environment config. + +### Configuring environment using hooks + +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 explicitly 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, options: EnvironmentOptions) { + if (name === 'rsc') { + options.resolve.conditions = // ... +``` + +### The `hotUpdate` hook + +- **Type:** `(ctx: HotContext) => Array | void | Promise | void>` +- **See also:** [HMR API](./api-hmr) + +The `hotUpdate` hook allows plugins to perform custom HMR update handling for a given environment. When a file changes, the HMR algorithm is run for each environment in series according to the order in `server.environments`, so the `hotUpdate` hook will be called multiple times. The hook receives a context object with the following signature: + +```ts +interface HotContext { + file: string + timestamp: number + modules: Array + read: () => string | Promise + server: ViteDevServer +} +``` + +- `this.environment` is the module execution environment where a file update is currently being processed. + +- `modules` is an array of modules in this environment that are affected by the changed file. It's an array because a single file may map to multiple served modules (e.g. Vue SFCs). + +- `read` is an async read function that returns the content of the file. This is provided because, on some systems, the file change callback may fire too fast before the editor finishes updating the file, and direct `fs.readFile` will return empty content. The read function passed in normalizes this behavior. + +The hook can choose to: + +- Filter and narrow down the affected module list so that the HMR is more accurate. + +- Return an empty array and perform a full reload: + + ```js + hotUpdate({ modules, timestamp }) { + if (this.environment.name !== 'client') + return + + this.environment.hot.send({ type: 'full-reload' }) + // Invalidate modules manually + const invalidatedModules = new Set() + for (const mod of modules) { + this.environment.moduleGraph.invalidateModule( + mod, + invalidatedModules, + timestamp, + true + ) + } + return [] + } + ``` + +- Return an empty array and perform complete custom HMR handling by sending custom events to the client: + + ```js + hotUpdate() { + if (this.environment.name !== 'client') + return + + this.environment.hot.send({ + type: 'custom', + event: 'special-update', + data: {} + }) + return [] + } + ``` + + Client code should register the corresponding handler using the [HMR API](./api-hmr) (this could be injected by the same plugin's `transform` hook): + + ```js + if (import.meta.hot) { + import.meta.hot.on('special-update', (data) => { + // perform custom update + }) + } + ``` + +### Per-environment Plugins + +A plugin can define what are the environments it should apply to with the `applyToEnvironment` function. + +```js +const UnoCssPlugin = () => { + // shared global state + return { + buildStart() { + // init per environment state with WeakMap, this.environment + }, + configureServer() { + // use global hooks normally + }, + applyToEnvironment(environment) { + // return true if this plugin should be active in this environment + // if the function isn't provided, the plugin is active in all environments + }, + resolveId(id, importer) { + // only called for environments this plugin apply to + }, + } +} +``` + +## `ModuleRunner` + +A module runner is instantiated in the target runtime. All APIs in the next section are imported from `vite/module-runner` unless stated otherwise. This export entry point is kept as lightweight as possible, only exporting the minimal needed to create module runners. + +**Type Signature:** + +```ts +export class ModuleRunner { + constructor( + public options: ModuleRunnerOptions, + public evaluator: ModuleEvaluator, + private debug?: ModuleRunnerDebugger, + ) {} + /** + * URL to execute. Accepts file path, server path, or id relative to the root. + */ + public async import(url: string): Promise + /** + * Clear all caches including HMR listeners. + */ + public clearCache(): void + /** + * Clears all caches, removes all HMR listeners, and resets source map support. + * This method doesn't stop the HMR connection. + */ + public async destroy(): Promise + /** + * Returns `true` if the runner has been destroyed by calling `destroy()` method. + */ + public isDestroyed(): boolean +} +``` + +The module evaluator in `ModuleRunner` is responsible for executing the code. Vite exports `ESModulesEvaluator` out of the box, it uses `new AsyncFunction` to evaluate the code. You can provide your own implementation if your JavaScript runtime doesn't support unsafe evaluation. + +Module runner exposes `import` method. When Vite server triggers `full-reload` HMR event, all affected modules will be re-executed. Be aware that Module Runner doesn't update `exports` object when this happens (it overrides it), you would need to run `import` or get the module from `moduleCache` again if you rely on having the latest `exports` object. + +**Example Usage:** + +```js +import { ModuleRunner, ESModulesEvaluator } from 'vite/module-runner' +import { root, fetchModule } from './rpc-implementation.js' + +const moduleRunner = new ModuleRunner( + { + root, + fetchModule, + // you can also provide hmr.connection to support HMR + }, + new ESModulesEvaluator(), +) + +await moduleRunner.import('/src/entry-point.js') +``` + +## `ModuleRunnerOptions` + +```ts +export interface ModuleRunnerOptions { + /** + * Root of the project + */ + root: string + /** + * A set of methods to communicate with the server. + */ + transport: RunnerTransport + /** + * Configure how source maps are resolved. Prefers `node` if `process.setSourceMapsEnabled` is available. + * Otherwise it will use `prepareStackTrace` by default which overrides `Error.prepareStackTrace` method. + * You can provide an object to configure how file contents and source maps are resolved for files that were not processed by Vite. + */ + sourcemapInterceptor?: + | false + | 'node' + | 'prepareStackTrace' + | InterceptorOptions + /** + * Disable HMR or configure HMR options. + */ + hmr?: + | false + | { + /** + * Configure how HMR communicates between the client and the server. + */ + connection: ModuleRunnerHMRConnection + /** + * Configure HMR logger. + */ + logger?: false | HMRLogger + } + /** + * Custom module cache. If not provided, it creates a separate module cache for each module runner instance. + */ + moduleCache?: ModuleCacheMap +} +``` + +## `ModuleEvaluator` + +**Type Signature:** + +```ts +export interface ModuleEvaluator { + /** + * Evaluate code that was transformed by Vite. + * @param context Function context + * @param code Transformed code + * @param id ID that was used to fetch the module + */ + runInlinedModule( + context: ModuleRunnerContext, + code: string, + id: string, + ): Promise + /** + * evaluate externalized module. + * @param file File URL to the external module + */ + runExternalModule(file: string): Promise +} +``` + +Vite exports `ESModulesEvaluator` that implements this interface by default. It uses `new AsyncFunction` to evaluate code, so if the code has inlined source map it should contain an [offset of 2 lines](https://tc39.es/ecma262/#sec-createdynamicfunction) to accommodate for new lines added. This is done automatically in the server node environment. If your runner implementation doesn't have this constraint, you should use `fetchModule` (exported from `vite`) directly. + +## RunnerTransport + +**Type Signature:** + +```ts +interface RunnerTransport { + /** + * A method to get the information about the module. + */ + fetchModule: FetchFunction +} +``` + +Transport object that communicates with the environment via an RPC or by directly calling the function. By default, you need to pass an object with `fetchModule` method - it can use any type of RPC inside of it, but Vite also exposes `RemoteRunnerTransport` to make the configuration easier. You need to couple it with the `RemoteEnvironmentTransport` instance on the server like in this example where module runner is created in the worker thread: + +::: code-group + +```ts [worker.js] +import { parentPort } from 'node:worker_threads' +import { fileURLToPath } from 'node:url' +import { + ESModulesEvaluator, + ModuleRunner, + RemoteRunnerTransport, +} from 'vite/module-runner' + +const runner = new ModuleRunner( + { + root: fileURLToPath(new URL('./', import.meta.url)), + transport: new RemoteRunnerTransport({ + send: (data) => parentPort.postMessage(data), + onMessage: (listener) => parentPort.on('message', listener), + timeout: 5000, + }), + }, + new ESModulesEvaluator(), +) +``` + +```ts [server.js] +import { BroadcastChannel } from 'node:worker_threads' +import { createServer, RemoteEnvironmentTransport, DevEnvironment } from 'vite' + +function createWorkerEnvironment(name, config) { + const worker = new Worker('./worker.js') + return new DevEnvironment(name, config, { + runner: { + transport: new RemoteEnvironmentTransport({ + send: (data) => worker.postMessage(data), + onMessage: (listener) => worker.on('message', listener), + }), + }, + }) +} + +await createServer({ + environments: { + worker: { + dev: { + createEnvironment: createWorkerEnvironment, + }, + }, + }, +}) +``` + +::: + +`RemoteRunnerTransport` and `RemoteEnvironmentTransport` are meant to be used together. If you don't use either of them, then you can define your own function to communicate between the runner and the server. + +## ModuleRunnerHMRConnection + +**Type Signature:** + +```ts +export interface ModuleRunnerHMRConnection { + /** + * Checked before sending messages to the client. + */ + isReady(): boolean + /** + * Send a message to the client. + */ + send(message: string): void + /** + * Configure how HMR is handled when this connection triggers an update. + * This method expects that the connection will start listening for HMR updates and call this callback when it's received. + */ + onUpdate(callback: (payload: HotPayload) => void): void +} +``` + +This interface defines how HMR communication is established. Vite exports `ServerHMRConnector` from the main entry point to support HMR during Vite SSR. The `isReady` and `send` methods are usually called when the custom event is triggered (like, `import.meta.hot.send("my-event")`). + +`onUpdate` is called only once when the new module runner is initiated. It passed down a method that should be called when connection triggers the HMR event. The implementation depends on the type of connection (as an example, it can be `WebSocket`/`EventEmitter`/`MessageChannel`), but it usually looks something like this: + +```js +function onUpdate(callback) { + this.connection.on('hmr', (event) => callback(event.data)) +} +``` + +The callback is queued and it will wait for the current update to be resolved before processing the next update. Unlike the browser implementation, HMR updates in a module runner will wait until all listeners (like, `vite:beforeUpdate`/`vite:beforeFullReload`) are finished before updating the modules. + +## Environments during build + +In the CLI, calling `vite build` and `vite build --ssr` will still build the client only and ssr only environments for backward compatibility. + +When `builder.entireApp` is `true` (or when calling `vite build --app`), `vite build` will opt-in into building the entire app instead. This would later on become the default in a future major. A `ViteBuilder` instance will be created (build-time equivalent to a `ViteDevServer`) to build all configured environments for production. By default the build of environments is run in series respecting the order of the `environments` record. A framework or user can further configure how the environments are built using: + +```js +export default { + builder: { + buildApp: async (builder) => { + const environments = Object.values(builder.environments) + return Promise.all( + environments.map((environment) => builder.build(environment)), + ) + }, + }, +} +``` + +### Environment in build hooks + +In the same way as during dev, plugin hooks also receive the environment instance during build, replacing the `ssr` boolean. +This also works for `renderChunk`, `generateBundle`, and other build only hooks. + +### Shared plugins during build + +Before Vite 6, the plugins pipelines worked in a different way during dev and build: + +- **During dev:** plugins are shared +- **During Build:** plugins are isolated for each environment (in different processes: `vite build` then `vite build --ssr`). + +This forced frameworks to share state between the `client` build and the `ssr` build through manifest files written to the file system. In Vite 6, we are now building all environments in a single process so the way the plugins pipeline and inter-environment communication can be aligned with dev. + +In a future major (Vite 7 or 8), we aim to have complete alignment: + +- **During both dev and build:** plugins are shared, opt-in to [per-environment isolation with functional plugins](#per-environment-plugins) + +There will also be a single `ResolvedConfig` instance shared during build, allowing for caching at entire app build process level in the same way as we have been doing with `WeakMap` during dev. + +For Vite 6, we need to do a smaller step to keep backward compatibility. Ecosystem plugins are currently using `config.build` instead of `environment.options.build` to access configuration, so we need to create a new `ResolvedConfig` per environment by default. A project can opt-in into sharing the full config and plugins pipeline setting `builder.sharedConfig` to `true`. + +This option would only work of a small subset of projects at first, so plugin authors can opt-in for a particular plugin to be shared by setting the `sharedDuringBuild` flag to `true`. This allows for easily sharing state both for regular plugins: + +```js +function myPlugin() { + // Share state among all environments in dev and build + const sharedState = ... + return { + name: 'shared-plugin', + transform(code, id) { ... }, + + // Opt-in into a single instance for all environments + sharedDuringBuild: true, + } +} +``` + +## Backward Compatibility + +The current Vite server API will be deprecated but keep working during the next major. + +| Before | After | +| :-------------------------------------------: | :------------------------------------------------: | +| `server.transformRequest(url)` | `server.environments.client.transformRequest(url)` | +| `server.transformRequest(url, { ssr: true })` | `server.environments.ssr.tranformRequest(url)` | +| `server.warmupRequest(url)` | `server.environments.client.warmupRequest(url)` | +| `server.ssrLoadModule(url)` | `TBD` | +| `server.moduleGraph` | `environment.moduleGraph` | +| `handleHotUpdate` | `hotUpdate` | + +The last one is just an idea. We may want to keep `server.open(url)` around. + +The `server.moduleGraph` will keep returning a mixed view of the client and ssr module graphs. Backward compatible mixed module nodes will be returned from all previous functions. The same scheme is used for the module nodes passed to `handleHotUpdate`. This is the most difficult change to get right regarding backward compatibility. We may need to accept small breaking changes when we release the API in Vite 6, making it opt-in until then when releasing the API as experimental in Vite 5.2. diff --git a/docs/guide/api-vite-runtime.md b/docs/guide/api-vite-runtime.md deleted file mode 100644 index 9aa579d268ddcf..00000000000000 --- a/docs/guide/api-vite-runtime.md +++ /dev/null @@ -1,236 +0,0 @@ -# Vite Runtime API - -:::warning Low-level API -This API was introduced in Vite 5.1 as an experimental feature. It was added to [gather feedback](https://github.com/vitejs/vite/discussions/15774). There will likely be breaking changes, so make sure to pin the Vite version to `~5.1.0` when using it. This is a low-level API meant for library and framework authors. If your goal is to create an application, make sure to check out the higher-level SSR plugins and tools at [Awesome Vite SSR section](https://github.com/vitejs/awesome-vite#ssr) first. - -Currently, the API is being revised as the [Environment API](https://github.com/vitejs/vite/discussions/16358) which is released at `^6.0.0-alpha.0`. -::: - -The "Vite Runtime" is a tool that allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runtime implementation is decoupled from the server. This allows library and framework authors to implement their own layer of communication between the server and the runtime. - -One of the goals of this feature is to provide a customizable API to process and run the code. Vite provides enough tools to use Vite Runtime out of the box, but users can build upon it if their needs do not align with Vite's built-in implementation. - -All APIs can be imported from `vite/runtime` unless stated otherwise. - -## `ViteRuntime` - -**Type Signature:** - -```ts -export class ViteRuntime { - constructor( - public options: ViteRuntimeOptions, - public runner: ViteModuleRunner, - private debug?: ViteRuntimeDebugger, - ) {} - /** - * URL to execute. Accepts file path, server path, or id relative to the root. - */ - public async executeUrl(url: string): Promise - /** - * Entry point URL to execute. Accepts file path, server path or id relative to the root. - * In the case of a full reload triggered by HMR, this is the module that will be reloaded. - * If this method is called multiple times, all entry points will be reloaded one at a time. - */ - public async executeEntrypoint(url: string): Promise - /** - * Clear all caches including HMR listeners. - */ - public clearCache(): void - /** - * Clears all caches, removes all HMR listeners, and resets source map support. - * This method doesn't stop the HMR connection. - */ - public async destroy(): Promise - /** - * Returns `true` if the runtime has been destroyed by calling `destroy()` method. - */ - public isDestroyed(): boolean -} -``` - -::: tip Advanced Usage -If you are just migrating from `server.ssrLoadModule` and want to support HMR, consider using [`createViteRuntime`](#createviteruntime) instead. -::: - -The `ViteRuntime` class requires `root` and `fetchModule` options when initiated. Vite exposes `ssrFetchModule` on the [`server`](/guide/api-javascript) instance for easier integration with Vite SSR. Vite also exports `fetchModule` from its main entry point - it doesn't make any assumptions about how the code is running unlike `ssrFetchModule` that expects the code to run using `new Function`. This can be seen in source maps that these functions return. - -Runner in `ViteRuntime` is responsible for executing the code. Vite exports `ESModulesRunner` out of the box, it uses `new AsyncFunction` to run the code. You can provide your own implementation if your JavaScript runtime doesn't support unsafe evaluation. - -The two main methods that runtime exposes are `executeUrl` and `executeEntrypoint`. The only difference between them is that all modules executed by `executeEntrypoint` will be reexecuted if HMR triggers `full-reload` event. Be aware that Vite Runtime doesn't update `exports` object when this happens (it overrides it), you would need to run `executeUrl` or get the module from `moduleCache` again if you rely on having the latest `exports` object. - -**Example Usage:** - -```js -import { ViteRuntime, ESModulesRunner } from 'vite/runtime' -import { root, fetchModule } from './rpc-implementation.js' - -const runtime = new ViteRuntime( - { - root, - fetchModule, - // you can also provide hmr.connection to support HMR - }, - new ESModulesRunner(), -) - -await runtime.executeEntrypoint('/src/entry-point.js') -``` - -## `ViteRuntimeOptions` - -```ts -export interface ViteRuntimeOptions { - /** - * Root of the project - */ - root: string - /** - * A method to get the information about the module. - * For SSR, Vite exposes `server.ssrFetchModule` function that you can use here. - * For other runtime use cases, Vite also exposes `fetchModule` from its main entry point. - */ - fetchModule: FetchFunction - /** - * Configure how source maps are resolved. Prefers `node` if `process.setSourceMapsEnabled` is available. - * Otherwise it will use `prepareStackTrace` by default which overrides `Error.prepareStackTrace` method. - * You can provide an object to configure how file contents and source maps are resolved for files that were not processed by Vite. - */ - sourcemapInterceptor?: - | false - | 'node' - | 'prepareStackTrace' - | InterceptorOptions - /** - * Disable HMR or configure HMR options. - */ - hmr?: - | false - | { - /** - * Configure how HMR communicates between the client and the server. - */ - connection: HMRRuntimeConnection - /** - * Configure HMR logger. - */ - logger?: false | HMRLogger - } - /** - * Custom module cache. If not provided, it creates a separate module cache for each ViteRuntime instance. - */ - moduleCache?: ModuleCacheMap -} -``` - -## `ViteModuleRunner` - -**Type Signature:** - -```ts -export interface ViteModuleRunner { - /** - * Run code that was transformed by Vite. - * @param context Function context - * @param code Transformed code - * @param id ID that was used to fetch the module - */ - runViteModule( - context: ViteRuntimeModuleContext, - code: string, - id: string, - ): Promise - /** - * Run externalized module. - * @param file File URL to the external module - */ - runExternalModule(file: string): Promise -} -``` - -Vite exports `ESModulesRunner` that implements this interface by default. It uses `new AsyncFunction` to run code, so if the code has inlined source map it should contain an [offset of 2 lines](https://tc39.es/ecma262/#sec-createdynamicfunction) to accommodate for new lines added. This is done automatically by `server.ssrFetchModule`. If your runner implementation doesn't have this constraint, you should use `fetchModule` (exported from `vite`) directly. - -## HMRRuntimeConnection - -**Type Signature:** - -```ts -export interface HMRRuntimeConnection { - /** - * Checked before sending messages to the client. - */ - isReady(): boolean - /** - * Send message to the client. - */ - send(message: string): void - /** - * Configure how HMR is handled when this connection triggers an update. - * This method expects that connection will start listening for HMR updates and call this callback when it's received. - */ - onUpdate(callback: (payload: HMRPayload) => void): void -} -``` - -This interface defines how HMR communication is established. Vite exports `ServerHMRConnector` from the main entry point to support HMR during Vite SSR. The `isReady` and `send` methods are usually called when the custom event is triggered (like, `import.meta.hot.send("my-event")`). - -`onUpdate` is called only once when the new runtime is initiated. It passed down a method that should be called when connection triggers the HMR event. The implementation depends on the type of connection (as an example, it can be `WebSocket`/`EventEmitter`/`MessageChannel`), but it usually looks something like this: - -```js -function onUpdate(callback) { - this.connection.on('hmr', (event) => callback(event.data)) -} -``` - -The callback is queued and it will wait for the current update to be resolved before processing the next update. Unlike the browser implementation, HMR updates in Vite Runtime wait until all listeners (like, `vite:beforeUpdate`/`vite:beforeFullReload`) are finished before updating the modules. - -## `createViteRuntime` - -**Type Signature:** - -```ts -async function createViteRuntime( - server: ViteDevServer, - options?: MainThreadRuntimeOptions, -): Promise -``` - -**Example Usage:** - -```js -import { createServer } from 'vite' - -const __dirname = fileURLToPath(new URL('.', import.meta.url)) - -;(async () => { - const server = await createServer({ - root: __dirname, - }) - await server.listen() - - const runtime = await createViteRuntime(server) - await runtime.executeEntrypoint('/src/entry-point.js') -})() -``` - -This method serves as an easy replacement for `server.ssrLoadModule`. Unlike `ssrLoadModule`, `createViteRuntime` provides HMR support out of the box. You can pass down [`options`](#mainthreadruntimeoptions) to customize how SSR runtime behaves to suit your needs. - -## `MainThreadRuntimeOptions` - -```ts -export interface MainThreadRuntimeOptions - extends Omit { - /** - * Disable HMR or configure HMR logger. - */ - hmr?: - | false - | { - logger?: false | HMRLogger - } - /** - * Provide a custom module runner. This controls how the code is executed. - */ - runner?: ViteModuleRunner -} -``` diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 32d8a499e3c358..04df92f5f6a2bb 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -1,246 +1,9 @@ -# Migration from v4 +# Migration from v5 -## Node.js Support +## Environment API -Vite no longer supports Node.js 14 / 16 / 17 / 19, which reached its EOL. Node.js 18 / 20+ is now required. +TODO -## Rollup 4 +## Migration from v4 -Vite is now using Rollup 4 which also brings along its breaking changes, in particular: - -- Import assertions (`assertions` prop) has been renamed to import attributes (`attributes` prop). -- Acorn plugins are no longer supported. -- For Vite plugins, `this.resolve` `skipSelf` option is now `true` by default. -- For Vite plugins, `this.parse` now only supports the `allowReturnOutsideFunction` option for now. - -Read the full breaking changes in [Rollup's release notes](https://github.com/rollup/rollup/releases/tag/v4.0.0) for build-related changes in [`build.rollupOptions`](/config/build-options.md#build-rollupoptions). - -If you are using TypeScript, make sure to set `moduleResolution: 'bundler'` (or `node16`/`nodenext`) as Rollup 4 requires it. Or you can set `skipLibCheck: true` instead. - -## Deprecate CJS Node API - -The CJS Node API of Vite is deprecated. When calling `require('vite')`, a deprecation warning is now logged. You should update your files or frameworks to import the ESM build of Vite instead. - -In a basic Vite project, make sure: - -1. The `vite.config.js` file content is using the ESM syntax. -2. The closest `package.json` file has `"type": "module"`, or use the `.mjs`/`.mts` extension, e.g. `vite.config.mjs` or `vite.config.mts`. - -For other projects, there are a few general approaches: - -- **Configure ESM as default, opt-in to CJS if needed:** Add `"type": "module"` in the project `package.json`. All `*.js` files are now interpreted as ESM and needs to use the ESM syntax. You can rename a file with the `.cjs` extension to keep using CJS instead. -- **Keep CJS as default, opt-in to ESM if needed:** If the project `package.json` does not have `"type": "module"`, all `*.js` files are interpreted as CJS. You can rename a file with the `.mjs` extension to use ESM instead. -- **Dynamically import Vite:** If you need to keep using CJS, you can dynamically import Vite using `import('vite')` instead. This requires your code to be written in an `async` context, but should still be manageable as Vite's API is mostly asynchronous. - -See the [troubleshooting guide](/guide/troubleshooting.html#vite-cjs-node-api-deprecated) for more information. - -## Rework `define` and `import.meta.env.*` replacement strategy - -In Vite 4, the [`define`](/config/shared-options.md#define) and [`import.meta.env.*`](/guide/env-and-mode.md#env-variables) features use different replacement strategies in dev and build: - -- In dev, both features are injected as global variables to `globalThis` and `import.meta` respectively. -- In build, both features are statically replaced with a regex. - -This results in a dev and build inconsistency when trying to access the variables, and sometimes even caused failed builds. For example: - -```js -// vite.config.js -export default defineConfig({ - define: { - __APP_VERSION__: JSON.stringify('1.0.0'), - }, -}) -``` - -```js -const data = { __APP_VERSION__ } -// dev: { __APP_VERSION__: "1.0.0" } ✅ -// build: { "1.0.0" } ❌ - -const docs = 'I like import.meta.env.MODE' -// dev: "I like import.meta.env.MODE" ✅ -// build: "I like "production"" ❌ -``` - -Vite 5 fixes this by using `esbuild` to handle the replacements in builds, aligning with the dev behaviour. - -This change should not affect most setups, as it's already documented that `define` values should follow esbuild's syntax: - -> To be consistent with esbuild behavior, expressions must either be a JSON object (null, boolean, number, string, array, or object) or a single identifier. - -However, if you prefer to keep statically replacing values directly, you can use [`@rollup/plugin-replace`](https://github.com/rollup/plugins/tree/master/packages/replace). - -## General Changes - -### SSR externalized modules value now matches production - -In Vite 4, SSR externalized modules are wrapped with `.default` and `.__esModule` handling for better interoperability, but it doesn't match the production behaviour when loaded by the runtime environment (e.g. Node.js), causing hard-to-catch inconsistencies. By default, all direct project dependencies are SSR externalized. - -Vite 5 now removes the `.default` and `.__esModule` handling to match the production behaviour. In practice, this shouldn't affect properly-packaged dependencies, but if you encounter new issues loading modules, you can try these refactors: - -```js -// Before: -import { foo } from 'bar' - -// After: -import _bar from 'bar' -const { foo } = _bar -``` - -```js -// Before: -import foo from 'bar' - -// After: -import * as _foo from 'bar' -const foo = _foo.default -``` - -Note that these changes matches the Node.js behaviour, so you can also run the imports in Node.js to test it out. If you prefer to stick with the previous behaviour, you can set `legacy.proxySsrExternalModules` to `true`. - -### `worker.plugins` is now a function - -In Vite 4, [`worker.plugins`](/config/worker-options.md#worker-plugins) accepted an array of plugins (`(Plugin | Plugin[])[]`). From Vite 5, it needs to be configured as a function that returns an array of plugins (`() => (Plugin | Plugin[])[]`). This change is required so parallel worker builds run more consistently and predictably. - -### Allow path containing `.` to fallback to index.html - -In Vite 4, accessing a path in dev containing `.` did not fallback to index.html even if [`appType`](/config/shared-options.md#apptype) is set to `'spa'` (default). From Vite 5, it will fallback to index.html. - -Note that the browser will no longer show a 404 error message in the console if you point the image path to a non-existent file (e.g. ``). - -### Align dev and preview HTML serving behaviour - -In Vite 4, the dev and preview servers serve HTML based on its directory structure and trailing slash differently. This causes inconsistencies when testing your built app. Vite 5 refactors into a single behaviour like below, given the following file structure: - -``` -├── index.html -├── file.html -└── dir - └── index.html -``` - -| Request | Before (dev) | Before (preview) | After (dev & preview) | -| ----------------- | ---------------------------- | ----------------- | ---------------------------- | -| `/dir/index.html` | `/dir/index.html` | `/dir/index.html` | `/dir/index.html` | -| `/dir` | `/index.html` (SPA fallback) | `/dir/index.html` | `/index.html` (SPA fallback) | -| `/dir/` | `/dir/index.html` | `/dir/index.html` | `/dir/index.html` | -| `/file.html` | `/file.html` | `/file.html` | `/file.html` | -| `/file` | `/index.html` (SPA fallback) | `/file.html` | `/file.html` | -| `/file/` | `/index.html` (SPA fallback) | `/file.html` | `/index.html` (SPA fallback) | - -### Manifest files are now generated in `.vite` directory by default - -In Vite 4, the manifest files ([`build.manifest`](/config/build-options.md#build-manifest) and [`build.ssrManifest`](/config/build-options.md#build-ssrmanifest)) were generated in the root of [`build.outDir`](/config/build-options.md#build-outdir) by default. - -From Vite 5, they will be generated in the `.vite` directory in the `build.outDir` by default. This change helps deconflict public files with the same manifest file names when they are copied to the `build.outDir`. - -### Corresponding CSS files are not listed as top level entry in manifest.json file - -In Vite 4, the corresponding CSS file for a JavaScript entry point was also listed as a top-level entry in the manifest file ([`build.manifest`](/config/build-options.md#build-manifest)). These entries were unintentionally added and only worked for simple cases. - -In Vite 5, corresponding CSS files can only be found within the JavaScript entry file section. -When injecting the JS file, the corresponding CSS files [should be injected](/guide/backend-integration.md#:~:text=%3C!%2D%2D%20if%20production%20%2D%2D%3E%0A%3Clink%20rel%3D%22stylesheet%22%20href%3D%22/assets/%7B%7B%20manifest%5B%27main.js%27%5D.css%20%7D%7D%22%20/%3E%0A%3Cscript%20type%3D%22module%22%20src%3D%22/assets/%7B%7B%20manifest%5B%27main.js%27%5D.file%20%7D%7D%22%3E%3C/script%3E). -When the CSS should be injected separately, it must be added as a separate entry point. - -### CLI shortcuts require an additional `Enter` press - -CLI shortcuts, like `r` to restart the dev server, now require an additional `Enter` press to trigger the shortcut. For example, `r + Enter` to restart the dev server. - -This change prevents Vite from swallowing and controlling OS-specific shortcuts, allowing better compatibility when combining the Vite dev server with other processes, and avoids the [previous caveats](https://github.com/vitejs/vite/pull/14342). - -### Update `experimentalDecorators` and `useDefineForClassFields` TypeScript behaviour - -Vite 5 uses esbuild 0.19 and removes the compatibility layer for esbuild 0.18, which changes how [`experimentalDecorators`](https://www.typescriptlang.org/tsconfig#experimentalDecorators) and [`useDefineForClassFields`](https://www.typescriptlang.org/tsconfig#useDefineForClassFields) are handled. - -- **`experimentalDecorators` is not enabled by default** - - You need to set `compilerOptions.experimentalDecorators` to `true` in `tsconfig.json` to use decorators. - -- **`useDefineForClassFields` defaults depend on the TypeScript `target` value** - - If `target` is not `ESNext` or `ES2022` or newer, or if there's no `tsconfig.json` file, `useDefineForClassFields` will default to `false` which can be problematic with the default `esbuild.target` value of `esnext`. It may transpile to [static initialization blocks](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Static_initialization_blocks#browser_compatibility) which may not be supported in your browser. - - As such, it is recommended to set `target` to `ESNext` or `ES2022` or newer, or set `useDefineForClassFields` to `true` explicitly when configuring `tsconfig.json`. - -```jsonc -{ - "compilerOptions": { - // Set true if you use decorators - "experimentalDecorators": true, - // Set true if you see parsing errors in your browser - "useDefineForClassFields": true, - }, -} -``` - -### Remove `--https` flag and `https: true` - -The `--https` flag sets `server.https: true` and `preview.https: true` internally. This config was meant to be used together with the automatic https certification generation feature which [was dropped in Vite 3](https://v3.vitejs.dev/guide/migration.html#automatic-https-certificate-generation). Hence, this config is no longer useful as it will start a Vite HTTPS server without a certificate. - -If you use [`@vitejs/plugin-basic-ssl`](https://github.com/vitejs/vite-plugin-basic-ssl) or [`vite-plugin-mkcert`](https://github.com/liuweiGL/vite-plugin-mkcert), they will already set the `https` config internally, so you can remove `--https`, `server.https: true`, and `preview.https: true` in your setup. - -### Remove `resolvePackageEntry` and `resolvePackageData` APIs - -The `resolvePackageEntry` and `resolvePackageData` APIs are removed as they exposed Vite's internals and blocked potential Vite 4.3 optimizations in the past. These APIs can be replaced with third-party packages, for example: - -- `resolvePackageEntry`: [`import.meta.resolve`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve) or the [`import-meta-resolve`](https://github.com/wooorm/import-meta-resolve) package. -- `resolvePackageData`: Same as above, and crawl up the package directory to get the root `package.json`. Or use the community [`vitefu`](https://github.com/svitejs/vitefu) package. - -```js -import { resolve } from 'import-meta-env' -import { findDepPkgJsonPath } from 'vitefu' -import fs from 'node:fs' - -const pkg = 'my-lib' -const basedir = process.cwd() - -// `resolvePackageEntry`: -const packageEntry = resolve(pkg, basedir) - -// `resolvePackageData`: -const packageJsonPath = findDepPkgJsonPath(pkg, basedir) -const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) -``` - -## Removed Deprecated APIs - -- Default exports of CSS files (e.g `import style from './foo.css'`): Use the `?inline` query instead -- `import.meta.globEager`: Use `import.meta.glob('*', { eager: true })` instead -- `ssr.format: 'cjs'` and `legacy.buildSsrCjsExternalHeuristics` ([#13816](https://github.com/vitejs/vite/discussions/13816)) -- `server.middlewareMode: 'ssr'` and `server.middlewareMode: 'html'`: Use [`appType`](/config/shared-options.md#apptype) + [`server.middlewareMode: true`](/config/server-options.md#server-middlewaremode) instead ([#8452](https://github.com/vitejs/vite/pull/8452)) - -## Advanced - -There are some changes which only affect plugin/tool creators. - -- [[#14119] refactor!: merge `PreviewServerForHook` into `PreviewServer` type](https://github.com/vitejs/vite/pull/14119) - - The `configurePreviewServer` hook now accepts the `PreviewServer` type instead of `PreviewServerForHook` type. -- [[#14818] refactor(preview)!: use base middleware](https://github.com/vitejs/vite/pull/14818) - - Middlewares added from the returned function in `configurePreviewServer` now does not have access to the `base` when comparing the `req.url` value. This aligns the behaviour with the dev server. You can check the `base` from the `configResolved` hook if needed. -- [[#14834] fix(types)!: expose httpServer with Http2SecureServer union](https://github.com/vitejs/vite/pull/14834) - - `http.Server | http2.Http2SecureServer` is now used instead of `http.Server` where appropriate. - -Also there are other breaking changes which only affect few users. - -- [[#14098] fix!: avoid rewriting this (reverts #5312)](https://github.com/vitejs/vite/pull/14098) - - Top level `this` was rewritten to `globalThis` by default when building. This behavior is now removed. -- [[#14231] feat!: add extension to internal virtual modules](https://github.com/vitejs/vite/pull/14231) - - Internal virtual modules' id now has an extension (`.js`). -- [[#14583] refactor!: remove exporting internal APIs](https://github.com/vitejs/vite/pull/14583) - - Removed accidentally exported internal APIs: `isDepsOptimizerEnabled` and `getDepOptimizationConfig` - - Removed exported internal types: `DepOptimizationResult`, `DepOptimizationProcessing`, and `DepsOptimizer` - - Renamed `ResolveWorkerOptions` type to `ResolvedWorkerOptions` -- [[#5657] fix: return 404 for resources requests outside the base path](https://github.com/vitejs/vite/pull/5657) - - In the past, Vite responded to requests outside the base path without `Accept: text/html`, as if they were requested with the base path. Vite no longer does that and responds with 404 instead. -- [[#14723] fix(resolve)!: remove special .mjs handling](https://github.com/vitejs/vite/pull/14723) - - In the past, when a library `"exports"` field maps to an `.mjs` file, Vite will still try to match the `"browser"` and `"module"` fields to fix compatibility with certain libraries. This behavior is now removed to align with the exports resolution algorithm. -- [[#14733] feat(resolve)!: remove `resolve.browserField`](https://github.com/vitejs/vite/pull/14733) - - `resolve.browserField` has been deprecated since Vite 3 in favour of an updated default of `['browser', 'module', 'jsnext:main', 'jsnext']` for [`resolve.mainFields`](/config/shared-options.md#resolve-mainfields). -- [[#14855] feat!: add isPreview to ConfigEnv and resolveConfig](https://github.com/vitejs/vite/pull/14855) - - Renamed `ssrBuild` to `isSsrBuild` in the `ConfigEnv` object. -- [[#14945] fix(css): correctly set manifest source name and emit CSS file](https://github.com/vitejs/vite/pull/14945) - - CSS file names are now generated based on the chunk name. - -## Migration from v3 - -Check the [Migration from v3 Guide](https://v4.vitejs.dev/guide/migration.html) in the Vite v4 docs first to see the needed changes to port your app to Vite v4, and then proceed with the changes on this page. +Check the [Migration from v4 Guide](https://v5.vitejs.dev/guide/migration.html) in the Vite v5 docs first to see the needed changes to port your app to Vite 5, and then proceed with the changes on this page. diff --git a/docs/images/vite-environments.svg b/docs/images/vite-environments.svg new file mode 100644 index 00000000000000..8f7c133b1d8bc2 --- /dev/null +++ b/docs/images/vite-environments.svg @@ -0,0 +1,40 @@ +Vite Dev ServerVite Dev ServerBrowser  +EnvironmentBrowser  +EnvironmentNode  +EnvironmentNode  +EnvironmentBrowser  +RuntimeBrowser  +RuntimeWorkerd  + RuntimeWorkerd  + RuntimeWorkerd  +EnvironmentWorkerd  +EnvironmentNode  +RuntimeNode  +RuntimeBrowser  +Module  +RunnerBrowser  +Module  +RunnerNode  +Module  +RunnerNode  +Module  +RunnerWorkerd  +Module  +RunnerWorkerd  +Module  +RunnerVitest JSDOM  +EnvironmentVitest JSDOM  +EnvironmentWorker  +Thread  +Module  +RunnerWorker  +Thread  +Module  +RunnerHTTP ServerHTTP ServerMiddlewaresMiddlewaresPlugins PipelinePlugins PipelineWeb SocketWeb Socket \ No newline at end of file diff --git a/package.json b/package.json index 45be868d8f5837..440bba5695e527 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "devDependencies": { "@eslint/js": "^9.4.0", "@rollup/plugin-typescript": "^11.1.6", + "@type-challenges/utils": "^0.1.1", "@types/babel__core": "^7.20.5", "@types/babel__preset-env": "^7.9.6", "@types/convert-source-map": "^2.0.3", diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index f167d532aab54c..693cf25ed9f063 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,552 @@ +## 6.0.0-alpha.18 (2024-06-04) + +* feat: expose createIdResolver ([3635ed8](https://github.com/vitejs/vite/commit/3635ed8)) +* feat: non-nullable this.environment (#17302) ([bdd1b91](https://github.com/vitejs/vite/commit/bdd1b91)), closes [#17302](https://github.com/vitejs/vite/issues/17302) +* feat(types): `defineVitePlugin` utils (#17298) ([398a49c](https://github.com/vitejs/vite/commit/398a49c)), closes [#17298](https://github.com/vitejs/vite/issues/17298) +* feat(typescript): update tsconfck to add support for `${configDir}` replacement in ts 5.5 (#17350) ([4835e2b](https://github.com/vitejs/vite/commit/4835e2b)), closes [#17350](https://github.com/vitejs/vite/issues/17350) +* docs: assign code for each deprecation changes, provide option to opt-in (#17305) ([d8aa74c](https://github.com/vitejs/vite/commit/d8aa74c)), closes [#17305](https://github.com/vitejs/vite/issues/17305) +* docs: update ([8649049](https://github.com/vitejs/vite/commit/8649049)) +* chore: add region comment (#17370) ([a8c7083](https://github.com/vitejs/vite/commit/a8c7083)), closes [#17370](https://github.com/vitejs/vite/issues/17370) +* chore: cleanup ([28e22c6](https://github.com/vitejs/vite/commit/28e22c6)) +* chore: fix build ([2b3329d](https://github.com/vitejs/vite/commit/2b3329d)) +* chore: ignore test-dts in tsconfig ([1af7081](https://github.com/vitejs/vite/commit/1af7081)) +* chore: prettier ([c373251](https://github.com/vitejs/vite/commit/c373251)) +* chore: refactor code style ([ca940e8](https://github.com/vitejs/vite/commit/ca940e8)) +* chore(deps): update all non-major dependencies (#17373) ([f2d52f1](https://github.com/vitejs/vite/commit/f2d52f1)), closes [#17373](https://github.com/vitejs/vite/issues/17373) +* fix: check last invalidation time ([e91e269](https://github.com/vitejs/vite/commit/e91e269)) +* fix: check lastHMRTimestamp for entries in moduleRunner ([611ce5c](https://github.com/vitejs/vite/commit/611ce5c)) +* fix: correctly invalidate module in the module runner if it was invalidated on the server ([fd46a2a](https://github.com/vitejs/vite/commit/fd46a2a)) +* fix: default value of `depOptimization` ([da354ea](https://github.com/vitejs/vite/commit/da354ea)) +* fix: do not invalidate modules in the runner if not invalidated on the server ([a63263c](https://github.com/vitejs/vite/commit/a63263c)) +* fix: improve invalidation sync in the module runner ([03bc590](https://github.com/vitejs/vite/commit/03bc590)) +* fix: lastHMRTimestamp compat ([909fe28](https://github.com/vitejs/vite/commit/909fe28)) +* fix: missing HMRPayload export ([b7cbd3d](https://github.com/vitejs/vite/commit/b7cbd3d)) +* fix(css): handle lightningcss minification in Deno (#17372) ([b3f5bd1](https://github.com/vitejs/vite/commit/b3f5bd1)), closes [#17372](https://github.com/vitejs/vite/issues/17372) +* fix(css): handle url replacing when preprocessing with lightningcss (#17364) ([6fbb5e0](https://github.com/vitejs/vite/commit/6fbb5e0)), closes [#17364](https://github.com/vitejs/vite/issues/17364) +* refactor: plugin container (#17288) ([909782f](https://github.com/vitejs/vite/commit/909782f)), closes [#17288](https://github.com/vitejs/vite/issues/17288) +* refactor: reporter using custom per env api (#17297) ([c43c987](https://github.com/vitejs/vite/commit/c43c987)), closes [#17297](https://github.com/vitejs/vite/issues/17297) +* test(v6): test ssrLoadModule and virtual module invalidation (#17313) ([7d78761](https://github.com/vitejs/vite/commit/7d78761)), closes [#17313](https://github.com/vitejs/vite/issues/17313) + + + +## 6.0.0-alpha.17 (2024-05-24) + +* release: v6.0.0-alpha.17 ([e45881c](https://github.com/vitejs/vite/commit/e45881c)) +* refactor!: rename to HMRPayload to HotPayload and remove HMRBroadcaster (#16875) ([87bbb04](https://github.com/vitejs/vite/commit/87bbb04)), closes [#16875](https://github.com/vitejs/vite/issues/16875) +* fix: dep optimization options ([624e751](https://github.com/vitejs/vite/commit/624e751)) +* fix: DepOptimizationConfig ([5c655e6](https://github.com/vitejs/vite/commit/5c655e6)) +* fix: handleHotUpdate compat (#17295) ([11bddb0](https://github.com/vitejs/vite/commit/11bddb0)), closes [#17295](https://github.com/vitejs/vite/issues/17295) +* fix: optimizeDeps back compat ([478a9aa](https://github.com/vitejs/vite/commit/478a9aa)) +* fix(types): avoid referencing `WeakKey` type ([e339959](https://github.com/vitejs/vite/commit/e339959)) +* feat: per-environment optimizeDeps entries and force ([006cfb7](https://github.com/vitejs/vite/commit/006cfb7)) +* feat: reporter as shared plugin using createWeakData (#17293) ([2b69389](https://github.com/vitejs/vite/commit/2b69389)), closes [#17293](https://github.com/vitejs/vite/issues/17293) +* chore: refactor isDepsOptimizerEnabled ([5f36aa6](https://github.com/vitejs/vite/commit/5f36aa6)) + + + +## 6.0.0-alpha.16 (2024-05-23) + +* release: v6.0.0-alpha.16 ([c7d02ac](https://github.com/vitejs/vite/commit/c7d02ac)) +* feat: createWeakData ([bcb1959](https://github.com/vitejs/vite/commit/bcb1959)) +* feat: improve Plugin option types ([21225c9](https://github.com/vitejs/vite/commit/21225c9)) +* feat: only shared plugins (#17289) ([e36f2f3](https://github.com/vitejs/vite/commit/e36f2f3)), closes [#17289](https://github.com/vitejs/vite/issues/17289) +* feat: provide `environment` in every hook context ([53734a8](https://github.com/vitejs/vite/commit/53734a8)) +* feat(types): expose PluginEnvironment type ([4d03124](https://github.com/vitejs/vite/commit/4d03124)) +* fix: argument rollup types for plugin context instead of wrapping ([89ec69c](https://github.com/vitejs/vite/commit/89ec69c)) +* fix: avoid duplicating values in shared optimizeDeps config (#16737) ([a3ee7f5](https://github.com/vitejs/vite/commit/a3ee7f5)), closes [#16737](https://github.com/vitejs/vite/issues/16737) +* fix: back compat for server.pluginContainer.buildStart ([46f21b8](https://github.com/vitejs/vite/commit/46f21b8)) +* fix: backcompat with config.optimizeDeps ([778e39d](https://github.com/vitejs/vite/commit/778e39d)) +* fix: keep plugin with environmentPlugins hook ([63de43a](https://github.com/vitejs/vite/commit/63de43a)) +* chore: remove extra symbol ([431455f](https://github.com/vitejs/vite/commit/431455f)) +* chore: rename to BaseEnvironment (#16797) ([d86553a](https://github.com/vitejs/vite/commit/d86553a)), closes [#16797](https://github.com/vitejs/vite/issues/16797) + + + +## 6.0.0-alpha.15 (2024-05-20) + +* release: v6.0.0-alpha.15 ([d231966](https://github.com/vitejs/vite/commit/d231966)) +* fix: only join base url in none ssr env ([7e9dd25](https://github.com/vitejs/vite/commit/7e9dd25)) +* fix: svelte back compat ([0166bfe](https://github.com/vitejs/vite/commit/0166bfe)) +* fix hmr connected message on client ([acb208e](https://github.com/vitejs/vite/commit/acb208e)) + + + +## 6.0.0-alpha.14 (2024-05-20) + +* release: v6.0.0-alpha.14 ([2b2d010](https://github.com/vitejs/vite/commit/2b2d010)) +* feat: back to environmentPlugins hook (#16732) ([54c219e](https://github.com/vitejs/vite/commit/54c219e)), closes [#16732](https://github.com/vitejs/vite/issues/16732) +* fix: export isFileLoadingAllowed ([34d8518](https://github.com/vitejs/vite/commit/34d8518)) +* fix: rely on the current environment in resolveId, if known ([8ee7fcd](https://github.com/vitejs/vite/commit/8ee7fcd)) +* fix: revert resolveId importer require ([3546d1e](https://github.com/vitejs/vite/commit/3546d1e)) + + + +## 6.0.0-alpha.13 (2024-05-20) + +* release: v6.0.0-alpha.13 ([2ae2fca](https://github.com/vitejs/vite/commit/2ae2fca)) +* fix: types ([82111bf](https://github.com/vitejs/vite/commit/82111bf)) + + + +## 6.0.0-alpha.12 (2024-05-20) + +* release: v6.0.0-alpha.12 ([371f7a0](https://github.com/vitejs/vite/commit/371f7a0)) +* chore: export ssrTransform ([0b3cf69](https://github.com/vitejs/vite/commit/0b3cf69)) +* chore: respect ssr flag in clientInjectsions transform ([8c5674e](https://github.com/vitejs/vite/commit/8c5674e)) +* fix: remove enforce inside IsolatedPlugins ([a5efd56](https://github.com/vitejs/vite/commit/a5efd56)) + + + +## 6.0.0-alpha.11 (2024-05-04) + +* release: v6.0.0-alpha.11 ([8851d9d](https://github.com/vitejs/vite/commit/8851d9d)) +* refactor: BoundedPlugin -> IsolatedPlugin ([1ec07a4](https://github.com/vitejs/vite/commit/1ec07a4)) + + + +## 6.0.0-alpha.10 (2024-05-02) + +* release: v6.0.0-alpha.10 ([57871a4](https://github.com/vitejs/vite/commit/57871a4)) +* feat: sharedDuringBuild for bounded plugins ([d213865](https://github.com/vitejs/vite/commit/d213865)) + + + +## 6.0.0-alpha.9 (2024-05-01) + +* release: v6.0.0-alpha.9 ([2e6abb3](https://github.com/vitejs/vite/commit/2e6abb3)) +* fix: lint ([848af54](https://github.com/vitejs/vite/commit/848af54)) +* fix: module-runner bundle issue with chokidar dep ([b8ef8ff](https://github.com/vitejs/vite/commit/b8ef8ff)) +* refactor: bounded plugins factory instead of create hook ([6b74221](https://github.com/vitejs/vite/commit/6b74221)) +* refactor: move reload on tsconfig change to the server ([1bcb67d](https://github.com/vitejs/vite/commit/1bcb67d)) + + + +## 6.0.0-alpha.8 (2024-04-30) + +* release: v6.0.0-alpha.8 ([1d9caef](https://github.com/vitejs/vite/commit/1d9caef)) +* fix: keep plugins with create hook ([12d467f](https://github.com/vitejs/vite/commit/12d467f)) +* refactor: environment hooks guard ([527621e](https://github.com/vitejs/vite/commit/527621e)) +* feat: define and html plugins ([7566aae](https://github.com/vitejs/vite/commit/7566aae)) + + + +## 6.0.0-alpha.7 (2024-04-29) + +* release: v6.0.0-alpha.7 ([05943cf](https://github.com/vitejs/vite/commit/05943cf)) +* feat: remove config.build from dynamicImportVars plugin ([8231283](https://github.com/vitejs/vite/commit/8231283)) +* feat: this.environment in buildStart, rework more internal plugins ([bda0dc5](https://github.com/vitejs/vite/commit/bda0dc5)) +* feat: this.environment in renderChunk and generateBundle ([a6fc1dd](https://github.com/vitejs/vite/commit/a6fc1dd)) + + + +## 6.0.0-alpha.6 (2024-04-28) + +* release: v6.0.0-alpha.6 ([e8473a6](https://github.com/vitejs/vite/commit/e8473a6)) +* fix: custom environment preload injection (#16541) ([00079da](https://github.com/vitejs/vite/commit/00079da)), closes [#16541](https://github.com/vitejs/vite/issues/16541) + + + +## 6.0.0-alpha.5 (2024-04-26) + +* release: v6.0.0-alpha.5 ([a8adcac](https://github.com/vitejs/vite/commit/a8adcac)) +* fix: use environment.plugins during build, support create hook ([a44810a](https://github.com/vitejs/vite/commit/a44810a)) + + + +## 6.0.0-alpha.4 (2024-04-26) + +* release: v6.0.0-alpha.4 ([e6fa9d9](https://github.com/vitejs/vite/commit/e6fa9d9)) +* fix: backcompat merging ([294a84e](https://github.com/vitejs/vite/commit/294a84e)) +* fix: disable hmr for ssrLoadModule ([33ef0fb](https://github.com/vitejs/vite/commit/33ef0fb)) +* fix: rework backcompat patching of environment config ([72150d6](https://github.com/vitejs/vite/commit/72150d6)) +* fix: sharedPlugins ([a63190c](https://github.com/vitejs/vite/commit/a63190c)) +* fix(cli): -> ([4cf322d](https://github.com/vitejs/vite/commit/4cf322d)) +* fix(v6): fix `ssrEmitAssets` compat (#16480) ([5c5efe4](https://github.com/vitejs/vite/commit/5c5efe4)), closes [#16480](https://github.com/vitejs/vite/issues/16480) +* feat: builder.entireApp, remove --environment ([1925eeb](https://github.com/vitejs/vite/commit/1925eeb)) +* feat: opt-in shared plugins during build ([2866d4f](https://github.com/vitejs/vite/commit/2866d4f)) +* feat: use environment.logger in buildEnvironment ([8843221](https://github.com/vitejs/vite/commit/8843221)) +* refactor: align createBuilder params with createServer ([62921e6](https://github.com/vitejs/vite/commit/62921e6)) +* refactor: build --app ([29dd26e](https://github.com/vitejs/vite/commit/29dd26e)) +* refactor: remove server from createServerModuleRunner ([9d6a152](https://github.com/vitejs/vite/commit/9d6a152)) +* refactor: rename createViteBuilder to createBuilder, align with createServer ([4dc2a75](https://github.com/vitejs/vite/commit/4dc2a75)) +* chore: remove the deprecation notice from ssrLoadModule for now ([bf65476](https://github.com/vitejs/vite/commit/bf65476)) + + + +## 6.0.0-alpha.3 (2024-04-20) + +* release: v6.0.0-alpha.3 ([635aad5](https://github.com/vitejs/vite/commit/635aad5)) +* fix: export missing types ([431cd4b](https://github.com/vitejs/vite/commit/431cd4b)) +* chore: remove configureServer from more plugins ([ad2b0bf](https://github.com/vitejs/vite/commit/ad2b0bf)) +* chore: rename plugin.split to plugin.create ([5f6b62f](https://github.com/vitejs/vite/commit/5f6b62f)) +* feat: environment api (#16129) ([f684d4c](https://github.com/vitejs/vite/commit/f684d4c)), closes [#16129](https://github.com/vitejs/vite/issues/16129) + + + +## 6.0.0-alpha.17 (2024-05-24) + +* refactor!: rename to HMRPayload to HotPayload and remove HMRBroadcaster (#16875) ([87bbb04](https://github.com/vitejs/vite/commit/87bbb04)), closes [#16875](https://github.com/vitejs/vite/issues/16875) +* fix: dep optimization options ([624e751](https://github.com/vitejs/vite/commit/624e751)) +* fix: DepOptimizationConfig ([5c655e6](https://github.com/vitejs/vite/commit/5c655e6)) +* fix: handleHotUpdate compat (#17295) ([11bddb0](https://github.com/vitejs/vite/commit/11bddb0)), closes [#17295](https://github.com/vitejs/vite/issues/17295) +* fix: optimizeDeps back compat ([478a9aa](https://github.com/vitejs/vite/commit/478a9aa)) +* fix(types): avoid referencing `WeakKey` type ([e339959](https://github.com/vitejs/vite/commit/e339959)) +* feat: per-environment optimizeDeps entries and force ([006cfb7](https://github.com/vitejs/vite/commit/006cfb7)) +* feat: reporter as shared plugin using createWeakData (#17293) ([2b69389](https://github.com/vitejs/vite/commit/2b69389)), closes [#17293](https://github.com/vitejs/vite/issues/17293) +* chore: refactor isDepsOptimizerEnabled ([5f36aa6](https://github.com/vitejs/vite/commit/5f36aa6)) + +## 6.0.0-alpha.16 (2024-05-23) + +* feat: createWeakData ([bcb1959](https://github.com/vitejs/vite/commit/bcb1959)) +* feat: improve Plugin option types ([21225c9](https://github.com/vitejs/vite/commit/21225c9)) +* feat: only shared plugins (#17289) ([e36f2f3](https://github.com/vitejs/vite/commit/e36f2f3)), closes [#17289](https://github.com/vitejs/vite/issues/17289) +* feat: provide `environment` in every hook context ([53734a8](https://github.com/vitejs/vite/commit/53734a8)) +* feat(types): expose PluginEnvironment type ([4d03124](https://github.com/vitejs/vite/commit/4d03124)) +* fix: argument rollup types for plugin context instead of wrapping ([89ec69c](https://github.com/vitejs/vite/commit/89ec69c)) +* fix: avoid duplicating values in shared optimizeDeps config (#16737) ([a3ee7f5](https://github.com/vitejs/vite/commit/a3ee7f5)), closes [#16737](https://github.com/vitejs/vite/issues/16737) +* fix: back compat for server.pluginContainer.buildStart ([46f21b8](https://github.com/vitejs/vite/commit/46f21b8)) +* fix: backcompat with config.optimizeDeps ([778e39d](https://github.com/vitejs/vite/commit/778e39d)) +* fix: keep plugin with environmentPlugins hook ([63de43a](https://github.com/vitejs/vite/commit/63de43a)) +* chore: remove extra symbol ([431455f](https://github.com/vitejs/vite/commit/431455f)) +* chore: rename to BaseEnvironment (#16797) ([d86553a](https://github.com/vitejs/vite/commit/d86553a)), closes [#16797](https://github.com/vitejs/vite/issues/16797) + + + +## 6.0.0-alpha.15 (2024-05-20) + +* fix: only join base url in none ssr env ([7e9dd25](https://github.com/vitejs/vite/commit/7e9dd25)) +* fix: svelte back compat ([0166bfe](https://github.com/vitejs/vite/commit/0166bfe)) +* fix hmr connected message on client ([acb208e](https://github.com/vitejs/vite/commit/acb208e)) + + + +## 6.0.0-alpha.14 (2024-05-20) + +* feat: back to environmentPlugins hook (#16732) ([54c219e](https://github.com/vitejs/vite/commit/54c219e)), closes [#16732](https://github.com/vitejs/vite/issues/16732) +* fix: export isFileLoadingAllowed ([34d8518](https://github.com/vitejs/vite/commit/34d8518)) +* fix: mention `build.rollupOptions.output.manualChunks` instead of `build.rollupOutput.manualChunks` ([89378c0](https://github.com/vitejs/vite/commit/89378c0)), closes [#16721](https://github.com/vitejs/vite/issues/16721) +* fix: rely on the current environment in resolveId, if known ([8ee7fcd](https://github.com/vitejs/vite/commit/8ee7fcd)) +* fix: revert resolveId importer require ([3546d1e](https://github.com/vitejs/vite/commit/3546d1e)) +* fix(build): make SystemJSWrapRE match lazy (#16633) ([6583ad2](https://github.com/vitejs/vite/commit/6583ad2)), closes [#16633](https://github.com/vitejs/vite/issues/16633) +* fix(css): avoid generating empty JS files when JS files becomes empty but has CSS files imported (#1 ([95fe5a7](https://github.com/vitejs/vite/commit/95fe5a7)), closes [#16078](https://github.com/vitejs/vite/issues/16078) +* fix(css): page reload was not happening with .css?raw (#16455) ([8041846](https://github.com/vitejs/vite/commit/8041846)), closes [#16455](https://github.com/vitejs/vite/issues/16455) +* fix(deps): update all non-major dependencies (#16603) ([6711553](https://github.com/vitejs/vite/commit/6711553)), closes [#16603](https://github.com/vitejs/vite/issues/16603) +* fix(deps): update all non-major dependencies (#16660) ([bf2f014](https://github.com/vitejs/vite/commit/bf2f014)), closes [#16660](https://github.com/vitejs/vite/issues/16660) +* fix(error-logging): rollup errors weren't displaying id and codeframe (#16540) ([22dc196](https://github.com/vitejs/vite/commit/22dc196)), closes [#16540](https://github.com/vitejs/vite/issues/16540) +* fix(hmr): trigger page reload when calling invalidate on root module (#16636) ([2b61cc3](https://github.com/vitejs/vite/commit/2b61cc3)), closes [#16636](https://github.com/vitejs/vite/issues/16636) +* fix(logger): truncate log over 5000 characters long (#16581) ([b0b839a](https://github.com/vitejs/vite/commit/b0b839a)), closes [#16581](https://github.com/vitejs/vite/issues/16581) +* fix(sourcemap): improve sourcemap compatibility for vue2 (#16594) ([913c040](https://github.com/vitejs/vite/commit/913c040)), closes [#16594](https://github.com/vitejs/vite/issues/16594) +* chore(deps): update all non-major dependencies (#16722) ([b45922a](https://github.com/vitejs/vite/commit/b45922a)), closes [#16722](https://github.com/vitejs/vite/issues/16722) +* docs: correct proxy shorthand example (#15938) ([abf766e](https://github.com/vitejs/vite/commit/abf766e)), closes [#15938](https://github.com/vitejs/vite/issues/15938) + + + +## 6.0.0-alpha.13 (2024-05-20) + +* fix: types ([82111bf](https://github.com/vitejs/vite/commit/82111bf)) + + + +## 6.0.0-alpha.12 (2024-05-20) + +* chore: export ssrTransform ([0b3cf69](https://github.com/vitejs/vite/commit/0b3cf69)) +* chore: respect ssr flag in clientInjectsions transform ([8c5674e](https://github.com/vitejs/vite/commit/8c5674e)) +* fix: remove enforce inside IsolatedPlugins ([a5efd56](https://github.com/vitejs/vite/commit/a5efd56)) + + + +## 6.0.0-alpha.11 (2024-05-04) + +* refactor: BoundedPlugin -> IsolatedPlugin ([1ec07a4](https://github.com/vitejs/vite/commit/1ec07a4)) + + + +## 6.0.0-alpha.10 (2024-05-02) + +* release: v6.0.0-alpha.10 ([57871a4](https://github.com/vitejs/vite/commit/57871a4)) +* feat: sharedDuringBuild for bounded plugins ([d213865](https://github.com/vitejs/vite/commit/d213865)) + + + +## 6.0.0-alpha.9 (2024-05-01) + +* release: v6.0.0-alpha.9 ([2e6abb3](https://github.com/vitejs/vite/commit/2e6abb3)) +* fix: lint ([848af54](https://github.com/vitejs/vite/commit/848af54)) +* fix: module-runner bundle issue with chokidar dep ([b8ef8ff](https://github.com/vitejs/vite/commit/b8ef8ff)) +* refactor: bounded plugins factory instead of create hook ([6b74221](https://github.com/vitejs/vite/commit/6b74221)) +* refactor: move reload on tsconfig change to the server ([1bcb67d](https://github.com/vitejs/vite/commit/1bcb67d)) + + + +## 6.0.0-alpha.8 (2024-04-30) + +* release: v6.0.0-alpha.8 ([1d9caef](https://github.com/vitejs/vite/commit/1d9caef)) +* fix: keep plugins with create hook ([12d467f](https://github.com/vitejs/vite/commit/12d467f)) +* refactor: environment hooks guard ([527621e](https://github.com/vitejs/vite/commit/527621e)) +* feat: define and html plugins ([7566aae](https://github.com/vitejs/vite/commit/7566aae)) + + + +## 6.0.0-alpha.7 (2024-04-29) + +* release: v6.0.0-alpha.7 ([05943cf](https://github.com/vitejs/vite/commit/05943cf)) +* feat: remove config.build from dynamicImportVars plugin ([8231283](https://github.com/vitejs/vite/commit/8231283)) +* feat: this.environment in buildStart, rework more internal plugins ([bda0dc5](https://github.com/vitejs/vite/commit/bda0dc5)) +* feat: this.environment in renderChunk and generateBundle ([a6fc1dd](https://github.com/vitejs/vite/commit/a6fc1dd)) + + + +## 6.0.0-alpha.6 (2024-04-28) + +* release: v6.0.0-alpha.6 ([e8473a6](https://github.com/vitejs/vite/commit/e8473a6)) +* fix: custom environment preload injection (#16541) ([00079da](https://github.com/vitejs/vite/commit/00079da)), closes [#16541](https://github.com/vitejs/vite/issues/16541) + + + +## 6.0.0-alpha.5 (2024-04-26) + +* release: v6.0.0-alpha.5 ([a8adcac](https://github.com/vitejs/vite/commit/a8adcac)) +* fix: use environment.plugins during build, support create hook ([a44810a](https://github.com/vitejs/vite/commit/a44810a)) + + + +## 6.0.0-alpha.4 (2024-04-26) + +* release: v6.0.0-alpha.4 ([e6fa9d9](https://github.com/vitejs/vite/commit/e6fa9d9)) +* fix: backcompat merging ([294a84e](https://github.com/vitejs/vite/commit/294a84e)) +* fix: disable hmr for ssrLoadModule ([33ef0fb](https://github.com/vitejs/vite/commit/33ef0fb)) +* fix: rework backcompat patching of environment config ([72150d6](https://github.com/vitejs/vite/commit/72150d6)) +* fix: sharedPlugins ([a63190c](https://github.com/vitejs/vite/commit/a63190c)) +* fix(cli): -> ([4cf322d](https://github.com/vitejs/vite/commit/4cf322d)) +* fix(v6): fix `ssrEmitAssets` compat (#16480) ([5c5efe4](https://github.com/vitejs/vite/commit/5c5efe4)), closes [#16480](https://github.com/vitejs/vite/issues/16480) +* feat: builder.entireApp, remove --environment ([1925eeb](https://github.com/vitejs/vite/commit/1925eeb)) +* feat: opt-in shared plugins during build ([2866d4f](https://github.com/vitejs/vite/commit/2866d4f)) +* feat: use environment.logger in buildEnvironment ([8843221](https://github.com/vitejs/vite/commit/8843221)) +* refactor: align createBuilder params with createServer ([62921e6](https://github.com/vitejs/vite/commit/62921e6)) +* refactor: build --app ([29dd26e](https://github.com/vitejs/vite/commit/29dd26e)) +* refactor: remove server from createServerModuleRunner ([9d6a152](https://github.com/vitejs/vite/commit/9d6a152)) +* refactor: rename createViteBuilder to createBuilder, align with createServer ([4dc2a75](https://github.com/vitejs/vite/commit/4dc2a75)) +* chore: remove the deprecation notice from ssrLoadModule for now ([bf65476](https://github.com/vitejs/vite/commit/bf65476)) + + + +## 6.0.0-alpha.3 (2024-04-20) + +* release: v6.0.0-alpha.3 ([635aad5](https://github.com/vitejs/vite/commit/635aad5)) +* fix: export missing types ([431cd4b](https://github.com/vitejs/vite/commit/431cd4b)) +* chore: remove configureServer from more plugins ([ad2b0bf](https://github.com/vitejs/vite/commit/ad2b0bf)) +* chore: rename plugin.split to plugin.create ([5f6b62f](https://github.com/vitejs/vite/commit/5f6b62f)) +* feat: environment api (#16129) ([f684d4c](https://github.com/vitejs/vite/commit/f684d4c)), closes [#16129](https://github.com/vitejs/vite/issues/16129) + + + +## 6.0.0-alpha.10 (2024-05-02) + +* feat: sharedDuringBuild for bounded plugins ([d213865](https://github.com/vitejs/vite/commit/d213865)) + + + +## 6.0.0-alpha.9 (2024-05-01) + +* fix: lint ([848af54](https://github.com/vitejs/vite/commit/848af54)) +* fix: module-runner bundle issue with chokidar dep ([b8ef8ff](https://github.com/vitejs/vite/commit/b8ef8ff)) +* fix: scripts and styles were missing from built HTML on Windows (#16421) ([0e93f58](https://github.com/vitejs/vite/commit/0e93f58)), closes [#16421](https://github.com/vitejs/vite/issues/16421) +* fix(deps): update all non-major dependencies (#16549) ([2d6a13b](https://github.com/vitejs/vite/commit/2d6a13b)), closes [#16549](https://github.com/vitejs/vite/issues/16549) +* fix(preload): skip preload for non-static urls (#16556) ([bb79c9b](https://github.com/vitejs/vite/commit/bb79c9b)), closes [#16556](https://github.com/vitejs/vite/issues/16556) +* fix(ssr): handle class declaration and expression name scoping (#16569) ([c071eb3](https://github.com/vitejs/vite/commit/c071eb3)), closes [#16569](https://github.com/vitejs/vite/issues/16569) +* fix(ssr): handle function expression name scoping (#16563) ([02db947](https://github.com/vitejs/vite/commit/02db947)), closes [#16563](https://github.com/vitejs/vite/issues/16563) +* refactor: bounded plugins factory instead of create hook ([6b74221](https://github.com/vitejs/vite/commit/6b74221)) +* refactor: move reload on tsconfig change to the server ([1bcb67d](https://github.com/vitejs/vite/commit/1bcb67d)) + + + +## 6.0.0-alpha.8 (2024-04-30) + +* fix: keep plugins with create hook ([12d467f](https://github.com/vitejs/vite/commit/12d467f)) +* refactor: environment hooks guard ([527621e](https://github.com/vitejs/vite/commit/527621e)) +* feat: define and html plugins ([7566aae](https://github.com/vitejs/vite/commit/7566aae)) + + + +## 6.0.0-alpha.7 (2024-04-29) + +* feat: remove config.build from dynamicImportVars plugin ([8231283](https://github.com/vitejs/vite/commit/8231283)) +* feat: this.environment in buildStart, rework more internal plugins ([bda0dc5](https://github.com/vitejs/vite/commit/bda0dc5)) +* feat: this.environment in renderChunk and generateBundle ([a6fc1dd](https://github.com/vitejs/vite/commit/a6fc1dd)) + + + +## 6.0.0-alpha.6 (2024-04-28) + +* fix: custom environment preload injection (#16541) ([00079da](https://github.com/vitejs/vite/commit/00079da)), closes [#16541](https://github.com/vitejs/vite/issues/16541) + + + +## 6.0.0-alpha.5 (2024-04-26) + +* fix: use environment.plugins during build, support create hook ([a44810a](https://github.com/vitejs/vite/commit/a44810a)) + + + +## 6.0.0-alpha.4 (2024-04-26) + +* fix: backcompat merging ([294a84e](https://github.com/vitejs/vite/commit/294a84e)) +* fix: disable hmr for ssrLoadModule ([33ef0fb](https://github.com/vitejs/vite/commit/33ef0fb)) +* fix: dynamic-import-vars plugin normalize path issue (#16518) ([f71ba5b](https://github.com/vitejs/vite/commit/f71ba5b)), closes [#16518](https://github.com/vitejs/vite/issues/16518) +* fix: rework backcompat patching of environment config ([72150d6](https://github.com/vitejs/vite/commit/72150d6)) +* fix: sharedPlugins ([a63190c](https://github.com/vitejs/vite/commit/a63190c)) +* fix(cli): -> ([4cf322d](https://github.com/vitejs/vite/commit/4cf322d)) +* fix(deps): update all non-major dependencies (#16488) ([2d50be2](https://github.com/vitejs/vite/commit/2d50be2)), closes [#16488](https://github.com/vitejs/vite/issues/16488) +* fix(dev): watch publicDir explicitly to include it outside the root (#16502) ([4d83eb5](https://github.com/vitejs/vite/commit/4d83eb5)), closes [#16502](https://github.com/vitejs/vite/issues/16502) +* fix(v6): fix `ssrEmitAssets` compat (#16480) ([5c5efe4](https://github.com/vitejs/vite/commit/5c5efe4)), closes [#16480](https://github.com/vitejs/vite/issues/16480) +* feat: builder.entireApp, remove --environment ([1925eeb](https://github.com/vitejs/vite/commit/1925eeb)) +* feat: opt-in shared plugins during build ([2866d4f](https://github.com/vitejs/vite/commit/2866d4f)) +* feat: use environment.logger in buildEnvironment ([8843221](https://github.com/vitejs/vite/commit/8843221)) +* refactor: align createBuilder params with createServer ([62921e6](https://github.com/vitejs/vite/commit/62921e6)) +* refactor: build --app ([29dd26e](https://github.com/vitejs/vite/commit/29dd26e)) +* refactor: remove server from createServerModuleRunner ([9d6a152](https://github.com/vitejs/vite/commit/9d6a152)) +* refactor: rename createViteBuilder to createBuilder, align with createServer ([4dc2a75](https://github.com/vitejs/vite/commit/4dc2a75)) +* chore: remove the deprecation notice from ssrLoadModule for now ([bf65476](https://github.com/vitejs/vite/commit/bf65476)) + + + +## 6.0.0-alpha.3 (2024-04-20) + +* release: v6.0.0-alpha.3 ([635aad5](https://github.com/vitejs/vite/commit/635aad5)) +* fix: export missing types ([431cd4b](https://github.com/vitejs/vite/commit/431cd4b)) +* chore: remove configureServer from more plugins ([ad2b0bf](https://github.com/vitejs/vite/commit/ad2b0bf)) +* chore: rename plugin.split to plugin.create ([5f6b62f](https://github.com/vitejs/vite/commit/5f6b62f)) +* feat: environment api (#16129) ([f684d4c](https://github.com/vitejs/vite/commit/f684d4c)), closes [#16129](https://github.com/vitejs/vite/issues/16129) + + + +## 6.0.0-alpha.3 (2024-04-20) + +* fix: add base to virtual html (#16442) ([721f94d](https://github.com/vitejs/vite/commit/721f94d)), closes [#16442](https://github.com/vitejs/vite/issues/16442) +* fix: adjust esm syntax judgment logic (#16436) ([af72eab](https://github.com/vitejs/vite/commit/af72eab)), closes [#16436](https://github.com/vitejs/vite/issues/16436) +* fix: don't add outDirs to watch.ignored if emptyOutDir is false (#16453) ([6a127d6](https://github.com/vitejs/vite/commit/6a127d6)), closes [#16453](https://github.com/vitejs/vite/issues/16453) +* fix: export missing types ([431cd4b](https://github.com/vitejs/vite/commit/431cd4b)) +* fix(cspNonce): don't overwrite existing nonce values (#16415) ([b872635](https://github.com/vitejs/vite/commit/b872635)), closes [#16415](https://github.com/vitejs/vite/issues/16415) +* chore: remove configureServer from more plugins ([ad2b0bf](https://github.com/vitejs/vite/commit/ad2b0bf)) +* chore: rename plugin.split to plugin.create ([5f6b62f](https://github.com/vitejs/vite/commit/5f6b62f)) +* chore(deps): update dependency eslint-plugin-n to v17 (#16381) ([6cccef7](https://github.com/vitejs/vite/commit/6cccef7)), closes [#16381](https://github.com/vitejs/vite/issues/16381) +* feat: environment api (#16129) ([f684d4c](https://github.com/vitejs/vite/commit/f684d4c)), closes [#16129](https://github.com/vitejs/vite/issues/16129) +* feat: show warning if root is in build.outDir (#16454) ([11444dc](https://github.com/vitejs/vite/commit/11444dc)), closes [#16454](https://github.com/vitejs/vite/issues/16454) +* feat: write cspNonce to style tags (#16419) ([8e54bbd](https://github.com/vitejs/vite/commit/8e54bbd)), closes [#16419](https://github.com/vitejs/vite/issues/16419) + + + +## 6.0.0-alpha.2 (2024-04-09) + +* chore: update ([46c8910](https://github.com/vitejs/vite/commit/46c8910)) +* feat: environment aware define ([9f9a716](https://github.com/vitejs/vite/commit/9f9a716)) +* feat: rework more ssr.target webworker branches ([1f644d0](https://github.com/vitejs/vite/commit/1f644d0)) + + + +## 6.0.0-alpha.1 (2024-04-08) + +* fix: `fsp.rm` removing files does not take effect (#16032) ([b05c405](https://github.com/vitejs/vite/commit/b05c405)), closes [#16032](https://github.com/vitejs/vite/issues/16032) +* fix: csp nonce injection when no closing tag (#16281) (#16282) ([67a74f8](https://github.com/vitejs/vite/commit/67a74f8)), closes [#16281](https://github.com/vitejs/vite/issues/16281) [#16282](https://github.com/vitejs/vite/issues/16282) +* fix: do not access document in `/@vite/client` when not defined (#16318) ([6c5536b](https://github.com/vitejs/vite/commit/6c5536b)), closes [#16318](https://github.com/vitejs/vite/issues/16318) +* fix: fix sourcemap when using object as `define` value (#15805) ([9699ba3](https://github.com/vitejs/vite/commit/9699ba3)), closes [#15805](https://github.com/vitejs/vite/issues/15805) +* fix: package types ([bdf13bb](https://github.com/vitejs/vite/commit/bdf13bb)) +* fix(deps): update all non-major dependencies (#16376) ([58a2938](https://github.com/vitejs/vite/commit/58a2938)), closes [#16376](https://github.com/vitejs/vite/issues/16376) +* fix(environment): use `environments.client.build.outDir` for preview (#16301) ([8621c3f](https://github.com/vitejs/vite/commit/8621c3f)), closes [#16301](https://github.com/vitejs/vite/issues/16301) +* feat: async createEnvironment ([d15a157](https://github.com/vitejs/vite/commit/d15a157)) +* feat: dedupe/preserveSymlinks ([3ba9214](https://github.com/vitejs/vite/commit/3ba9214)) +* refactor: environment.dev.recoverable ([ea1c7eb](https://github.com/vitejs/vite/commit/ea1c7eb)) +* refactor: isFileServingAllowed load fallback for SSR ([d91714b](https://github.com/vitejs/vite/commit/d91714b)) +* refactor: lib options ([70731ce](https://github.com/vitejs/vite/commit/70731ce)) +* chore: merge ([bcac048](https://github.com/vitejs/vite/commit/bcac048)) +* chore: merge ([833dabf](https://github.com/vitejs/vite/commit/833dabf)) +* chore: remove ssr.target use ([0ea8be9](https://github.com/vitejs/vite/commit/0ea8be9)) +* chore: remove ssrConfig ([27371dc](https://github.com/vitejs/vite/commit/27371dc)) +* chore: update region comment (#16380) ([77562c3](https://github.com/vitejs/vite/commit/77562c3)), closes [#16380](https://github.com/vitejs/vite/issues/16380) +* chore(deps): update all non-major dependencies (#16325) ([c7efec4](https://github.com/vitejs/vite/commit/c7efec4)), closes [#16325](https://github.com/vitejs/vite/issues/16325) +* perf: reduce size of injected __vite__mapDeps code (#16184) ([a9bf430](https://github.com/vitejs/vite/commit/a9bf430)), closes [#16184](https://github.com/vitejs/vite/issues/16184) +* perf: reduce size of injected __vite__mapDeps code (#16184) ([c0ec6be](https://github.com/vitejs/vite/commit/c0ec6be)), closes [#16184](https://github.com/vitejs/vite/issues/16184) +* perf(css): only replace empty chunk if imported (#16349) ([f61d8b1](https://github.com/vitejs/vite/commit/f61d8b1)), closes [#16349](https://github.com/vitejs/vite/issues/16349) + + + +## 6.0.0-alpha.0 (2024-04-05) + +* feat: abstract moduleGraph into ModuleExecutionEnvironment ([5f5e0ec](https://github.com/vitejs/vite/commit/5f5e0ec)) +* feat: add `hot` property to environments ([e966ba0](https://github.com/vitejs/vite/commit/e966ba0)) +* feat: build.ssrEmitAssets -> build.emitAssets ([ef8c9b9](https://github.com/vitejs/vite/commit/ef8c9b9)) +* feat: builder config, runBuildTasks option ([f4789a3](https://github.com/vitejs/vite/commit/f4789a3)) +* feat: configureDevEnvironments + configureBuildEnvironments ([88fea3b](https://github.com/vitejs/vite/commit/88fea3b)) +* feat: environment aware createIdResolver ([f1dcd2c](https://github.com/vitejs/vite/commit/f1dcd2c)) +* feat: environment aware createResolver and resolvePlugin ([dd6332e](https://github.com/vitejs/vite/commit/dd6332e)) +* feat: environment aware depsOptimizer ([a7e52aa](https://github.com/vitejs/vite/commit/a7e52aa)) +* feat: environment id resolver for css plugin ([0bec1b9](https://github.com/vitejs/vite/commit/0bec1b9)) +* feat: environment in hooks, context vs param (#16261) ([fbe6361](https://github.com/vitejs/vite/commit/fbe6361)), closes [#16261](https://github.com/vitejs/vite/issues/16261) +* feat: environment.transformRequest ([fcebb7d](https://github.com/vitejs/vite/commit/fcebb7d)) +* feat: inject environment in build hooks ([cef1091](https://github.com/vitejs/vite/commit/cef1091)) +* feat: separate module graphs per environment ([83068fe](https://github.com/vitejs/vite/commit/83068fe)) +* feat: server.runHmrTasks ([7f94c03](https://github.com/vitejs/vite/commit/7f94c03)) +* feat: ssr.external/noExternal -> resolve.external/noExternal ([2a0b524](https://github.com/vitejs/vite/commit/2a0b524)) +* feat: ssr.target -> environment.webCompatible ([1a7d290](https://github.com/vitejs/vite/commit/1a7d290)) +* feat: support transport options to communicate between the environment and the runner (#16209) ([dbcc375](https://github.com/vitejs/vite/commit/dbcc375)), closes [#16209](https://github.com/vitejs/vite/issues/16209) +* feat: vite runtime renamed to module runner (#16137) ([60f7f2b](https://github.com/vitejs/vite/commit/60f7f2b)), closes [#16137](https://github.com/vitejs/vite/issues/16137) +* feat(hmr): call `hotUpdate` hook with file create/delete (#16249) ([3d37ac1](https://github.com/vitejs/vite/commit/3d37ac1)), closes [#16249](https://github.com/vitejs/vite/issues/16249) +* refactor: allow custom connections in node module runner ([9005841](https://github.com/vitejs/vite/commit/9005841)) +* refactor: base environment.config + environment.options ([c7e4da2](https://github.com/vitejs/vite/commit/c7e4da2)) +* refactor: buildEnvironments + hmrEnvironments ([c1fc111](https://github.com/vitejs/vite/commit/c1fc111)) +* refactor: clientEnvironment instead of browserEnvironment (#16194) ([ccf3de4](https://github.com/vitejs/vite/commit/ccf3de4)), closes [#16194](https://github.com/vitejs/vite/issues/16194) +* refactor: configEnvironment hook + enviroment config resolving ([fee54ea](https://github.com/vitejs/vite/commit/fee54ea)) +* refactor: environment id,type -> name + fixes ([29f1b7b](https://github.com/vitejs/vite/commit/29f1b7b)) +* refactor: environments array to plain object ([a7a06fe](https://github.com/vitejs/vite/commit/a7a06fe)) +* refactor: environments as array instead of map (#16193) ([f1d660c](https://github.com/vitejs/vite/commit/f1d660c)), closes [#16193](https://github.com/vitejs/vite/issues/16193) +* refactor: hooks get an environment object instead of a string ([5e60d8a](https://github.com/vitejs/vite/commit/5e60d8a)) +* refactor: hooks to config for creating environments ([3e6216c](https://github.com/vitejs/vite/commit/3e6216c)) +* refactor: isolate back compat module graph in its own module ([8000e8e](https://github.com/vitejs/vite/commit/8000e8e)) +* refactor: ModuleExecutionEnvironment -> DevEnvironment ([6e71b24](https://github.com/vitejs/vite/commit/6e71b24)) +* refactor: move safeModulesPath set to server ([95ae29b](https://github.com/vitejs/vite/commit/95ae29b)) +* refactor: move transport to properties ([9cfa916](https://github.com/vitejs/vite/commit/9cfa916)) +* refactor: node -> ssr for default environment ([e03bac8](https://github.com/vitejs/vite/commit/e03bac8)) +* refactor: options and environment are required when calling container.hook ([e30b858](https://github.com/vitejs/vite/commit/e30b858)) +* refactor: pass down name to the environment factory ([52edfc9](https://github.com/vitejs/vite/commit/52edfc9)) +* refactor: remove default nodeModuleRunner because it's not used anywhere ([f29e95a](https://github.com/vitejs/vite/commit/f29e95a)) +* refactor: remove environment name from the hmr context ([a183a0f](https://github.com/vitejs/vite/commit/a183a0f)) +* refactor: rename "hmrEnvironments" to "hotUpdateEnvironments" ([a0b7edb](https://github.com/vitejs/vite/commit/a0b7edb)) +* refactor: rename createSsrEnvironment to createNodeEnvironment ([c9abcfc](https://github.com/vitejs/vite/commit/c9abcfc)) +* refactor: rename ssrInvalidates to invalidates ([72fe84e](https://github.com/vitejs/vite/commit/72fe84e)) +* refactor: rework resolveId in ModuleExecutionEnvironment constructor ([03d3889](https://github.com/vitejs/vite/commit/03d3889)) +* refactor: ssrConfig.optimizeDeps.include/exclude ([5bd8e95](https://github.com/vitejs/vite/commit/5bd8e95)) +* refactor: use ssr environment module graph in ssrFixStacktrace ([5477972](https://github.com/vitejs/vite/commit/5477972)) +* fix: add auto complete to server.environments ([a160a1b](https://github.com/vitejs/vite/commit/a160a1b)) +* fix: call updateModules for each environmnet ([281cf97](https://github.com/vitejs/vite/commit/281cf97)) +* fix: fine-grained hmr ([31e1d3a](https://github.com/vitejs/vite/commit/31e1d3a)) +* fix: HotContext only gets ModuleExecutionEnvironment ([30be775](https://github.com/vitejs/vite/commit/30be775)) +* fix: injectEnvironmentInContext ([a1d385c](https://github.com/vitejs/vite/commit/a1d385c)) +* fix: injectEnvironmentToHooks ([681ccd4](https://github.com/vitejs/vite/commit/681ccd4)) +* fix: missing externalConditions back compat ([beb40ef](https://github.com/vitejs/vite/commit/beb40ef)) +* fix: optimizeDeps backward compatibility layer ([3806fe6](https://github.com/vitejs/vite/commit/3806fe6)) +* fix: partial backward compat for config.ssr ([85ada0d](https://github.com/vitejs/vite/commit/85ada0d)) +* fix: resolve.externalConditions ([fb9365c](https://github.com/vitejs/vite/commit/fb9365c)) +* fix: use "register" event for remote environment transport ([c4f4dfb](https://github.com/vitejs/vite/commit/c4f4dfb)) +* fix(css): unknown file error happened with lightningcss (#16306) ([01af308](https://github.com/vitejs/vite/commit/01af308)), closes [#16306](https://github.com/vitejs/vite/issues/16306) +* fix(hmr): multiple updates happened when invalidate is called while multiple tabs open (#16307) ([21cc10b](https://github.com/vitejs/vite/commit/21cc10b)), closes [#16307](https://github.com/vitejs/vite/issues/16307) +* fix(scanner): duplicate modules for same id if glob is used in html-like types (#16305) ([eca68fa](https://github.com/vitejs/vite/commit/eca68fa)), closes [#16305](https://github.com/vitejs/vite/issues/16305) +* test: add test for worker transport ([a5ef42e](https://github.com/vitejs/vite/commit/a5ef42e)) +* test: fix after merge ([d9ed857](https://github.com/vitejs/vite/commit/d9ed857)) +* test(environment): add environment playground (#16299) ([a5c7e4f](https://github.com/vitejs/vite/commit/a5c7e4f)), closes [#16299](https://github.com/vitejs/vite/issues/16299) +* chore: fix lint ([b4e46fe](https://github.com/vitejs/vite/commit/b4e46fe)) +* chore: fix lint ([6040ab3](https://github.com/vitejs/vite/commit/6040ab3)) +* chore: lint ([8785f4f](https://github.com/vitejs/vite/commit/8785f4f)) +* chore: lint ([92eccf9](https://github.com/vitejs/vite/commit/92eccf9)) +* chore: lint ([f927702](https://github.com/vitejs/vite/commit/f927702)) +* chore: rename module and error back to ssrModule and ssrError ([d8ff12a](https://github.com/vitejs/vite/commit/d8ff12a)) +* chore: rename server environment to node environment ([4808b27](https://github.com/vitejs/vite/commit/4808b27)) +* chore: run prettier on environment file ([1fe63b1](https://github.com/vitejs/vite/commit/1fe63b1)) +* chore: update ([9a600fe](https://github.com/vitejs/vite/commit/9a600fe)) +* chore: update environment.server.config ([2ddf28e](https://github.com/vitejs/vite/commit/2ddf28e)) +* wip: environment config overrides ([81abf6e](https://github.com/vitejs/vite/commit/81abf6e)) + + + ## 5.3.0-beta.0 (2024-05-30) * fix: adjust import analysis behavior to match Node (#16738) ([f599ab4](https://github.com/vitejs/vite/commit/f599ab4)), closes [#16738](https://github.com/vitejs/vite/issues/16738) @@ -38,8 +587,6 @@ * docs: correct proxy shorthand example (#15938) ([abf766e](https://github.com/vitejs/vite/commit/abf766e)), closes [#15938](https://github.com/vitejs/vite/issues/15938) * docs: deprecate server.hot (#16741) ([e7d38ab](https://github.com/vitejs/vite/commit/e7d38ab)), closes [#16741](https://github.com/vitejs/vite/issues/16741) - - ## 5.2.11 (2024-05-02) * feat: improve dynamic import variable failure error message (#16519) ([f8feeea](https://github.com/vitejs/vite/commit/f8feeea)), closes [#16519](https://github.com/vitejs/vite/issues/16519) @@ -78,6 +625,18 @@ +## 5.2.8 (2024-04-03) + +* release: v5.2.8 ([8b8d402](https://github.com/vitejs/vite/commit/8b8d402)) +* fix: csp nonce injection when no closing tag (#16281) (#16282) ([3c85c6b](https://github.com/vitejs/vite/commit/3c85c6b)), closes [#16281](https://github.com/vitejs/vite/issues/16281) [#16282](https://github.com/vitejs/vite/issues/16282) +* fix: do not access document in `/@vite/client` when not defined (#16318) ([646319c](https://github.com/vitejs/vite/commit/646319c)), closes [#16318](https://github.com/vitejs/vite/issues/16318) +* fix: fix sourcemap when using object as `define` value (#15805) ([445c4f2](https://github.com/vitejs/vite/commit/445c4f2)), closes [#15805](https://github.com/vitejs/vite/issues/15805) +* chore(deps): update all non-major dependencies (#16325) ([a78e265](https://github.com/vitejs/vite/commit/a78e265)), closes [#16325](https://github.com/vitejs/vite/issues/16325) +* refactor: use types from sass instead of @types/sass (#16340) ([4581e83](https://github.com/vitejs/vite/commit/4581e83)), closes [#16340](https://github.com/vitejs/vite/issues/16340) + + + + ## 5.2.8 (2024-04-03) * fix: csp nonce injection when no closing tag (#16281) (#16282) ([3c85c6b](https://github.com/vitejs/vite/commit/3c85c6b)), closes [#16281](https://github.com/vitejs/vite/issues/16281) [#16282](https://github.com/vitejs/vite/issues/16282) @@ -1794,7 +2353,7 @@ Vite is now using [Rollup 3](https://github.com/vitejs/vite/issues/9870), which The modern browser build now targets `safari14` by default for wider ES2020 compatibility (https://github.com/vitejs/vite/issues/9063). This means that modern builds can now use [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) and that the [nullish coallessing operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) isn't transpiled anymore. If you need to support older browsers, you can add [`@vitejs/plugin-legacy`](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy) as usual. -#### Importing CSS as a string +#### Importing CSS as a string In Vite 3, importing the default export of a `.css` file could introduce a double loading of CSS. @@ -1988,7 +2547,7 @@ See [4.0.0-alpha.0 changelog](https://github.com/vitejs/vite/blob/v4.0.0-alpha.0 * fix: escape msg in render restricted error html, backport (#12889) (#12892) ([b48ac2a](https://github.com/vitejs/vite/commit/b48ac2a)), closes [#12889](https://github.com/vitejs/vite/issues/12889) [#12892](https://github.com/vitejs/vite/issues/12892) - + ## 3.2.5 (2022-12-05) * chore: cherry pick more v4 bug fixes to v3 (#11189) ([eba9b42](https://github.com/vitejs/vite/commit/eba9b42)), closes [#11189](https://github.com/vitejs/vite/issues/11189) [#10949](https://github.com/vitejs/vite/issues/10949) [#11056](https://github.com/vitejs/vite/issues/11056) [#8663](https://github.com/vitejs/vite/issues/8663) [#10958](https://github.com/vitejs/vite/issues/10958) [#11120](https://github.com/vitejs/vite/issues/11120) [#11122](https://github.com/vitejs/vite/issues/11122) [#11123](https://github.com/vitejs/vite/issues/11123) [#11132](https://github.com/vitejs/vite/issues/11132) @@ -2071,7 +2630,7 @@ Laravel and other backends integrations will now get entries for every asset fil #### Customizable ErrorOverlay You can now customize the ErrorOverlay by using [css parts](https://developer.mozilla.org/en-US/docs/Web/CSS/::part). Check out the PR for more details: [#10234](https://github.com/vitejs/vite/issues/10234). - + ### Features * feat(build): experimental copyPublicDir option (#10550) ([4f4a39f](https://github.com/vitejs/vite/commit/4f4a39f)), closes [#10550](https://github.com/vitejs/vite/issues/10550) diff --git a/packages/vite/index.cjs b/packages/vite/index.cjs index ef2a182045698e..7e5fec44e90637 100644 --- a/packages/vite/index.cjs +++ b/packages/vite/index.cjs @@ -6,7 +6,6 @@ warnCjsUsage() module.exports.defineConfig = (config) => config // proxy cjs utils (sync functions) -// eslint-disable-next-line n/no-missing-require -- will be generated by build Object.assign(module.exports, require('./dist/node-cjs/publicUtils.cjs')) // async functions, can be redirect from ESM build diff --git a/packages/vite/package.json b/packages/vite/package.json index 9900d3fabc027c..21a941bd741334 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "5.3.0-beta.0", + "version": "6.0.0-alpha.18", "type": "module", "license": "MIT", "author": "Evan You", @@ -32,9 +32,9 @@ "./client": { "types": "./client.d.ts" }, - "./runtime": { - "types": "./dist/node/runtime.d.ts", - "import": "./dist/node/runtime.js" + "./module-runner": { + "types": "./dist/node/module-runner.d.ts", + "import": "./dist/node/module-runner.js" }, "./dist/client/*": "./dist/client/*", "./types/*": { @@ -44,8 +44,8 @@ }, "typesVersions": { "*": { - "runtime": [ - "dist/node/runtime.d.ts" + "module-runner": [ + "dist/node/module-runner.d.ts" ] } }, @@ -78,7 +78,7 @@ "build-types-temp": "tsc --emitDeclarationOnly --outDir temp -p src/node", "build-types-roll": "rollup --config rollup.dts.config.ts --configPlugin typescript && rimraf temp", "build-types-check": "tsc --project tsconfig.check.json", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && tsc --noEmit -p src/node", "lint": "eslint --cache --ext .ts src/**", "format": "prettier --write --cache --parser typescript \"src/**/*.ts\"", "prepublishOnly": "npm run build" diff --git a/packages/vite/rollup.config.ts b/packages/vite/rollup.config.ts index 2d14eace0eea9c..a83fcc5e62f525 100644 --- a/packages/vite/rollup.config.ts +++ b/packages/vite/rollup.config.ts @@ -177,11 +177,11 @@ function createNodeConfig(isProduction: boolean) { }) } -function createRuntimeConfig(isProduction: boolean) { +function createModuleRunnerConfig(isProduction: boolean) { return defineConfig({ ...sharedNodeOptions, input: { - runtime: path.resolve(__dirname, 'src/runtime/index.ts'), + 'module-runner': path.resolve(__dirname, 'src/module-runner/index.ts'), }, output: { ...sharedNodeOptions.output, @@ -202,7 +202,7 @@ function createRuntimeConfig(isProduction: boolean) { isProduction ? false : './dist/node', ), esbuildMinifyPlugin({ minify: false, minifySyntax: true }), - bundleSizeLimit(45), + bundleSizeLimit(47), ], }) } @@ -240,7 +240,7 @@ export default (commandLineArgs: any): RollupOptions[] => { envConfig, clientConfig, createNodeConfig(isProduction), - createRuntimeConfig(isProduction), + createModuleRunnerConfig(isProduction), createCjsConfig(isProduction), ]) } @@ -332,10 +332,10 @@ const __require = require; name: 'cjs-chunk-patch', renderChunk(code, chunk) { if (!chunk.fileName.includes('chunks/dep-')) return - // don't patch runtime utils chunk because it should stay lightweight and we know it doesn't use require + // don't patch runner utils chunk because it should stay lightweight and we know it doesn't use require if ( chunk.name === 'utils' && - chunk.moduleIds.some((id) => id.endsWith('/ssr/runtime/utils.ts')) + chunk.moduleIds.some((id) => id.endsWith('/ssr/module-runner/utils.ts')) ) return const match = code.match(/^(?:import[\s\S]*?;\s*)+/) diff --git a/packages/vite/rollup.dts.config.ts b/packages/vite/rollup.dts.config.ts index 41b1da1458a31e..1848c2c2b5ba04 100644 --- a/packages/vite/rollup.dts.config.ts +++ b/packages/vite/rollup.dts.config.ts @@ -25,7 +25,7 @@ const external = [ export default defineConfig({ input: { index: './temp/node/index.d.ts', - runtime: './temp/runtime/index.d.ts', + 'module-runner': './temp/module-runner/index.d.ts', }, output: { dir: './dist/node', @@ -48,6 +48,8 @@ const identifierWithTrailingDollarRE = /\b(\w+)\$\d+\b/g const identifierReplacements: Record> = { rollup: { Plugin$1: 'rollup.Plugin', + PluginContext$1: 'rollup.PluginContext', + TransformPluginContext$1: 'rollup.TransformPluginContext', TransformResult$2: 'rollup.TransformResult', }, esbuild: { @@ -91,10 +93,10 @@ function patchTypes(): Plugin { }, renderChunk(code, chunk) { if ( - chunk.fileName.startsWith('runtime') || + chunk.fileName.startsWith('module-runner') || chunk.fileName.startsWith('types.d-') ) { - validateRuntimeChunk.call(this, chunk) + validateRunnerChunk.call(this, chunk) } else { validateChunkImports.call(this, chunk) code = replaceConfusingTypeNames.call(this, code, chunk) @@ -107,9 +109,9 @@ function patchTypes(): Plugin { } /** - * Runtime chunk should only import local dependencies to stay lightweight + * Runner chunk should only import local dependencies to stay lightweight */ -function validateRuntimeChunk(this: PluginContext, chunk: RenderedChunk) { +function validateRunnerChunk(this: PluginContext, chunk: RenderedChunk) { for (const id of chunk.imports) { if ( !id.startsWith('./') && diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 8fcb145fd0242c..88e2ad8e39875c 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -1,4 +1,4 @@ -import type { ErrorPayload, HMRPayload } from 'types/hmrPayload' +import type { ErrorPayload, HotPayload } from 'types/hotPayload' import type { ViteHotContext } from 'types/hot' import type { InferCustomEventPayload } from 'types/customEvent' import { HMRClient, HMRContext } from '../shared/hmr' @@ -136,7 +136,10 @@ const debounceReload = (time: number) => { const pageReload = debounceReload(50) const hmrClient = new HMRClient( - console, + { + error: (err) => console.error('[vite]', err), + debug: (...msg) => console.debug('[vite]', ...msg), + }, { isReady: () => socket && socket.readyState === 1, send: (message) => socket.send(message), @@ -169,7 +172,7 @@ const hmrClient = new HMRClient( }, ) -async function handleMessage(payload: HMRPayload) { +async function handleMessage(payload: HotPayload) { switch (payload.type) { case 'connected': console.debug(`[vite] connected.`) diff --git a/packages/vite/src/client/overlay.ts b/packages/vite/src/client/overlay.ts index 63f570be488efb..971577236dc3f0 100644 --- a/packages/vite/src/client/overlay.ts +++ b/packages/vite/src/client/overlay.ts @@ -1,4 +1,4 @@ -import type { ErrorPayload } from 'types/hmrPayload' +import type { ErrorPayload } from 'types/hotPayload' // injected by the hmr plugin when served declare const __BASE__: string diff --git a/packages/vite/src/runtime/constants.ts b/packages/vite/src/module-runner/constants.ts similarity index 100% rename from packages/vite/src/runtime/constants.ts rename to packages/vite/src/module-runner/constants.ts diff --git a/packages/vite/src/runtime/esmRunner.ts b/packages/vite/src/module-runner/esmEvaluator.ts similarity index 82% rename from packages/vite/src/runtime/esmRunner.ts rename to packages/vite/src/module-runner/esmEvaluator.ts index 5d4c481c39e85a..3e84f98f7646bd 100644 --- a/packages/vite/src/runtime/esmRunner.ts +++ b/packages/vite/src/module-runner/esmEvaluator.ts @@ -6,11 +6,11 @@ import { ssrImportMetaKey, ssrModuleExportsKey, } from './constants' -import type { ViteModuleRunner, ViteRuntimeModuleContext } from './types' +import type { ModuleEvaluator, ModuleRunnerContext } from './types' -export class ESModulesRunner implements ViteModuleRunner { - async runViteModule( - context: ViteRuntimeModuleContext, +export class ESModulesEvaluator implements ModuleEvaluator { + async runInlinedModule( + context: ModuleRunnerContext, code: string, ): Promise { // use AsyncFunction instead of vm module to support broader array of environments out of the box diff --git a/packages/vite/src/runtime/hmrHandler.ts b/packages/vite/src/module-runner/hmrHandler.ts similarity index 51% rename from packages/vite/src/runtime/hmrHandler.ts rename to packages/vite/src/module-runner/hmrHandler.ts index b0b9fdd5fd6f32..a0965f854354c6 100644 --- a/packages/vite/src/runtime/hmrHandler.ts +++ b/packages/vite/src/module-runner/hmrHandler.ts @@ -1,24 +1,24 @@ -import type { HMRPayload } from 'types/hmrPayload' -import { unwrapId } from '../shared/utils' -import type { ViteRuntime } from './runtime' +import type { HotPayload } from 'types/hotPayload' +import { slash, unwrapId } from '../shared/utils' +import type { ModuleRunner } from './runner' // updates to HMR should go one after another. It is possible to trigger another update during the invalidation for example. export function createHMRHandler( - runtime: ViteRuntime, -): (payload: HMRPayload) => Promise { + runner: ModuleRunner, +): (payload: HotPayload) => Promise { const queue = new Queue() - return (payload) => queue.enqueue(() => handleHMRPayload(runtime, payload)) + return (payload) => queue.enqueue(() => handleHotPayload(runner, payload)) } -export async function handleHMRPayload( - runtime: ViteRuntime, - payload: HMRPayload, +export async function handleHotPayload( + runner: ModuleRunner, + payload: HotPayload, ): Promise { - const hmrClient = runtime.hmrClient - if (!hmrClient || runtime.isDestroyed()) return + const hmrClient = runner.hmrClient + if (!hmrClient || runner.isDestroyed()) return switch (payload.type) { case 'connected': - hmrClient.logger.debug(`[vite] connected.`) + hmrClient.logger.debug(`connected.`) hmrClient.messenger.flush() break case 'update': @@ -26,15 +26,13 @@ export async function handleHMRPayload( await Promise.all( payload.updates.map(async (update): Promise => { if (update.type === 'js-update') { - // runtime always caches modules by their full path without /@id/ prefix + // runner always caches modules by their full path without /@id/ prefix update.acceptedPath = unwrapId(update.acceptedPath) update.path = unwrapId(update.path) return hmrClient.queueUpdate(update) } - hmrClient.logger.error( - '[vite] css hmr is not supported in runtime mode.', - ) + hmrClient.logger.error('css hmr is not supported in runner mode.') }), ) await hmrClient.notifyListeners('vite:afterUpdate', payload) @@ -46,22 +44,20 @@ export async function handleHMRPayload( case 'full-reload': { const { triggeredBy } = payload const clearEntrypoints = triggeredBy - ? [...runtime.entrypoints].filter((entrypoint) => - runtime.moduleCache.isImported({ - importedId: triggeredBy, - importedBy: entrypoint, - }), + ? getModulesEntrypoints( + runner, + getModulesByFile(runner, slash(triggeredBy)), ) - : [...runtime.entrypoints] + : findAllEntrypoints(runner) - if (!clearEntrypoints.length) break + if (!clearEntrypoints.size) break - hmrClient.logger.debug(`[vite] program reload`) + hmrClient.logger.debug(`program reload`) await hmrClient.notifyListeners('vite:beforeFullReload', payload) - runtime.moduleCache.clear() + runner.moduleCache.clear() for (const id of clearEntrypoints) { - await runtime.executeUrl(id) + await runner.import(id) } break } @@ -73,7 +69,7 @@ export async function handleHMRPayload( await hmrClient.notifyListeners('vite:error', payload) const err = payload.err hmrClient.logger.error( - `[vite] Internal Server Error\n${err.message}\n${err.stack}`, + `Internal Server Error\n${err.message}\n${err.stack}`, ) break } @@ -123,3 +119,46 @@ class Queue { return true } } + +function getModulesByFile(runner: ModuleRunner, file: string) { + const modules: string[] = [] + for (const [id, mod] of runner.moduleCache.entries()) { + if (mod.meta && 'file' in mod.meta && mod.meta.file === file) { + modules.push(id) + } + } + return modules +} + +function getModulesEntrypoints( + runner: ModuleRunner, + modules: string[], + visited = new Set(), + entrypoints = new Set(), +) { + for (const moduleId of modules) { + if (visited.has(moduleId)) continue + visited.add(moduleId) + const module = runner.moduleCache.getByModuleId(moduleId) + if (module.importers && !module.importers.size) { + entrypoints.add(moduleId) + continue + } + for (const importer of module.importers || []) { + getModulesEntrypoints(runner, [importer], visited, entrypoints) + } + } + return entrypoints +} + +function findAllEntrypoints( + runner: ModuleRunner, + entrypoints = new Set(), +): Set { + for (const [id, mod] of runner.moduleCache.entries()) { + if (mod.importers && !mod.importers.size) { + entrypoints.add(id) + } + } + return entrypoints +} diff --git a/packages/vite/src/runtime/hmrLogger.ts b/packages/vite/src/module-runner/hmrLogger.ts similarity index 51% rename from packages/vite/src/runtime/hmrLogger.ts rename to packages/vite/src/module-runner/hmrLogger.ts index 57325298949e09..931a69d125d45b 100644 --- a/packages/vite/src/runtime/hmrLogger.ts +++ b/packages/vite/src/module-runner/hmrLogger.ts @@ -6,3 +6,8 @@ export const silentConsole: HMRLogger = { debug: noop, error: noop, } + +export const hmrLogger: HMRLogger = { + debug: (...msg) => console.log('[vite]', ...msg), + error: (error) => console.log('[vite]', error), +} diff --git a/packages/vite/src/runtime/index.ts b/packages/vite/src/module-runner/index.ts similarity index 51% rename from packages/vite/src/runtime/index.ts rename to packages/vite/src/module-runner/index.ts index ded7222e45d690..836b261a8b8687 100644 --- a/packages/vite/src/runtime/index.ts +++ b/packages/vite/src/module-runner/index.ts @@ -1,21 +1,25 @@ -// this file should re-export only things that don't rely on Node.js or other runtime features +// this file should re-export only things that don't rely on Node.js or other runner features export { ModuleCacheMap } from './moduleCache' -export { ViteRuntime } from './runtime' -export { ESModulesRunner } from './esmRunner' +export { ModuleRunner } from './runner' +export { ESModulesEvaluator } from './esmEvaluator' +export { RemoteRunnerTransport } from './runnerTransport' +export type { RunnerTransport } from './runnerTransport' export type { HMRLogger, HMRConnection } from '../shared/hmr' export type { - ViteModuleRunner, - ViteRuntimeModuleContext, + ModuleEvaluator, + ModuleRunnerContext, ModuleCache, FetchResult, FetchFunction, + FetchFunctionOptions, ResolvedResult, SSRImportMetadata, - HMRRuntimeConnection, - ViteRuntimeImportMeta, - ViteRuntimeOptions, + ModuleRunnerHMRConnection, + ModuleRunnerImportMeta, + ModuleRunnerOptions, + ModuleRunnerHmr, } from './types' export { ssrDynamicImportKey, diff --git a/packages/vite/src/runtime/moduleCache.ts b/packages/vite/src/module-runner/moduleCache.ts similarity index 83% rename from packages/vite/src/runtime/moduleCache.ts rename to packages/vite/src/module-runner/moduleCache.ts index 3681e3db1ed78d..ed94cc7bcbcbe2 100644 --- a/packages/vite/src/runtime/moduleCache.ts +++ b/packages/vite/src/module-runner/moduleCache.ts @@ -4,7 +4,7 @@ import { decodeBase64 } from './utils' import { DecodedMap } from './sourcemap/decoder' import type { ModuleCache } from './types' -const VITE_RUNTIME_SOURCEMAPPING_REGEXP = new RegExp( +const MODULE_RUNNER_SOURCEMAPPING_REGEXP = new RegExp( `//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`, ) @@ -46,6 +46,7 @@ export class ModuleCacheMap extends Map { Object.assign(mod, { imports: new Set(), importers: new Set(), + timestamp: 0, }) } return mod @@ -63,8 +64,12 @@ export class ModuleCacheMap extends Map { return this.deleteByModuleId(this.normalize(fsPath)) } - invalidate(id: string): void { + invalidateUrl(id: string): void { const module = this.get(id) + this.invalidateModule(module) + } + + invalidateModule(module: ModuleCache): void { module.evaluated = false module.meta = undefined module.map = undefined @@ -77,43 +82,6 @@ export class ModuleCacheMap extends Map { module.imports?.clear() } - isImported( - { - importedId, - importedBy, - }: { - importedId: string - importedBy: string - }, - seen = new Set(), - ): boolean { - importedId = this.normalize(importedId) - importedBy = this.normalize(importedBy) - - if (importedBy === importedId) return true - - if (seen.has(importedId)) return false - seen.add(importedId) - - const fileModule = this.getByModuleId(importedId) - const importers = fileModule?.importers - - if (!importers) return false - - if (importers.has(importedBy)) return true - - for (const importer of importers) { - if ( - this.isImported({ - importedBy: importedBy, - importedId: importer, - }) - ) - return true - } - return false - } - /** * Invalidate modules that dependent on the given modules, up to the main entry */ @@ -127,7 +95,7 @@ export class ModuleCacheMap extends Map { invalidated.add(id) const mod = super.get(id) if (mod?.importers) this.invalidateDepTree(mod.importers, invalidated) - super.delete(id) + this.invalidateUrl(id) } return invalidated } @@ -157,7 +125,7 @@ export class ModuleCacheMap extends Map { if (mod.map) return mod.map if (!mod.meta || !('code' in mod.meta)) return null const mapString = mod.meta.code.match( - VITE_RUNTIME_SOURCEMAPPING_REGEXP, + MODULE_RUNNER_SOURCEMAPPING_REGEXP, )?.[1] if (!mapString) return null const baseFile = mod.meta.file || moduleId.split('?')[0] diff --git a/packages/vite/src/runtime/runtime.ts b/packages/vite/src/module-runner/runner.ts similarity index 64% rename from packages/vite/src/runtime/runtime.ts rename to packages/vite/src/module-runner/runner.ts index b7f08fed3d3a1b..19e3d86197aee2 100644 --- a/packages/vite/src/runtime/runtime.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -11,14 +11,13 @@ import { import { analyzeImportedModDifference } from '../shared/ssrTransform' import { ModuleCacheMap } from './moduleCache' import type { - FetchResult, ModuleCache, + ModuleEvaluator, + ModuleRunnerContext, + ModuleRunnerImportMeta, + ModuleRunnerOptions, ResolvedResult, SSRImportMetadata, - ViteModuleRunner, - ViteRuntimeImportMeta, - ViteRuntimeModuleContext, - ViteRuntimeOptions, } from './types' import { posixDirname, @@ -33,92 +32,76 @@ import { ssrImportMetaKey, ssrModuleExportsKey, } from './constants' -import { silentConsole } from './hmrLogger' +import { hmrLogger, silentConsole } from './hmrLogger' import { createHMRHandler } from './hmrHandler' import { enableSourceMapSupport } from './sourcemap/index' +import type { RunnerTransport } from './runnerTransport' -interface ViteRuntimeDebugger { +interface ModuleRunnerDebugger { (formatter: unknown, ...args: unknown[]): void } -export class ViteRuntime { +export class ModuleRunner { /** * Holds the cache of modules * Keys of the map are ids */ public moduleCache: ModuleCacheMap public hmrClient?: HMRClient - public entrypoints = new Set() - private idToUrlMap = new Map() - private fileToIdMap = new Map() - private envProxy = new Proxy({} as any, { + private readonly urlToIdMap = new Map() + private readonly fileToIdMap = new Map() + private readonly envProxy = new Proxy({} as any, { get(_, p) { throw new Error( - `[vite-runtime] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`, + `[module runner] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`, ) }, }) + private readonly transport: RunnerTransport + private readonly resetSourceMapSupport?: () => void - private _destroyed = false - private _resetSourceMapSupport?: () => void + private destroyed = false constructor( - public options: ViteRuntimeOptions, - public runner: ViteModuleRunner, - private debug?: ViteRuntimeDebugger, + public options: ModuleRunnerOptions, + public evaluator: ModuleEvaluator, + private debug?: ModuleRunnerDebugger, ) { this.moduleCache = options.moduleCache ?? new ModuleCacheMap(options.root) + this.transport = options.transport if (typeof options.hmr === 'object') { this.hmrClient = new HMRClient( options.hmr.logger === false ? silentConsole - : options.hmr.logger || console, + : options.hmr.logger || hmrLogger, options.hmr.connection, - ({ acceptedPath, ssrInvalidates }) => { - this.moduleCache.invalidate(acceptedPath) - if (ssrInvalidates) { - this.invalidateFiles(ssrInvalidates) - } - return this.executeUrl(acceptedPath) + ({ acceptedPath }) => { + return this.import(acceptedPath) }, ) options.hmr.connection.onUpdate(createHMRHandler(this)) } if (options.sourcemapInterceptor !== false) { - this._resetSourceMapSupport = enableSourceMapSupport(this) + this.resetSourceMapSupport = enableSourceMapSupport(this) } } /** * URL to execute. Accepts file path, server path or id relative to the root. */ - public async executeUrl(url: string): Promise { + public async import(url: string): Promise { url = this.normalizeEntryUrl(url) const fetchedModule = await this.cachedModule(url) return await this.cachedRequest(url, fetchedModule) } - /** - * Entrypoint URL to execute. Accepts file path, server path or id relative to the root. - * In the case of a full reload triggered by HMR, this is the module that will be reloaded. - * If this method is called multiple times, all entrypoints will be reloaded one at a time. - */ - public async executeEntrypoint(url: string): Promise { - url = this.normalizeEntryUrl(url) - const fetchedModule = await this.cachedModule(url) - return await this.cachedRequest(url, fetchedModule, [], { - entrypoint: true, - }) - } - /** * Clear all caches including HMR listeners. */ public clearCache(): void { this.moduleCache.clear() - this.idToUrlMap.clear() - this.entrypoints.clear() + this.urlToIdMap.clear() this.hmrClient?.clear() } @@ -127,26 +110,17 @@ export class ViteRuntime { * This method doesn't stop the HMR connection. */ public async destroy(): Promise { - this._resetSourceMapSupport?.() + this.resetSourceMapSupport?.() this.clearCache() this.hmrClient = undefined - this._destroyed = true + this.destroyed = true } /** * Returns `true` if the runtime has been destroyed by calling `destroy()` method. */ public isDestroyed(): boolean { - return this._destroyed - } - - private invalidateFiles(files: string[]) { - files.forEach((file) => { - const ids = this.fileToIdMap.get(file) - if (ids) { - ids.forEach((id) => this.moduleCache.invalidate(id)) - } - }) + return this.destroyed } // we don't use moduleCache.normalize because this URL doesn't have to follow the same rules @@ -195,17 +169,12 @@ export class ViteRuntime { private async cachedRequest( id: string, - fetchedModule: ResolvedResult, + mod: ModuleCache, callstack: string[] = [], metadata?: SSRImportMetadata, ): Promise { - const moduleId = fetchedModule.id - - if (metadata?.entrypoint) { - this.entrypoints.add(moduleId) - } - - const mod = this.moduleCache.getByModuleId(moduleId) + const meta = mod.meta! + const moduleId = meta.id const { imports, importers } = mod as Required @@ -218,8 +187,7 @@ export class ViteRuntime { callstack.includes(moduleId) || Array.from(imports.values()).some((i) => importers.has(i)) ) { - if (mod.exports) - return this.processImport(mod.exports, fetchedModule, metadata) + if (mod.exports) return this.processImport(mod.exports, meta, metadata) } let debugTimer: any @@ -232,7 +200,7 @@ export class ViteRuntime { .join('\n')}` this.debug!( - `[vite-runtime] module ${moduleId} takes over 2s to load.\n${getStack()}`, + `[module runner] module ${moduleId} takes over 2s to load.\n${getStack()}`, ) }, 2000) } @@ -240,12 +208,12 @@ export class ViteRuntime { try { // cached module if (mod.promise) - return this.processImport(await mod.promise, fetchedModule, metadata) + return this.processImport(await mod.promise, meta, metadata) - const promise = this.directRequest(id, fetchedModule, callstack) + const promise = this.directRequest(id, mod, callstack) mod.promise = promise mod.evaluated = false - return this.processImport(await promise, fetchedModule, metadata) + return this.processImport(await promise, meta, metadata) } finally { mod.evaluated = true if (debugTimer) clearTimeout(debugTimer) @@ -253,34 +221,56 @@ export class ViteRuntime { } private async cachedModule( - id: string, + url: string, importer?: string, - ): Promise { - if (this._destroyed) { - throw new Error(`[vite] Vite runtime has been destroyed.`) + ): Promise { + if (this.destroyed) { + throw new Error(`Vite module runner has been destroyed.`) + } + + this.debug?.('[module runner] fetching', url) + + const normalized = this.urlToIdMap.get(url) + let cachedModule = normalized && this.moduleCache.getByModuleId(normalized) + if (!cachedModule) { + cachedModule = this.moduleCache.getByModuleId(url) } - const normalized = this.idToUrlMap.get(id) - if (normalized) { - const mod = this.moduleCache.getByModuleId(normalized) - if (mod.meta) { - return mod.meta as ResolvedResult + + const isCached = !!(typeof cachedModule === 'object' && cachedModule.meta) + + const fetchedModule = // fast return for established externalized pattern + ( + url.startsWith('data:') + ? { externalize: url, type: 'builtin' } + : await this.transport.fetchModule(url, importer, { + cached: isCached, + }) + ) as ResolvedResult + + if ('cache' in fetchedModule) { + if (!cachedModule || !cachedModule.meta) { + throw new Error( + `Module "${url}" was mistakenly invalidated during fetch phase.`, + ) } + return cachedModule } - this.debug?.('[vite-runtime] fetching', id) - // fast return for established externalized patterns - const fetchedModule = id.startsWith('data:') - ? ({ externalize: id, type: 'builtin' } satisfies FetchResult) - : await this.options.fetchModule(id, importer) + // base moduleId on "file" and not on id // if `import(variable)` is called it's possible that it doesn't have an extension for example - // if we used id for that, it's possible to have a duplicated module - const idQuery = id.split('?')[1] + // if we used id for that, then a module will be duplicated + const idQuery = url.split('?')[1] const query = idQuery ? `?${idQuery}` : '' const file = 'file' in fetchedModule ? fetchedModule.file : undefined - const fullFile = file ? `${file}${query}` : id - const moduleId = this.moduleCache.normalize(fullFile) + const fileId = file ? `${file}${query}` : url + const moduleId = this.moduleCache.normalize(fileId) const mod = this.moduleCache.getByModuleId(moduleId) - ;(fetchedModule as ResolvedResult).id = moduleId + + if ('invalidate' in fetchedModule && fetchedModule.invalidate) { + this.moduleCache.invalidateModule(mod) + } + + fetchedModule.id = moduleId mod.meta = fetchedModule if (file) { @@ -289,27 +279,28 @@ export class ViteRuntime { this.fileToIdMap.set(file, fileModules) } - this.idToUrlMap.set(id, moduleId) - this.idToUrlMap.set(unwrapId(id), moduleId) - return fetchedModule as ResolvedResult + this.urlToIdMap.set(url, moduleId) + this.urlToIdMap.set(unwrapId(url), moduleId) + return mod } // override is allowed, consider this a public API protected async directRequest( id: string, - fetchResult: ResolvedResult, + mod: ModuleCache, _callstack: string[], ): Promise { + const fetchResult = mod.meta! const moduleId = fetchResult.id const callstack = [..._callstack, moduleId] - const mod = this.moduleCache.getByModuleId(moduleId) - const request = async (dep: string, metadata?: SSRImportMetadata) => { - const fetchedModule = await this.cachedModule(dep, moduleId) - const depMod = this.moduleCache.getByModuleId(fetchedModule.id) + const importer = ('file' in fetchResult && fetchResult.file) || moduleId + const fetchedModule = await this.cachedModule(dep, importer) + const resolvedId = fetchedModule.meta!.id + const depMod = this.moduleCache.getByModuleId(resolvedId) depMod.importers!.add(moduleId) - mod.imports!.add(fetchedModule.id) + mod.imports!.add(resolvedId) return this.cachedRequest(dep, fetchedModule, callstack, metadata) } @@ -325,8 +316,8 @@ export class ViteRuntime { if ('externalize' in fetchResult) { const { externalize } = fetchResult - this.debug?.('[vite-runtime] externalizing', externalize) - const exports = await this.runner.runExternalModule(externalize) + this.debug?.('[module runner] externalizing', externalize) + const exports = await this.evaluator.runExternalModule(externalize) mod.exports = exports return exports } @@ -336,7 +327,7 @@ export class ViteRuntime { if (code == null) { const importer = callstack[callstack.length - 2] throw new Error( - `[vite-runtime] Failed to load "${id}"${ + `[module runner] Failed to load "${id}"${ importer ? ` imported from ${importer}` : '' }`, ) @@ -347,19 +338,19 @@ export class ViteRuntime { const href = posixPathToFileHref(modulePath) const filename = modulePath const dirname = posixDirname(modulePath) - const meta: ViteRuntimeImportMeta = { + const meta: ModuleRunnerImportMeta = { filename: isWindows ? toWindowsPath(filename) : filename, dirname: isWindows ? toWindowsPath(dirname) : dirname, url: href, env: this.envProxy, resolve(id, parent) { throw new Error( - '[vite-runtime] "import.meta.resolve" is not supported.', + '[module runner] "import.meta.resolve" is not supported.', ) }, // should be replaced during transformation glob() { - throw new Error('[vite-runtime] "import.meta.glob" is not supported.') + throw new Error('[module runner] "import.meta.glob" is not supported.') }, } const exports = Object.create(null) @@ -377,9 +368,9 @@ export class ViteRuntime { enumerable: true, get: () => { if (!this.hmrClient) { - throw new Error(`[vite-runtime] HMR client was destroyed.`) + throw new Error(`[module runner] HMR client was destroyed.`) } - this.debug?.('[vite-runtime] creating hmr context for', moduleId) + this.debug?.('[module runner] creating hmr context for', moduleId) hotContext ||= new HMRContext(this.hmrClient, moduleId) return hotContext }, @@ -389,7 +380,7 @@ export class ViteRuntime { }) } - const context: ViteRuntimeModuleContext = { + const context: ModuleRunnerContext = { [ssrImportKey]: request, [ssrDynamicImportKey]: dynamicRequest, [ssrModuleExportsKey]: exports, @@ -397,9 +388,9 @@ export class ViteRuntime { [ssrImportMetaKey]: meta, } - this.debug?.('[vite-runtime] executing', href) + this.debug?.('[module runner] executing', href) - await this.runner.runViteModule(context, code, id) + await this.evaluator.runInlinedModule(context, code, id) return exports } diff --git a/packages/vite/src/module-runner/runnerTransport.ts b/packages/vite/src/module-runner/runnerTransport.ts new file mode 100644 index 00000000000000..f946d956342c25 --- /dev/null +++ b/packages/vite/src/module-runner/runnerTransport.ts @@ -0,0 +1,83 @@ +import type { FetchFunction, FetchResult } from './types' + +export interface RunnerTransport { + fetchModule: FetchFunction +} + +export class RemoteRunnerTransport implements RunnerTransport { + private rpcPromises = new Map< + string, + { + resolve: (data: any) => void + reject: (data: any) => void + timeoutId?: NodeJS.Timeout + } + >() + + constructor( + private readonly options: { + send: (data: any) => void + onMessage: (handler: (data: any) => void) => void + timeout?: number + }, + ) { + this.options.onMessage(async (data) => { + if (typeof data !== 'object' || !data || !data.__v) return + + const promise = this.rpcPromises.get(data.i) + if (!promise) return + + if (promise.timeoutId) clearTimeout(promise.timeoutId) + + this.rpcPromises.delete(data.i) + + if (data.e) { + promise.reject(data.e) + } else { + promise.resolve(data.r) + } + }) + } + + private resolve(method: string, ...args: any[]) { + const promiseId = nanoid() + this.options.send({ + __v: true, + m: method, + a: args, + i: promiseId, + }) + + return new Promise((resolve, reject) => { + const timeout = this.options.timeout ?? 60000 + let timeoutId + if (timeout > 0) { + timeoutId = setTimeout(() => { + this.rpcPromises.delete(promiseId) + reject( + new Error( + `${method}(${args.map((arg) => JSON.stringify(arg)).join(', ')}) timed out after ${timeout}ms`, + ), + ) + }, timeout) + timeoutId?.unref?.() + } + this.rpcPromises.set(promiseId, { resolve, reject, timeoutId }) + }) + } + + fetchModule(id: string, importer?: string): Promise { + return this.resolve('fetchModule', id, importer) + } +} + +// port from nanoid +// https://github.com/ai/nanoid +const urlAlphabet = + 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' +function nanoid(size = 21) { + let id = '' + let i = size + while (i--) id += urlAlphabet[(Math.random() * 64) | 0] + return id +} diff --git a/packages/vite/src/runtime/sourcemap/decoder.ts b/packages/vite/src/module-runner/sourcemap/decoder.ts similarity index 100% rename from packages/vite/src/runtime/sourcemap/decoder.ts rename to packages/vite/src/module-runner/sourcemap/decoder.ts diff --git a/packages/vite/src/runtime/sourcemap/index.ts b/packages/vite/src/module-runner/sourcemap/index.ts similarity index 76% rename from packages/vite/src/runtime/sourcemap/index.ts rename to packages/vite/src/module-runner/sourcemap/index.ts index 648c5e52717fc2..0efc5ca2db97b5 100644 --- a/packages/vite/src/runtime/sourcemap/index.ts +++ b/packages/vite/src/module-runner/sourcemap/index.ts @@ -1,8 +1,8 @@ -import type { ViteRuntime } from '../runtime' +import type { ModuleRunner } from '../runner' import { interceptStackTrace } from './interceptor' -export function enableSourceMapSupport(runtime: ViteRuntime): () => void { - if (runtime.options.sourcemapInterceptor === 'node') { +export function enableSourceMapSupport(runner: ModuleRunner): () => void { + if (runner.options.sourcemapInterceptor === 'node') { if (typeof process === 'undefined') { throw new TypeError( `Cannot use "sourcemapInterceptor: 'node'" because global "process" variable is not available.`, @@ -20,9 +20,9 @@ export function enableSourceMapSupport(runtime: ViteRuntime): () => void { /* eslint-enable n/no-unsupported-features/node-builtins */ } return interceptStackTrace( - runtime, - typeof runtime.options.sourcemapInterceptor === 'object' - ? runtime.options.sourcemapInterceptor + runner, + typeof runner.options.sourcemapInterceptor === 'object' + ? runner.options.sourcemapInterceptor : undefined, ) } diff --git a/packages/vite/src/runtime/sourcemap/interceptor.ts b/packages/vite/src/module-runner/sourcemap/interceptor.ts similarity index 97% rename from packages/vite/src/runtime/sourcemap/interceptor.ts rename to packages/vite/src/module-runner/sourcemap/interceptor.ts index 58d324e79b943c..424337839dc610 100644 --- a/packages/vite/src/runtime/sourcemap/interceptor.ts +++ b/packages/vite/src/module-runner/sourcemap/interceptor.ts @@ -1,5 +1,5 @@ import type { OriginalMapping } from '@jridgewell/trace-mapping' -import type { ViteRuntime } from '../runtime' +import type { ModuleRunner } from '../runner' import { posixDirname, posixResolve } from '../utils' import type { ModuleCacheMap } from '../moduleCache' import { slash } from '../../shared/utils' @@ -45,8 +45,8 @@ const retrieveSourceMapFromHandlers = createExecHandlers( let overridden = false const originalPrepare = Error.prepareStackTrace -function resetInterceptor(runtime: ViteRuntime, options: InterceptorOptions) { - moduleGraphs.delete(runtime.moduleCache) +function resetInterceptor(runner: ModuleRunner, options: InterceptorOptions) { + moduleGraphs.delete(runner.moduleCache) if (options.retrieveFile) retrieveFileHandlers.delete(options.retrieveFile) if (options.retrieveSourceMap) retrieveSourceMapHandlers.delete(options.retrieveSourceMap) @@ -57,18 +57,18 @@ function resetInterceptor(runtime: ViteRuntime, options: InterceptorOptions) { } export function interceptStackTrace( - runtime: ViteRuntime, + runner: ModuleRunner, options: InterceptorOptions = {}, ): () => void { if (!overridden) { Error.prepareStackTrace = prepareStackTrace overridden = true } - moduleGraphs.add(runtime.moduleCache) + moduleGraphs.add(runner.moduleCache) if (options.retrieveFile) retrieveFileHandlers.add(options.retrieveFile) if (options.retrieveSourceMap) retrieveSourceMapHandlers.add(options.retrieveSourceMap) - return () => resetInterceptor(runtime, options) + return () => resetInterceptor(runner, options) } interface CallSite extends NodeJS.CallSite { @@ -101,7 +101,7 @@ function supportRelativeURL(file: string, url: string) { return protocol + posixResolve(startPath, url) } -function getRuntimeSourceMap(position: OriginalMapping): CachedMapEntry | null { +function getRunnerSourceMap(position: OriginalMapping): CachedMapEntry | null { for (const moduleCache of moduleGraphs) { const sourceMap = moduleCache.getSourceMap(position.source!) if (sourceMap) { @@ -172,7 +172,7 @@ function retrieveSourceMap(source: string) { function mapSourcePosition(position: OriginalMapping) { if (!position.source) return position - let sourceMap = getRuntimeSourceMap(position) + let sourceMap = getRunnerSourceMap(position) if (!sourceMap) sourceMap = sourceMapCache[position.source] if (!sourceMap) { // Call the (overrideable) retrieveSourceMap function to get the source map. diff --git a/packages/vite/src/runtime/tsconfig.json b/packages/vite/src/module-runner/tsconfig.json similarity index 78% rename from packages/vite/src/runtime/tsconfig.json rename to packages/vite/src/module-runner/tsconfig.json index b664c0ea7a093f..40840789f59fc8 100644 --- a/packages/vite/src/runtime/tsconfig.json +++ b/packages/vite/src/module-runner/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "include": ["./", "../node", "../dep-types", "../types"], - "exclude": ["**/__tests__"], + "exclude": ["**/__tests__", "**/__tests_dts__"], "compilerOptions": { "lib": ["ESNext", "DOM"], "stripInternal": true diff --git a/packages/vite/src/runtime/types.ts b/packages/vite/src/module-runner/types.ts similarity index 69% rename from packages/vite/src/runtime/types.ts rename to packages/vite/src/module-runner/types.ts index 730ed59630e26d..ce53841c8443cf 100644 --- a/packages/vite/src/runtime/types.ts +++ b/packages/vite/src/module-runner/types.ts @@ -1,5 +1,5 @@ import type { ViteHotContext } from 'types/hot' -import type { HMRPayload } from 'types/hmrPayload' +import type { HotPayload } from 'types/hotPayload' import type { HMRConnection, HMRLogger } from '../shared/hmr' import type { DefineImportMetadata, @@ -15,28 +15,26 @@ import type { } from './constants' import type { DecodedMap } from './sourcemap/decoder' import type { InterceptorOptions } from './sourcemap/interceptor' +import type { RunnerTransport } from './runnerTransport' -export type { DefineImportMetadata } -export interface SSRImportMetadata extends SSRImportBaseMetadata { - entrypoint?: boolean -} +export type { DefineImportMetadata, SSRImportBaseMetadata as SSRImportMetadata } -export interface HMRRuntimeConnection extends HMRConnection { +export interface ModuleRunnerHMRConnection extends HMRConnection { /** * Configure how HMR is handled when this connection triggers an update. * This method expects that connection will start listening for HMR updates and call this callback when it's received. */ - onUpdate(callback: (payload: HMRPayload) => void): void + onUpdate(callback: (payload: HotPayload) => void): void } -export interface ViteRuntimeImportMeta extends ImportMeta { +export interface ModuleRunnerImportMeta extends ImportMeta { url: string env: ImportMetaEnv hot?: ViteHotContext [key: string]: any } -export interface ViteRuntimeModuleContext { +export interface ModuleRunnerContext { [ssrModuleExportsKey]: Record [ssrImportKey]: (id: string, metadata?: DefineImportMetadata) => Promise [ssrDynamicImportKey]: ( @@ -44,18 +42,18 @@ export interface ViteRuntimeModuleContext { options?: ImportCallOptions, ) => Promise [ssrExportAllKey]: (obj: any) => void - [ssrImportMetaKey]: ViteRuntimeImportMeta + [ssrImportMetaKey]: ModuleRunnerImportMeta } -export interface ViteModuleRunner { +export interface ModuleEvaluator { /** * Run code that was transformed by Vite. * @param context Function context * @param code Transformed code * @param id ID that was used to fetch the module */ - runViteModule( - context: ViteRuntimeModuleContext, + runInlinedModule( + context: ModuleRunnerContext, code: string, id: string, ): Promise @@ -71,7 +69,7 @@ export interface ModuleCache { exports?: any evaluated?: boolean map?: DecodedMap - meta?: FetchResult + meta?: ResolvedResult /** * Module ids that imports this module */ @@ -79,13 +77,24 @@ export interface ModuleCache { imports?: Set } -export type FetchResult = ExternalFetchResult | ViteFetchResult +export type FetchResult = + | CachedFetchResult + | ExternalFetchResult + | ViteFetchResult + +export interface CachedFetchResult { + /** + * If module cached in the runner, we can just confirm + * it wasn't invalidated on the server side. + */ + cache: true +} export interface ExternalFetchResult { /** * The path to the externalized module starting with file://, * by default this will be imported via a dynamic "import" - * instead of being transformed by vite and loaded with vite runtime + * instead of being transformed by vite and loaded with vite runner */ externalize: string /** @@ -97,44 +106,56 @@ export interface ExternalFetchResult { export interface ViteFetchResult { /** - * Code that will be evaluated by vite runtime + * Code that will be evaluated by vite runner * by default this will be wrapped in an async function */ code: string /** * File path of the module on disk. * This will be resolved as import.meta.url/filename + * Will be equal to `null` for virtual modules */ file: string | null + /** + * Invalidate module on the client side. + */ + invalidate: boolean } export type ResolvedResult = (ExternalFetchResult | ViteFetchResult) & { id: string } -/** - * @experimental - */ export type FetchFunction = ( id: string, importer?: string, + options?: FetchFunctionOptions, ) => Promise -export interface ViteRuntimeOptions { +export interface FetchFunctionOptions { + cached?: boolean +} + +export interface ModuleRunnerHmr { /** - * Root of the project + * Configure how HMR communicates between the client and the server. */ - root: string + connection: ModuleRunnerHMRConnection /** - * A method to get the information about the module. - * For SSR, Vite exposes `server.ssrFetchModule` function that you can use here. - * For other runtime use cases, Vite also exposes `fetchModule` from its main entry point. + * Configure HMR logger. */ - fetchModule: FetchFunction + logger?: false | HMRLogger +} + +export interface ModuleRunnerOptions { /** - * Custom environment variables available on `import.meta.env`. This doesn't modify the actual `process.env`. + * Root of the project */ - environmentVariables?: Record + root: string + /** + * A set of methods to communicate with the server. + */ + transport: RunnerTransport /** * Configure how source maps are resolved. Prefers `node` if `process.setSourceMapsEnabled` is available. * Otherwise it will use `prepareStackTrace` by default which overrides `Error.prepareStackTrace` method. @@ -148,20 +169,9 @@ export interface ViteRuntimeOptions { /** * Disable HMR or configure HMR options. */ - hmr?: - | false - | { - /** - * Configure how HMR communicates between the client and the server. - */ - connection: HMRRuntimeConnection - /** - * Configure HMR logger. - */ - logger?: false | HMRLogger - } - /** - * Custom module cache. If not provided, creates a separate module cache for each ViteRuntime instance. + hmr?: false | ModuleRunnerHmr + /** + * Custom module cache. If not provided, creates a separate module cache for each ModuleRunner instance. */ moduleCache?: ModuleCacheMap } diff --git a/packages/vite/src/runtime/utils.ts b/packages/vite/src/module-runner/utils.ts similarity index 100% rename from packages/vite/src/runtime/utils.ts rename to packages/vite/src/module-runner/utils.ts diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 2dad85578812cc..c7cdf38055e9bd 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -4,7 +4,12 @@ import colors from 'picocolors' import { describe, expect, test, vi } from 'vitest' import type { OutputChunk, OutputOptions, RollupOutput } from 'rollup' import type { LibraryFormats, LibraryOptions } from '../build' -import { build, resolveBuildOutputs, resolveLibFilename } from '../build' +import { + build, + createBuilder, + resolveBuildOutputs, + resolveLibFilename, +} from '../build' import type { Logger } from '../logger' import { createLogger } from '../logger' @@ -576,6 +581,102 @@ describe('resolveBuildOutputs', () => { ), ) }) + + test('ssrEmitAssets', async () => { + const result = await build({ + root: resolve(__dirname, 'fixtures/emit-assets'), + logLevel: 'silent', + build: { + ssr: true, + ssrEmitAssets: true, + rollupOptions: { + input: { + index: '/entry', + }, + }, + }, + }) + expect(result).toMatchObject({ + output: [ + { + fileName: 'index.mjs', + }, + { + fileName: expect.stringMatching(/assets\/index-\w*\.css/), + }, + ], + }) + }) + + test('emitAssets', async () => { + const builder = await createBuilder({ + root: resolve(__dirname, 'fixtures/emit-assets'), + environments: { + ssr: { + build: { + ssr: true, + emitAssets: true, + rollupOptions: { + input: { + index: '/entry', + }, + }, + }, + }, + }, + }) + const result = await builder.build(builder.environments.ssr) + expect(result).toMatchObject({ + output: [ + { + fileName: 'index.mjs', + }, + { + fileName: expect.stringMatching(/assets\/index-\w*\.css/), + }, + ], + }) + }) + + test('ssr builtin', async () => { + const builder = await createBuilder({ + root: resolve(__dirname, 'fixtures/dynamic-import'), + environments: { + ssr: { + build: { + ssr: true, + rollupOptions: { + input: { + index: '/entry', + }, + }, + }, + }, + }, + }) + const result = await builder.build(builder.environments.ssr) + expect((result as RollupOutput).output[0].code).not.toContain('preload') + }) + + test('ssr custom', async () => { + const builder = await createBuilder({ + root: resolve(__dirname, 'fixtures/dynamic-import'), + environments: { + custom: { + build: { + ssr: true, + rollupOptions: { + input: { + index: '/entry', + }, + }, + }, + }, + }, + }) + const result = await builder.build(builder.environments.custom) + expect((result as RollupOutput).output[0].code).not.toContain('preload') + }) }) /** diff --git a/packages/vite/src/node/__tests__/dev.spec.ts b/packages/vite/src/node/__tests__/dev.spec.ts index 1ade6c0adde9ea..346bebd2aac42e 100644 --- a/packages/vite/src/node/__tests__/dev.spec.ts +++ b/packages/vite/src/node/__tests__/dev.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from 'vitest' import { resolveConfig } from '..' -describe('resolveBuildOptions in dev', () => { +describe('resolveBuildEnvironmentOptions in dev', () => { test('build.rollupOptions should not have input in lib', async () => { const config = await resolveConfig( { diff --git a/packages/vite/src/node/ssr/__tests__/ssrExternal.spec.ts b/packages/vite/src/node/__tests__/external.spec.ts similarity index 52% rename from packages/vite/src/node/ssr/__tests__/ssrExternal.spec.ts rename to packages/vite/src/node/__tests__/external.spec.ts index 68e753af703ce2..a4c78519fd91ef 100644 --- a/packages/vite/src/node/ssr/__tests__/ssrExternal.spec.ts +++ b/packages/vite/src/node/__tests__/external.spec.ts @@ -1,29 +1,30 @@ import { fileURLToPath } from 'node:url' import { describe, expect, test } from 'vitest' -import type { SSROptions } from '..' -import { resolveConfig } from '../../config' -import { createIsConfiguredAsSsrExternal } from '../ssrExternal' +import { resolveConfig } from '../config' +import { createIsConfiguredAsExternal } from '../external' +import { PartialEnvironment } from '../baseEnvironment' -describe('createIsConfiguredAsSsrExternal', () => { +describe('createIsConfiguredAsExternal', () => { test('default', async () => { const isExternal = await createIsExternal() expect(isExternal('@vitejs/cjs-ssr-dep')).toBe(false) }) test('force external', async () => { - const isExternal = await createIsExternal({ external: true }) + const isExternal = await createIsExternal(true) expect(isExternal('@vitejs/cjs-ssr-dep')).toBe(true) }) }) -async function createIsExternal(ssrConfig?: SSROptions) { +async function createIsExternal(external?: true) { const resolvedConfig = await resolveConfig( { configFile: false, root: fileURLToPath(new URL('./', import.meta.url)), - ssr: ssrConfig, + resolve: { external }, }, 'serve', ) - return createIsConfiguredAsSsrExternal(resolvedConfig) + const environment = new PartialEnvironment('ssr', resolvedConfig) + return createIsConfiguredAsExternal(environment) } diff --git a/packages/vite/src/node/ssr/__tests__/fixtures/cjs-ssr-dep/index.js b/packages/vite/src/node/__tests__/fixtures/cjs-ssr-dep/index.js similarity index 100% rename from packages/vite/src/node/ssr/__tests__/fixtures/cjs-ssr-dep/index.js rename to packages/vite/src/node/__tests__/fixtures/cjs-ssr-dep/index.js diff --git a/packages/vite/src/node/ssr/__tests__/fixtures/cjs-ssr-dep/package.json b/packages/vite/src/node/__tests__/fixtures/cjs-ssr-dep/package.json similarity index 100% rename from packages/vite/src/node/ssr/__tests__/fixtures/cjs-ssr-dep/package.json rename to packages/vite/src/node/__tests__/fixtures/cjs-ssr-dep/package.json diff --git a/packages/vite/src/node/__tests__/fixtures/dynamic-import/dep.mjs b/packages/vite/src/node/__tests__/fixtures/dynamic-import/dep.mjs new file mode 100644 index 00000000000000..76805196e3d27d --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/dynamic-import/dep.mjs @@ -0,0 +1 @@ +export const hello = 'hello' diff --git a/packages/vite/src/node/__tests__/fixtures/dynamic-import/entry.mjs b/packages/vite/src/node/__tests__/fixtures/dynamic-import/entry.mjs new file mode 100644 index 00000000000000..997d636183c8a9 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/dynamic-import/entry.mjs @@ -0,0 +1,4 @@ +export async function main() { + const mod = await import('./dep.mjs') + console.log(mod) +} diff --git a/packages/vite/src/node/__tests__/fixtures/emit-assets/css-module.module.css b/packages/vite/src/node/__tests__/fixtures/emit-assets/css-module.module.css new file mode 100644 index 00000000000000..ccb357b951b97a --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/emit-assets/css-module.module.css @@ -0,0 +1,6 @@ +.css-module { + background: rgb(200, 250, 250); + padding: 20px; + width: 200px; + border: 1px solid gray; +} diff --git a/packages/vite/src/node/__tests__/fixtures/emit-assets/css-normal.css b/packages/vite/src/node/__tests__/fixtures/emit-assets/css-normal.css new file mode 100644 index 00000000000000..b71240fe326ac3 --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/emit-assets/css-normal.css @@ -0,0 +1,6 @@ +#css-normal { + background: rgb(250, 250, 200); + padding: 20px; + width: 200px; + border: 1px solid gray; +} diff --git a/packages/vite/src/node/__tests__/fixtures/emit-assets/entry.mjs b/packages/vite/src/node/__tests__/fixtures/emit-assets/entry.mjs new file mode 100644 index 00000000000000..f62bad6569e92e --- /dev/null +++ b/packages/vite/src/node/__tests__/fixtures/emit-assets/entry.mjs @@ -0,0 +1,6 @@ +import './css-normal.css' +import cssModule from './css-module.module.css' + +export default function Page() { + console.log(cssModule) +} diff --git a/packages/vite/src/node/ssr/__tests__/package.json b/packages/vite/src/node/__tests__/package.json similarity index 100% rename from packages/vite/src/node/ssr/__tests__/package.json rename to packages/vite/src/node/__tests__/package.json diff --git a/packages/vite/src/node/__tests__/plugins/css.spec.ts b/packages/vite/src/node/__tests__/plugins/css.spec.ts index e1c435211c9593..7076bb915c386c 100644 --- a/packages/vite/src/node/__tests__/plugins/css.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/css.spec.ts @@ -11,6 +11,7 @@ import { hoistAtRules, preprocessCSS, } from '../../plugins/css' +import { PartialEnvironment } from '../../baseEnvironment' describe('search css url function', () => { test('some spaces before it', () => { @@ -216,6 +217,8 @@ async function createCssPluginTransform( inlineConfig: InlineConfig = {}, ) { const config = await resolveConfig(inlineConfig, 'serve') + const environment = new PartialEnvironment('client', config) + const { transform, buildStart } = cssPlugin(config) // @ts-expect-error buildStart is function @@ -236,6 +239,7 @@ async function createCssPluginTransform( addWatchFile() { return }, + environment, }, code, id, diff --git a/packages/vite/src/node/__tests__/plugins/define.spec.ts b/packages/vite/src/node/__tests__/plugins/define.spec.ts index 8876a9d7ecc3e2..cd01281229d624 100644 --- a/packages/vite/src/node/__tests__/plugins/define.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/define.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from 'vitest' import { definePlugin } from '../../plugins/define' import { resolveConfig } from '../../config' +import { PartialEnvironment } from '../../baseEnvironment' async function createDefinePluginTransform( define: Record = {}, @@ -12,9 +13,15 @@ async function createDefinePluginTransform( build ? 'build' : 'serve', ) const instance = definePlugin(config) + const environment = new PartialEnvironment(ssr ? 'ssr' : 'client', config) + return async (code: string) => { // @ts-expect-error transform should exist - const result = await instance.transform.call({}, code, 'foo.ts', { ssr }) + const result = await instance.transform.call( + { environment }, + code, + 'foo.ts', + ) return result?.code || result } } diff --git a/packages/vite/src/node/__tests_dts__/plugin.ts b/packages/vite/src/node/__tests_dts__/plugin.ts new file mode 100644 index 00000000000000..5b4ebeb82895c8 --- /dev/null +++ b/packages/vite/src/node/__tests_dts__/plugin.ts @@ -0,0 +1,38 @@ +/** + * This is a development only file for testing types. + */ +import type { Plugin as RollupPlugin } from 'rollup' +import type { Equal, ExpectExtends, ExpectTrue } from '@type-challenges/utils' +import type { Plugin, PluginContextExtension } from '../plugin' +import type { ROLLUP_HOOKS } from '../constants' +import type { + GetHookContextMap, + NonNeverKeys, + RollupPluginHooks, +} from '../typeUtils' + +type EnvironmentPluginHooksContext = GetHookContextMap +type EnvironmentPluginHooksContextMatched = { + [K in keyof EnvironmentPluginHooksContext]: EnvironmentPluginHooksContext[K] extends PluginContextExtension + ? never + : false +} + +type HooksMissingExtension = NonNeverKeys +type HooksMissingInConstants = Exclude< + RollupPluginHooks, + (typeof ROLLUP_HOOKS)[number] +> + +export type cases = [ + // Ensure environment plugin hooks are superset of rollup plugin hooks + ExpectTrue>, + + // Ensure all Rollup hooks have Vite's plugin context extension + ExpectTrue>, + + // Ensure the `ROLLUP_HOOKS` constant is up-to-date + ExpectTrue>, +] + +export {} diff --git a/packages/vite/src/node/baseEnvironment.ts b/packages/vite/src/node/baseEnvironment.ts new file mode 100644 index 00000000000000..b03d8164b08974 --- /dev/null +++ b/packages/vite/src/node/baseEnvironment.ts @@ -0,0 +1,103 @@ +import colors from 'picocolors' +import type { Logger } from './logger' +import type { ResolvedConfig, ResolvedEnvironmentOptions } from './config' +import type { Plugin } from './plugin' + +export class PartialEnvironment { + name: string + config: ResolvedConfig + options: ResolvedEnvironmentOptions + logger: Logger + + constructor( + name: string, + config: ResolvedConfig, + options: ResolvedEnvironmentOptions = config.environments[name], + ) { + this.name = name + this.config = config + this.options = options + const environment = colors.dim(`(${this.name})`) + const colorIndex = + [...environment].reduce((acc, c) => acc + c.charCodeAt(0), 0) % + environmentColors.length + const infoColor = environmentColors[colorIndex || 0] + this.logger = { + get hasWarned() { + return config.logger.hasWarned + }, + info(msg, opts) { + return config.logger.info(msg, { + ...opts, + environment: infoColor(environment), + }) + }, + warn(msg, opts) { + return config.logger.warn(msg, { + ...opts, + environment: colors.yellow(environment), + }) + }, + warnOnce(msg, opts) { + return config.logger.warnOnce(msg, { + ...opts, + environment: colors.yellow(environment), + }) + }, + error(msg, opts) { + return config.logger.error(msg, { + ...opts, + environment: colors.red(environment), + }) + }, + clearScreen(type) { + return config.logger.clearScreen(type) + }, + hasErrorLogged(error) { + return config.logger.hasErrorLogged(error) + }, + } + } +} + +export class BaseEnvironment extends PartialEnvironment { + get plugins(): Plugin[] { + if (!this._plugins) + throw new Error( + `${this.name} environment.plugins called before initialized`, + ) + return this._plugins + } + + /** + * @internal + */ + _plugins: Plugin[] | undefined + /** + * @internal + */ + _initiated: boolean = false + + constructor( + name: string, + config: ResolvedConfig, + options: ResolvedEnvironmentOptions = config.environments[name], + ) { + super(name, config, options) + } +} + +/** + * This is used both to avoid users to hardcode conditions like + * !scan && !build => dev + */ +export class FutureCompatEnvironment extends BaseEnvironment { + mode = 'futureCompat' as const +} + +const environmentColors = [ + colors.blue, + colors.magenta, + colors.green, + colors.gray, +] diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 75f207482d010c..cd78f2baf8b1a2 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -8,7 +8,6 @@ import type { LoggingFunction, ModuleFormat, OutputOptions, - Plugin, RollupBuild, RollupError, RollupLog, @@ -25,10 +24,16 @@ import { withTrailingSlash } from '../shared/utils' import { DEFAULT_ASSETS_INLINE_LIMIT, ESBUILD_MODULES_TARGET, + ROLLUP_HOOKS, VERSION, } from './constants' -import type { InlineConfig, ResolvedConfig } from './config' -import { resolveConfig } from './config' +import type { + EnvironmentOptions, + InlineConfig, + ResolvedConfig, + ResolvedEnvironmentOptions, +} from './config' +import { getDefaultResolvedEnvironmentOptions, resolveConfig } from './config' import { buildReporterPlugin } from './plugins/reporter' import { buildEsbuildPlugin } from './plugins/esbuild' import { type TerserOptions, terserPlugin } from './plugins/terser' @@ -43,12 +48,13 @@ import { partialEncodeURIPath, requireResolveFromRootWithFallback, } from './utils' +import { resolveEnvironmentPlugins } from './plugin' import { manifestPlugin } from './plugins/manifest' import type { Logger } from './logger' import { dataURIPlugin } from './plugins/dataUri' import { buildImportAnalysisPlugin } from './plugins/importAnalysisBuild' import { ssrManifestPlugin } from './ssr/ssrManifestPlugin' -import { loadFallbackPlugin } from './plugins/loadFallback' +import { buildLoadFallbackPlugin } from './plugins/loadFallback' import { findNearestPackageData } from './packages' import type { PackageCache } from './packages' import { @@ -60,8 +66,11 @@ import { completeSystemWrapPlugin } from './plugins/completeSystemWrap' import { mergeConfig } from './publicUtils' import { webWorkerPostPlugin } from './plugins/worker' import { getHookHandler } from './plugins' +import { BaseEnvironment } from './baseEnvironment' +import type { Plugin, PluginContext } from './plugin' +import type { RollupPluginHooks } from './typeUtils' -export interface BuildOptions { +export interface BuildEnvironmentOptions { /** * Compatibility transform target. The transform is performed with esbuild * and the lowest supported target is es2015/es6. Note this only handles @@ -206,13 +215,6 @@ export interface BuildOptions { * @default false */ manifest?: boolean | string - /** - * Build in library mode. The value should be the global name of the lib in - * UMD mode. This will produce esm + cjs + umd bundle formats with default - * configurations that are suitable for distributing libraries. - * @default false - */ - lib?: LibraryOptions | false /** * Produce SSR oriented build. Note this requires specifying SSR entry via * `rollupOptions.input`. @@ -228,8 +230,16 @@ export interface BuildOptions { /** * Emit assets during SSR. * @default false + * @deprecated use emitAssets */ ssrEmitAssets?: boolean + /** + * Emit assets during build. Frameworks can set environments.ssr.build.emitAssets + * By default, it is true for the client and false for other environments. + * TODO: Should this be true for all environments by default? Or should this be + * controlled by the builder so so we can avoid emitting duplicated assets. + */ + emitAssets?: boolean /** * Set to false to disable reporting compressed chunk sizes. * Can slightly improve build speed. @@ -247,6 +257,23 @@ export interface BuildOptions { * @default null */ watch?: WatcherOptions | null + /** + * create the Build Environment instance + */ + createEnvironment?: ( + name: string, + config: ResolvedConfig, + ) => Promise | BuildEnvironment +} + +export interface BuildOptions extends BuildEnvironmentOptions { + /** + * Build in library mode. The value should be the global name of the lib in + * UMD mode. This will produce esm + cjs + umd bundle formats with default + * configurations that are suitable for distributing libraries. + * @default false + */ + lib?: LibraryOptions | false } export interface LibraryOptions { @@ -301,78 +328,99 @@ export type ResolveModulePreloadDependenciesFn = ( }, ) => string[] +export interface ResolvedBuildEnvironmentOptions + extends Required> { + modulePreload: false | ResolvedModulePreloadOptions +} + export interface ResolvedBuildOptions extends Required> { modulePreload: false | ResolvedModulePreloadOptions } export function resolveBuildOptions( - raw: BuildOptions | undefined, + raw: BuildOptions, logger: Logger, root: string, ): ResolvedBuildOptions { + const libMode = raw.lib ?? false + const buildOptions = resolveBuildEnvironmentOptions( + raw, + logger, + root, + undefined, + libMode, + ) + return { ...buildOptions, lib: libMode } +} + +export function resolveBuildEnvironmentOptions( + raw: BuildEnvironmentOptions, + logger: Logger, + root: string, + environmentName: string | undefined, + libMode: false | LibraryOptions = false, +): ResolvedBuildEnvironmentOptions { const deprecatedPolyfillModulePreload = raw?.polyfillModulePreload - if (raw) { - const { polyfillModulePreload, ...rest } = raw - raw = rest - if (deprecatedPolyfillModulePreload !== undefined) { - logger.warn( - 'polyfillModulePreload is deprecated. Use modulePreload.polyfill instead.', - ) - } - if ( - deprecatedPolyfillModulePreload === false && - raw.modulePreload === undefined - ) { - raw.modulePreload = { polyfill: false } - } + const { polyfillModulePreload, ...rest } = raw + raw = rest + if (deprecatedPolyfillModulePreload !== undefined) { + logger.warn( + 'polyfillModulePreload is deprecated. Use modulePreload.polyfill instead.', + ) + } + if ( + deprecatedPolyfillModulePreload === false && + raw.modulePreload === undefined + ) { + raw.modulePreload = { polyfill: false } } - const modulePreload = raw?.modulePreload + const modulePreload = raw.modulePreload const defaultModulePreload = { polyfill: true, } - const defaultBuildOptions: BuildOptions = { + const defaultBuildEnvironmentOptions: BuildEnvironmentOptions = { outDir: 'dist', assetsDir: 'assets', assetsInlineLimit: DEFAULT_ASSETS_INLINE_LIMIT, - cssCodeSplit: !raw?.lib, + cssCodeSplit: !libMode, sourcemap: false, rollupOptions: {}, - minify: raw?.ssr ? false : 'esbuild', + minify: raw.ssr ? false : 'esbuild', terserOptions: {}, write: true, emptyOutDir: null, copyPublicDir: true, manifest: false, - lib: false, ssr: false, ssrManifest: false, ssrEmitAssets: false, + emitAssets: environmentName === 'client', reportCompressedSize: true, chunkSizeWarningLimit: 500, watch: null, } - const userBuildOptions = raw - ? mergeConfig(defaultBuildOptions, raw) - : defaultBuildOptions + const userBuildEnvironmentOptions = raw + ? mergeConfig(defaultBuildEnvironmentOptions, raw) + : defaultBuildEnvironmentOptions // @ts-expect-error Fallback options instead of merging - const resolved: ResolvedBuildOptions = { + const resolved: ResolvedBuildEnvironmentOptions = { target: 'modules', cssTarget: false, - ...userBuildOptions, + ...userBuildEnvironmentOptions, commonjsOptions: { include: [/node_modules/], extensions: ['.js', '.cjs'], - ...userBuildOptions.commonjsOptions, + ...userBuildEnvironmentOptions.commonjsOptions, }, dynamicImportVarsOptions: { warnOnError: true, exclude: [/node_modules/], - ...userBuildOptions.dynamicImportVarsOptions, + ...userBuildEnvironmentOptions.dynamicImportVarsOptions, }, // Resolve to false | object modulePreload: @@ -450,12 +498,12 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ ...(options.minify ? [terserPlugin(config)] : []), ...(!config.isWorker ? [ - ...(options.manifest ? [manifestPlugin(config)] : []), + ...(options.manifest ? [manifestPlugin()] : []), ...(options.ssrManifest ? [ssrManifestPlugin(config)] : []), buildReporterPlugin(config), ] : []), - loadFallbackPlugin(), + buildLoadFallbackPlugin(), ], } } @@ -467,17 +515,53 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ export async function build( inlineConfig: InlineConfig = {}, ): Promise { - const config = await resolveConfig( + const builder = await createBuilder(inlineConfig) + + if (builder.config.build.lib) { + // TODO: temporal workaround. Should we support `libraries: Record` + // to build multiple libraries and be able to target different environments (for example for a Svelte components + // library generating both client and SSR builds)? + return buildEnvironment( + builder.config, + builder.environments.client, + builder.config.build.lib, + ) + } else { + const ssr = !!builder.config.build.ssr + const environment = builder.environments[ssr ? 'ssr' : 'client'] + return builder.build(environment) + } +} + +function resolveConfigToBuild( + inlineConfig: InlineConfig = {}, + patchConfig?: (config: ResolvedConfig) => void, + patchPlugins?: (resolvedPlugins: Plugin[]) => void, +) { + return resolveConfig( inlineConfig, 'build', 'production', 'production', + false, + patchConfig, + patchPlugins, ) +} + +/** + * Build an App environment, or a App library (if libraryOptions is provided) + **/ +export async function buildEnvironment( + config: ResolvedConfig, + environment: BuildEnvironment, + libOptions: LibraryOptions | false = false, +): Promise { const options = config.build - const ssr = !!options.ssr - const libOptions = options.lib + const { logger } = environment + const ssr = environment.name !== 'client' - config.logger.info( + logger.info( colors.cyan( `vite v${VERSION} ${colors.green( `building ${ssr ? `SSR bundle ` : ``}for ${config.mode}...`, @@ -524,10 +608,11 @@ export async function build( const outDir = resolve(options.outDir) - // inject ssr arg to plugin load/transform hooks - const plugins = ( - ssr ? config.plugins.map((p) => injectSsrFlagToHooks(p)) : config.plugins - ) as Plugin[] + // inject environment and ssr arg to plugin load/transform hooks + // TODO: rework lib mode + const plugins = (libOptions ? config : environment).plugins.map((p) => + injectEnvironmentToHooks(environment, p), + ) const rollupOptions: RollupOptions = { preserveEntrySignatures: ssr @@ -602,7 +687,7 @@ export async function build( const outputBuildError = (e: RollupError) => { enhanceRollupError(e) clearLine() - config.logger.error(e.message, { error: e }) + logger.error(e.message, { error: e }) } let bundle: RollupBuild | undefined @@ -611,7 +696,7 @@ export async function build( const buildOutputOptions = (output: OutputOptions = {}): OutputOptions => { // @ts-expect-error See https://github.com/vitejs/vite/issues/5812#issuecomment-984345618 if (output.output) { - config.logger.warn( + logger.warn( `You've set "rollupOptions.output.output" in your config. ` + `This is deprecated and will override all Vite.js default output options. ` + `Please use "rollupOptions.output" instead.`, @@ -624,7 +709,7 @@ export async function build( ) } if (output.sourcemap) { - config.logger.warnOnce( + logger.warnOnce( colors.yellow( `Vite does not support "rollupOptions.output.sourcemap". ` + `Please use "build.sourcemap" instead.`, @@ -632,12 +717,9 @@ export async function build( ) } - const ssrNodeBuild = ssr && config.ssr.target === 'node' - const ssrWorkerBuild = ssr && config.ssr.target === 'webworker' - const format = output.format || 'es' const jsExt = - ssrNodeBuild || libOptions + !environment.options.webCompatible || libOptions ? resolveOutputJsExtension( format, findNearestPackageData(config.root, config.packageCache)?.data @@ -678,7 +760,11 @@ export async function build( inlineDynamicImports: output.format === 'umd' || output.format === 'iife' || - (ssrWorkerBuild && + // TODO: We need an abstraction for non-client environments? + // We should remove the explicit 'client' hcek here. + // Or maybe `inlineDynamicImports` should be an environment option? + (environment.name !== 'client' && + environment.options.webCompatible && (typeof input === 'string' || Object.keys(input).length === 1)), ...output, } @@ -688,7 +774,7 @@ export async function build( const outputs = resolveBuildOutputs( options.rollupOptions?.output, libOptions, - config.logger, + logger, ) const normalizedOutputs: OutputOptions[] = [] @@ -709,12 +795,12 @@ export async function build( options.emptyOutDir, config.root, resolvedOutDirs, - config.logger, + logger, ) // watch file changes with rollup if (config.build.watch) { - config.logger.info(colors.cyan(`\nwatching for file changes...`)) + logger.info(colors.cyan(`\nwatching for file changes...`)) const resolvedChokidarOptions = resolveChokidarOptions( config, @@ -735,13 +821,13 @@ export async function build( watcher.on('event', (event) => { if (event.code === 'BUNDLE_START') { - config.logger.info(colors.cyan(`\nbuild started...`)) + logger.info(colors.cyan(`\nbuild started...`)) if (options.write) { prepareOutDir(resolvedOutDirs, emptyOutDir, config) } } else if (event.code === 'BUNDLE_END') { event.result.close() - config.logger.info(colors.cyan(`built in ${event.duration}ms.`)) + logger.info(colors.cyan(`built in ${event.duration}ms.`)) } else if (event.code === 'ERROR') { outputBuildError(event.error) } @@ -763,7 +849,7 @@ export async function build( for (const output of normalizedOutputs) { res.push(await bundle[options.write ? 'write' : 'generate'](output)) } - config.logger.info( + logger.info( `${colors.green(`✓ built in ${displayTime(Date.now() - startTime)}`)}`, ) return Array.isArray(outputs) ? res : res[0] @@ -771,7 +857,7 @@ export async function build( enhanceRollupError(e) clearLine() if (startTime) { - config.logger.error( + logger.error( `${colors.red('x')} Build failed in ${displayTime(Date.now() - startTime)}`, ) startTime = undefined @@ -1033,22 +1119,51 @@ function isExternal(id: string, test: string | RegExp) { } } -function injectSsrFlagToHooks(plugin: Plugin): Plugin { +// TODO: Could we get Rollup to let us extends PluginContext in a more performant way? +export function injectEnvironmentToHooks( + environment: BuildEnvironment, + plugin: Plugin, +): Plugin { const { resolveId, load, transform } = plugin - return { - ...plugin, - resolveId: wrapSsrResolveId(resolveId), - load: wrapSsrLoad(load), - transform: wrapSsrTransform(transform), + + const clone = { ...plugin } + + for (const hook of Object.keys(clone) as RollupPluginHooks[]) { + switch (hook) { + case 'resolveId': + clone[hook] = wrapEnvironmentResolveId(environment, resolveId) + break + case 'load': + clone[hook] = wrapEnvironmentLoad(environment, load) + break + case 'transform': + clone[hook] = wrapEnvironmentTransform(environment, transform) + break + default: + if (ROLLUP_HOOKS.includes(hook)) { + ;(clone as any)[hook] = wrapEnvironmentHook(environment, clone[hook]) + } + break + } } + + return clone } -function wrapSsrResolveId(hook?: Plugin['resolveId']): Plugin['resolveId'] { +function wrapEnvironmentResolveId( + environment: BuildEnvironment, + hook?: Plugin['resolveId'], +): Plugin['resolveId'] { if (!hook) return const fn = getHookHandler(hook) const handler: Plugin['resolveId'] = function (id, importer, options) { - return fn.call(this, id, importer, injectSsrFlag(options)) + return fn.call( + injectEnvironmentInContext(this, environment), + id, + importer, + injectSsrFlag(options, environment), + ) } if ('handler' in hook) { @@ -1061,13 +1176,19 @@ function wrapSsrResolveId(hook?: Plugin['resolveId']): Plugin['resolveId'] { } } -function wrapSsrLoad(hook?: Plugin['load']): Plugin['load'] { +function wrapEnvironmentLoad( + environment: BuildEnvironment, + hook?: Plugin['load'], +): Plugin['load'] { if (!hook) return const fn = getHookHandler(hook) const handler: Plugin['load'] = function (id, ...args) { - // @ts-expect-error: Receiving options param to be future-proof if Rollup adds it - return fn.call(this, id, injectSsrFlag(args[0])) + return fn.call( + injectEnvironmentInContext(this, environment), + id, + injectSsrFlag(args[0], environment), + ) } if ('handler' in hook) { @@ -1080,13 +1201,20 @@ function wrapSsrLoad(hook?: Plugin['load']): Plugin['load'] { } } -function wrapSsrTransform(hook?: Plugin['transform']): Plugin['transform'] { +function wrapEnvironmentTransform( + environment: BuildEnvironment, + hook?: Plugin['transform'], +): Plugin['transform'] { if (!hook) return const fn = getHookHandler(hook) const handler: Plugin['transform'] = function (code, importer, ...args) { - // @ts-expect-error: Receiving options param to be future-proof if Rollup adds it - return fn.call(this, code, importer, injectSsrFlag(args[0])) + return fn.call( + injectEnvironmentInContext(this, environment), + code, + importer, + injectSsrFlag(args[0], environment), + ) } if ('handler' in hook) { @@ -1099,10 +1227,48 @@ function wrapSsrTransform(hook?: Plugin['transform']): Plugin['transform'] { } } +function wrapEnvironmentHook( + environment: BuildEnvironment, + hook?: Plugin[HookName], +): Plugin[HookName] { + if (!hook) return + + const fn = getHookHandler(hook) + if (typeof fn !== 'function') return hook + + const handler: Plugin[HookName] = function ( + this: PluginContext, + ...args: any[] + ) { + return fn.call(injectEnvironmentInContext(this, environment), ...args) + } + + if ('handler' in hook) { + return { + ...hook, + handler, + } as Plugin[HookName] + } else { + return handler + } +} + +function injectEnvironmentInContext( + context: Context, + environment: BuildEnvironment, +) { + context.environment ??= environment + return context +} + function injectSsrFlag>( options?: T, -): T & { ssr: boolean } { - return { ...(options ?? {}), ssr: true } as T & { ssr: boolean } + environment?: BuildEnvironment, +): T & { ssr?: boolean } { + const ssr = environment ? environment.name !== 'client' : true + return { ...(options ?? {}), ssr } as T & { + ssr?: boolean + } } /* @@ -1189,6 +1355,7 @@ export type RenderBuiltAssetUrl = ( }, ) => string | { relative?: boolean; runtime?: string } | undefined +// TODO: experimental.renderBuiltUrl => environment.build.renderBuiltUrl? export function toOutputFilePathInJS( filename: string, type: 'asset' | 'public', @@ -1288,3 +1455,158 @@ function areSeparateFolders(a: string, b: string) { !nb.startsWith(withTrailingSlash(na)) ) } + +export class BuildEnvironment extends BaseEnvironment { + mode = 'build' as const + + constructor( + name: string, + config: ResolvedConfig, + setup?: { + options?: EnvironmentOptions + }, + ) { + // TODO: move this to the base Environment class? + let options = + config.environments[name] ?? getDefaultResolvedEnvironmentOptions(config) + if (setup?.options) { + options = mergeConfig( + options, + setup?.options, + ) as ResolvedEnvironmentOptions + } + super(name, config, options) + } + + // TODO: This could be sync, discuss if applyToEnvironment should support async + async init(): Promise { + if (this._initiated) { + return + } + this._initiated = true + this._plugins = resolveEnvironmentPlugins(this) + } +} + +export interface ViteBuilder { + environments: Record + config: ResolvedConfig + buildApp(): Promise + build( + environment: BuildEnvironment, + ): Promise +} + +export interface BuilderOptions { + sharedConfigBuild?: boolean + sharedPlugins?: boolean + entireApp?: boolean + buildApp?: (builder: ViteBuilder) => Promise +} + +async function defaultBuildApp(builder: ViteBuilder): Promise { + for (const environment of Object.values(builder.environments)) { + await builder.build(environment) + } +} + +export function resolveBuilderOptions( + options: BuilderOptions = {}, +): ResolvedBuilderOptions { + return { + sharedConfigBuild: options.sharedConfigBuild ?? false, + sharedPlugins: options.sharedPlugins ?? false, + entireApp: options.entireApp ?? false, + buildApp: options.buildApp ?? defaultBuildApp, + } +} + +export type ResolvedBuilderOptions = Required + +export async function createBuilder( + inlineConfig: InlineConfig = {}, +): Promise { + const config = await resolveConfigToBuild(inlineConfig) + + const environments: Record = {} + + const builder: ViteBuilder = { + environments, + config, + async buildApp() { + if (config.build.watch) { + throw new Error( + 'Watch mode is not yet supported in viteBuilder.buildApp()', + ) + } + return config.builder.buildApp(builder) + }, + async build(environment: BuildEnvironment) { + return buildEnvironment(environment.config, environment) + }, + } + + for (const name of Object.keys(config.environments)) { + const environmentOptions = config.environments[name] + const createEnvironment = + environmentOptions.build?.createEnvironment ?? + ((name: string, config: ResolvedConfig) => + new BuildEnvironment(name, config)) + + // We need to resolve the config again so we can properly merge options + // and get a new set of plugins for each build environment. The ecosystem + // expects plugins to be run for the same environment once they are created + // and to process a single bundle at a time (contrary to dev mode where + // plugins are built to handle multiple environments concurrently). + let environmentConfig = config + if (!config.builder.sharedConfigBuild) { + const patchConfig = (resolved: ResolvedConfig) => { + // Until the ecosystem updates to use `environment.options.build` instead of `config.build`, + // we need to make override `config.build` for the current environment. + // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later + // remove the default values that shouldn't be used at all once the config is resolved + ;(resolved.build as ResolvedBuildOptions) = { + ...resolved.environments[name].build, + lib: false, + } + } + const patchPlugins = (resolvedPlugins: Plugin[]) => { + // Force opt-in shared plugins + const environmentPlugins = [...resolvedPlugins] + let validMixedPlugins = true + for (let i = 0; i < environmentPlugins.length; i++) { + const environmentPlugin = environmentPlugins[i] + const sharedPlugin = config.plugins[i] + if ( + config.builder.sharedPlugins || + environmentPlugin.sharedDuringBuild + ) { + if (environmentPlugin.name !== sharedPlugin.name) { + validMixedPlugins = false + break + } + environmentPlugins[i] = sharedPlugin + } + } + if (validMixedPlugins) { + for (let i = 0; i < environmentPlugins.length; i++) { + resolvedPlugins[i] = environmentPlugins[i] + } + } + } + environmentConfig = await resolveConfigToBuild( + inlineConfig, + patchConfig, + patchPlugins, + ) + } + + const environment = await createEnvironment(name, environmentConfig) + + await environment.init() + + environments[name] = environment + } + + return builder +} diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index f0fa2092110175..290524284b7acd 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -4,7 +4,7 @@ import { performance } from 'node:perf_hooks' import { cac } from 'cac' import colors from 'picocolors' import { VERSION } from './constants' -import type { BuildOptions } from './build' +import type { BuildEnvironmentOptions } from './build' import type { ServerOptions } from './server' import type { CLIShortcut } from './shortcuts' import type { LogLevel } from './logger' @@ -31,6 +31,10 @@ interface GlobalCLIOptions { force?: boolean } +interface BuilderCLIOptions { + app?: boolean +} + let profileSession = global.__vite_profile_session let profileCount = 0 @@ -70,7 +74,7 @@ const filterDuplicateOptions = (options: T) => { /** * removing global flags before passing as command specific sub-configs */ -function cleanOptions( +function cleanGlobalCLIOptions( options: Options, ): Omit { const ret = { ...options } @@ -102,6 +106,17 @@ function cleanOptions( return ret } +/** + * removing builder flags before passing as command specific sub-configs + */ +function cleanBuilderCLIOptions( + options: Options, +): Omit { + const ret = { ...options } + delete ret.app + return ret +} + /** * host may be a number (like 0), should convert to string */ @@ -161,7 +176,7 @@ cli logLevel: options.logLevel, clearScreen: options.clearScreen, optimizeDeps: { force: options.force }, - server: cleanOptions(options), + server: cleanGlobalCLIOptions(options), }) if (!server.httpServer) { @@ -263,13 +278,21 @@ cli `[boolean] force empty outDir when it's outside of root`, ) .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`) - .action(async (root: string, options: BuildOptions & GlobalCLIOptions) => { - filterDuplicateOptions(options) - const { build } = await import('./build') - const buildOptions: BuildOptions = cleanOptions(options) + .option('--environment [name]', `[string] build a single environment`) + .option('--app', `[boolean] same as builder.entireApp`) + .action( + async ( + root: string, + options: BuildEnvironmentOptions & BuilderCLIOptions & GlobalCLIOptions, + ) => { + filterDuplicateOptions(options) + const { createBuilder, buildEnvironment } = await import('./build') - try { - await build({ + const buildOptions: BuildEnvironmentOptions = cleanGlobalCLIOptions( + cleanBuilderCLIOptions(options), + ) + + const config = { root, base: options.base, mode: options.mode, @@ -277,17 +300,36 @@ cli logLevel: options.logLevel, clearScreen: options.clearScreen, build: buildOptions, - }) - } catch (e) { - createLogger(options.logLevel).error( - colors.red(`error during build:\n${e.stack}`), - { error: e }, - ) - process.exit(1) - } finally { - stopProfiler((message) => createLogger(options.logLevel).info(message)) - } - }) + } + + try { + const builder = await createBuilder(config) + // TODO: Backward compatibility with lib and single environment build + // Ideally we would move to only building the entire app with this command + if (builder.config.build.lib) { + await buildEnvironment( + builder.config, + builder.environments.client, + builder.config.build.lib, + ) + } else if (builder.config.builder.entireApp || options.app) { + await builder.buildApp() + } else { + const ssr = !!builder.config.build.ssr + const environment = builder.environments[ssr ? 'ssr' : 'client'] + await builder.build(environment) + } + } catch (e) { + createLogger(options.logLevel).error( + colors.red(`error during build:\n${e.stack}`), + { error: e }, + ) + process.exit(1) + } finally { + stopProfiler((message) => createLogger(options.logLevel).info(message)) + } + }, + ) // optimize cli diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 2b0f6d28360987..5b90dede236a96 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -9,7 +9,7 @@ import colors from 'picocolors' import type { Alias, AliasOptions } from 'dep-types/alias' import aliasPlugin from '@rollup/plugin-alias' import { build } from 'esbuild' -import type { RollupOptions } from 'rollup' +import type { PartialResolvedId, RollupOptions } from 'rollup' import { withTrailingSlash } from '../shared/utils' import { CLIENT_ENTRY, @@ -20,15 +20,29 @@ import { ENV_ENTRY, FS_PREFIX, } from './constants' -import type { HookHandler, Plugin, PluginWithRequiredHook } from './plugin' import type { + HookHandler, + Plugin, + PluginOption, + PluginWithRequiredHook, +} from './plugin' +import type { + BuildEnvironmentOptions, BuildOptions, + BuilderOptions, RenderBuiltAssetUrl, + ResolvedBuildEnvironmentOptions, ResolvedBuildOptions, + ResolvedBuilderOptions, +} from './build' +import { + resolveBuildEnvironmentOptions, + resolveBuildOptions, + resolveBuilderOptions, } from './build' -import { resolveBuildOptions } from './build' import type { ResolvedServerOptions, ServerOptions } from './server' import { resolveServerOptions } from './server' +import type { DevEnvironment } from './server/environment' import type { PreviewOptions, ResolvedPreviewOptions } from './preview' import { resolvePreviewOptions } from './preview' import { @@ -43,6 +57,7 @@ import { isBuiltin, isExternalUrl, isFilePathESM, + isInNodeModules, isNodeBuiltin, isObject, isParentDirectory, @@ -63,15 +78,17 @@ import type { InternalResolveOptions, ResolveOptions } from './plugins/resolve' import { resolvePlugin, tryNodeResolve } from './plugins/resolve' import type { LogLevel, Logger } from './logger' import { createLogger } from './logger' -import type { DepOptimizationConfig, DepOptimizationOptions } from './optimizer' +import type { DepOptimizationOptions } from './optimizer' import type { JsonOptions } from './plugins/json' -import type { PluginContainer } from './server/pluginContainer' -import { createPluginContainer } from './server/pluginContainer' +import type { EnvironmentPluginContainer } from './server/pluginContainer' +import { createEnvironmentPluginContainer } from './server/pluginContainer' import type { PackageCache } from './packages' import { findNearestPackageData } from './packages' import { loadEnv, resolveEnvPrefix } from './env' import type { ResolvedSSROptions, SSROptions } from './ssr' import { resolveSSROptions } from './ssr' +import { FutureCompatEnvironment } from './baseEnvironment' +import type { FutureDeprecationWarningsOptions } from './deprecations' const debug = createDebugger('vite:config') const promisifiedRealpath = promisify(fs.realpath) @@ -120,15 +137,142 @@ export function defineConfig(config: UserConfigExport): UserConfigExport { return config } -export type PluginOption = - | Plugin - | false - | null - | undefined - | PluginOption[] - | Promise +export interface DevEnvironmentOptions { + /** + * Files to be pre-transformed. Supports glob patterns. + */ + warmup?: string[] + /** + * Pre-transform known direct imports + * defaults to true for the client environment, false for the rest + */ + preTransformRequests?: boolean + /** + * Enables sourcemaps during dev + * @default { js: true } + * @experimental + */ + sourcemap?: boolean | { js?: boolean; css?: boolean } + /** + * Whether or not to ignore-list source files in the dev server sourcemap, used to populate + * the [`x_google_ignoreList` source map extension](https://developer.chrome.com/blog/devtools-better-angular-debugging/#the-x_google_ignorelist-source-map-extension). + * + * By default, it excludes all paths containing `node_modules`. You can pass `false` to + * disable this behavior, or, for full control, a function that takes the source path and + * sourcemap path and returns whether to ignore the source path. + */ + sourcemapIgnoreList?: + | false + | ((sourcePath: string, sourcemapPath: string) => boolean) + + /** + * Optimize deps config + */ + optimizeDeps?: DepOptimizationOptions + + /** + * create the Dev Environment instance + */ + createEnvironment?: ( + name: string, + config: ResolvedConfig, + ) => Promise | DevEnvironment + + /** + * For environments that support a full-reload, like the client, we can short-circuit when + * restarting the server throwing early to stop processing current files. We avoided this for + * SSR requests. Maybe this is no longer needed. + * @experimental + */ + recoverable?: boolean + + /** + * For environments associated with a module runner. + * By default it is true for the client environment and false for non-client environments. + * This option can also be used instead of the removed config.experimental.skipSsrTransform. + */ + moduleRunnerTransform?: boolean + + /** + * Defaults to true for the client environment and false for others, following node permissive + * security model. + * TODO: We need to move at least server.fs.strict to dev options because we want to restrict + * fs access from the client, but keep it open for SSR running on node. For now, we moved + * the check to use environment.nodeCompatible + * Should we only have a boolean toggle per environment and a keep allow/deny configuration + * in server.fs, or move the whole configuration to the environment? + */ + // fs: { strict?: boolean, allow, deny } +} + +export type ResolvedDevEnvironmentOptions = Required< + Omit +> & { + // TODO: Should we set the default at config time? For now, it is defined on server init + createEnvironment: + | (( + name: string, + config: ResolvedConfig, + ) => Promise | DevEnvironment) + | undefined +} + +type EnvironmentResolveOptions = ResolveOptions & { + alias?: AliasOptions +} + +export interface SharedEnvironmentOptions { + /** + * Configure resolver + */ + resolve?: EnvironmentResolveOptions + /** + * Runtime Compatibility + * Temporal options, we should remove these in favor of fine-grained control + */ + nodeCompatible?: boolean + webCompatible?: boolean // was ssr.target === 'webworker' + /** + * Should Vite inject timestamp if module is invalidated + * Disabling this will break built-in HMR support + * @experimental + * @default true + */ + injectInvalidationTimestamp?: boolean +} -export interface UserConfig { +export interface EnvironmentOptions extends SharedEnvironmentOptions { + /** + * Dev specific options + */ + dev?: DevEnvironmentOptions + /** + * Build specific options + */ + build?: BuildEnvironmentOptions +} + +export type ResolvedEnvironmentResolveOptions = + Required + +export type ResolvedEnvironmentOptions = { + resolve: ResolvedEnvironmentResolveOptions + nodeCompatible: boolean + webCompatible: boolean + injectInvalidationTimestamp: boolean + dev: ResolvedDevEnvironmentOptions + build: ResolvedBuildEnvironmentOptions +} + +export type DefaultEnvironmentOptions = Omit< + EnvironmentOptions, + 'build' | 'nodeCompatible' | 'webCompatible' +> & { + // Includes lib mode support + build?: BuildOptions +} + +export interface UserConfig extends DefaultEnvironmentOptions { /** * Project root directory. Can be an absolute path, or a path relative from * the location of the config file itself. @@ -173,10 +317,6 @@ export interface UserConfig { * Array of vite plugins to use. */ plugins?: PluginOption[] - /** - * Configure resolver - */ - resolve?: ResolveOptions & { alias?: AliasOptions } /** * HTML related options */ @@ -199,25 +339,17 @@ export interface UserConfig { */ assetsInclude?: string | RegExp | (string | RegExp)[] /** - * Server specific options, e.g. host, port, https... + * Builder specific options */ - server?: ServerOptions + builder?: BuilderOptions /** - * Build specific options + * Server specific options, e.g. host, port, https... */ - build?: BuildOptions + server?: ServerOptions /** * Preview specific options, e.g. host, port, https... */ preview?: PreviewOptions - /** - * Dep optimization options - */ - optimizeDeps?: DepOptimizationOptions - /** - * SSR specific options - */ - ssr?: SSROptions /** * Experimental features * @@ -226,6 +358,10 @@ export interface UserConfig { * @experimental */ experimental?: ExperimentalOptions + /** + * Options to opt-in to future behavior + */ + future?: FutureOptions /** * Legacy options * @@ -280,6 +416,20 @@ export interface UserConfig { 'plugins' | 'input' | 'onwarn' | 'preserveEntrySignatures' > } + /** + * Dep optimization options + */ + optimizeDeps?: DepOptimizationOptions + /** + * SSR specific options + * We could make SSROptions be a EnvironmentOptions if we can abstract + * external/noExternal for environments in general. + */ + ssr?: SSROptions + /** + * Environment overrides + */ + environments?: Record /** * Whether your application is a Single Page Application (SPA), * a Multi-Page Application (MPA), or Custom Application (SSR @@ -298,6 +448,15 @@ export interface HTMLOptions { cspNonce?: string } +export interface FutureOptions { + /** + * Emit warning messages for deprecated/will-deprecated features at runtime. + * + * Setting to `true` to enable all warnings + */ + deprecationWarnings?: boolean | FutureDeprecationWarningsOptions +} + export interface ExperimentalOptions { /** * Append fake `&lang.(ext)` when queries are specified, to preserve the file extension for following plugins to process. @@ -345,7 +504,9 @@ export interface LegacyOptions { export interface ResolvedWorkerOptions { format: 'es' | 'iife' - plugins: (bundleChain: string[]) => Promise + plugins: ( + bundleChain: string[], + ) => Promise<{ plugins: Plugin[]; config: ResolvedConfig }> rollupOptions: RollupOptions } @@ -357,7 +518,14 @@ export interface InlineConfig extends UserConfig { export type ResolvedConfig = Readonly< Omit< UserConfig, - 'plugins' | 'css' | 'assetsInclude' | 'optimizeDeps' | 'worker' | 'build' + | 'plugins' + | 'css' + | 'assetsInclude' + | 'optimizeDeps' + | 'worker' + | 'build' + | 'dev' + | 'environments' > & { configFile: string | undefined configFileDependencies: string[] @@ -386,6 +554,8 @@ export type ResolvedConfig = Readonly< css: ResolvedCSSOptions esbuild: ESBuildOptions | false server: ResolvedServerOptions + dev: ResolvedDevEnvironmentOptions + builder: ResolvedBuilderOptions build: ResolvedBuildOptions preview: ResolvedPreviewOptions ssr: ResolvedSSROptions @@ -398,9 +568,94 @@ export type ResolvedConfig = Readonly< worker: ResolvedWorkerOptions appType: AppType experimental: ExperimentalOptions + environments: Record } & PluginHookUtils > +export function resolveDevEnvironmentOptions( + dev: DevEnvironmentOptions | undefined, + preserverSymlinks: boolean, + environmentName: string | undefined, + // Backward compatibility + skipSsrTransform?: boolean, +): ResolvedDevEnvironmentOptions { + return { + sourcemap: dev?.sourcemap ?? { js: true }, + sourcemapIgnoreList: + dev?.sourcemapIgnoreList === false + ? () => false + : dev?.sourcemapIgnoreList || isInNodeModules, + preTransformRequests: + dev?.preTransformRequests ?? environmentName === 'client', + warmup: dev?.warmup ?? [], + optimizeDeps: resolveDepOptimizationOptions( + dev?.optimizeDeps, + preserverSymlinks, + environmentName, + ), + createEnvironment: dev?.createEnvironment, + recoverable: dev?.recoverable ?? environmentName === 'client', + moduleRunnerTransform: + dev?.moduleRunnerTransform ?? + (skipSsrTransform !== undefined && environmentName === 'ssr' + ? skipSsrTransform + : environmentName !== 'client'), + } +} + +function resolveEnvironmentOptions( + options: EnvironmentOptions, + resolvedRoot: string, + logger: Logger, + environmentName: string, + // Backward compatibility + skipSsrTransform?: boolean, +): ResolvedEnvironmentOptions { + const resolve = resolveEnvironmentResolveOptions(options.resolve, logger) + return { + resolve, + nodeCompatible: options.nodeCompatible ?? environmentName !== 'client', + webCompatible: options.webCompatible ?? environmentName === 'client', + injectInvalidationTimestamp: + options.injectInvalidationTimestamp ?? environmentName === 'client', + dev: resolveDevEnvironmentOptions( + options.dev, + resolve.preserveSymlinks, + environmentName, + skipSsrTransform, + ), + build: resolveBuildEnvironmentOptions( + options.build ?? {}, + logger, + resolvedRoot, + environmentName, + ), + } +} + +export function getDefaultEnvironmentOptions( + config: UserConfig, +): EnvironmentOptions { + return { + resolve: config.resolve, + dev: config.dev, + build: config.build, + } +} + +export function getDefaultResolvedEnvironmentOptions( + config: ResolvedConfig, +): ResolvedEnvironmentOptions { + return { + resolve: config.resolve, + nodeCompatible: true, + webCompatible: false, + injectInvalidationTimestamp: false, + dev: config.dev, + build: config.build, + } +} + export interface PluginHookUtils { getSortedPlugins: ( hookName: K, @@ -445,12 +700,86 @@ function checkBadCharactersInPath(path: string, logger: Logger): void { } } +const clientAlias = [ + { + find: /^\/?@vite\/env/, + replacement: path.posix.join(FS_PREFIX, normalizePath(ENV_ENTRY)), + }, + { + find: /^\/?@vite\/client/, + replacement: path.posix.join(FS_PREFIX, normalizePath(CLIENT_ENTRY)), + }, +] + +function resolveEnvironmentResolveOptions( + resolve: EnvironmentResolveOptions | undefined, + logger: Logger, +): ResolvedConfig['resolve'] { + // resolve alias with internal client alias + const resolvedAlias = normalizeAlias( + mergeAlias(clientAlias, resolve?.alias || []), + ) + + const resolvedResolve: ResolvedConfig['resolve'] = { + mainFields: resolve?.mainFields ?? DEFAULT_MAIN_FIELDS, + conditions: resolve?.conditions ?? [], + externalConditions: resolve?.externalConditions ?? [], + external: resolve?.external ?? [], + noExternal: resolve?.noExternal ?? [], + extensions: resolve?.extensions ?? DEFAULT_EXTENSIONS, + dedupe: resolve?.dedupe ?? [], + preserveSymlinks: resolve?.preserveSymlinks ?? false, + alias: resolvedAlias, + } + + if ( + // @ts-expect-error removed field + resolve?.browserField === false && + resolvedResolve.mainFields.includes('browser') + ) { + logger.warn( + colors.yellow( + `\`resolve.browserField\` is set to false, but the option is removed in favour of ` + + `the 'browser' string in \`resolve.mainFields\`. You may want to update \`resolve.mainFields\` ` + + `to remove the 'browser' string and preserve the previous browser behaviour.`, + ), + ) + } + return resolvedResolve +} + +// TODO: Introduce ResolvedDepOptimizationOptions +function resolveDepOptimizationOptions( + optimizeDeps: DepOptimizationOptions | undefined, + preserveSymlinks: boolean, + environmentName: string | undefined, +): DepOptimizationOptions { + optimizeDeps ??= {} + return { + include: optimizeDeps.include ?? [], + exclude: optimizeDeps.exclude ?? [], + needsInterop: optimizeDeps.needsInterop ?? [], + extensions: optimizeDeps.extensions ?? [], + noDiscovery: optimizeDeps.noDiscovery ?? environmentName !== 'client', + holdUntilCrawlEnd: optimizeDeps.holdUntilCrawlEnd ?? true, + esbuildOptions: { + preserveSymlinks, // TODO: ? + ...optimizeDeps.esbuildOptions, + }, + disabled: optimizeDeps.disabled, + entries: optimizeDeps.entries, + force: optimizeDeps.force ?? false, + } +} + export async function resolveConfig( inlineConfig: InlineConfig, command: 'build' | 'serve', defaultMode = 'development', defaultNodeEnv = 'development', isPreview = false, + patchConfig: ((config: ResolvedConfig) => void) | undefined = undefined, + patchPlugins: ((resolvedPlugins: Plugin[]) => void) | undefined = undefined, ): Promise { let config = inlineConfig let configFileDependencies: string[] = [] @@ -494,7 +823,7 @@ export async function resolveConfig( const filterPlugin = (p: Plugin) => { if (!p) { return false - } else if (!p.apply) { + } else if (typeof p === 'function' || !p.apply) { return true } else if (typeof p.apply === 'function') { return p.apply({ ...config, mode }, configEnv) @@ -504,17 +833,37 @@ export async function resolveConfig( } // resolve plugins - const rawUserPlugins = ( + const rawPlugins = ( (await asyncFlatten(config.plugins || [])) as Plugin[] ).filter(filterPlugin) - const [prePlugins, normalPlugins, postPlugins] = - sortUserPlugins(rawUserPlugins) + const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawPlugins) + + const isBuild = command === 'build' // run config hooks const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins] config = await runConfigHook(config, userPlugins, configEnv) + // Ensure default client and ssr environments + // If there are present, ensure order { client, ssr, ...custom } + config.environments ??= {} + if ( + !config.environments.ssr && + (!isBuild || config.ssr || config.build?.ssr) + ) { + // During dev, the ssr environment is always available even if it isn't configure + // There is no perf hit, because the optimizer is initialized only if ssrLoadModule + // is called. + // During build, we only build the ssr environment if it is configured + // through the deprecated ssr top level options or if it is explicitly defined + // in the environments config + config.environments = { ssr: {}, ...config.environments } + } + if (!config.environments.client) { + config.environments = { client: {}, ...config.environments } + } + // Define logger const logger = createLogger(config.logLevel, { allowClearScreen: config.clearScreen, @@ -528,45 +877,140 @@ export async function resolveConfig( checkBadCharactersInPath(resolvedRoot, logger) - const clientAlias = [ - { - find: /^\/?@vite\/env/, - replacement: path.posix.join(FS_PREFIX, normalizePath(ENV_ENTRY)), - }, - { - find: /^\/?@vite\/client/, - replacement: path.posix.join(FS_PREFIX, normalizePath(CLIENT_ENTRY)), - }, - ] - - // resolve alias with internal client alias - const resolvedAlias = normalizeAlias( - mergeAlias(clientAlias, config.resolve?.alias || []), + // Backward compatibility: merge optimizeDeps into environments.client.dev.optimizeDeps as defaults + // TODO: should entries and force be in EnvironmentOptions? + const { entries, force, ...deprecatedClientOptimizeDepsConfig } = + config.optimizeDeps ?? {} + const configEnvironmentsClient = config.environments!.client! + configEnvironmentsClient.dev ??= {} + configEnvironmentsClient.dev.optimizeDeps = mergeConfig( + deprecatedClientOptimizeDepsConfig, + configEnvironmentsClient.dev.optimizeDeps ?? {}, ) - const resolveOptions: ResolvedConfig['resolve'] = { - mainFields: config.resolve?.mainFields ?? DEFAULT_MAIN_FIELDS, - conditions: config.resolve?.conditions ?? [], - extensions: config.resolve?.extensions ?? DEFAULT_EXTENSIONS, - dedupe: config.resolve?.dedupe ?? [], - preserveSymlinks: config.resolve?.preserveSymlinks ?? false, - alias: resolvedAlias, + const deprecatedSsrOptimizeDepsConfig = config.ssr?.optimizeDeps ?? {} + let configEnvironmentsSsr = config.environments!.ssr + + // Backward compatibility: server.warmup.clientFiles/ssrFiles -> environment.dev.warmup + const warmupOptions = config.server?.warmup + if (warmupOptions?.clientFiles) { + configEnvironmentsClient.dev.warmup = warmupOptions?.clientFiles + } + if (warmupOptions?.ssrFiles) { + configEnvironmentsSsr ??= {} + configEnvironmentsSsr.dev ??= {} + configEnvironmentsSsr.dev.warmup = warmupOptions?.ssrFiles } + // Backward compatibility: merge ssr into environments.ssr.config as defaults + if (configEnvironmentsSsr) { + configEnvironmentsSsr.dev ??= {} + configEnvironmentsSsr.dev.optimizeDeps = mergeConfig( + deprecatedSsrOptimizeDepsConfig, + configEnvironmentsSsr.dev.optimizeDeps ?? {}, + ) + // TODO: should we merge here? + configEnvironmentsSsr.resolve ??= {} + configEnvironmentsSsr.resolve.conditions ??= config.ssr?.resolve?.conditions + configEnvironmentsSsr.resolve.externalConditions ??= + config.ssr?.resolve?.externalConditions + configEnvironmentsSsr.resolve.external ??= config.ssr?.external + configEnvironmentsSsr.resolve.noExternal ??= config.ssr?.noExternal + + if (config.ssr?.target === 'webworker') { + configEnvironmentsSsr.webCompatible = true + } + } + + if (config.build?.ssrEmitAssets !== undefined) { + configEnvironmentsSsr ??= {} + configEnvironmentsSsr.build ??= {} + configEnvironmentsSsr.build.emitAssets = config.build.ssrEmitAssets + } + + // The client and ssr environment configs can't be removed by the user in the config hook if ( - // @ts-expect-error removed field - config.resolve?.browserField === false && - resolveOptions.mainFields.includes('browser') + !config.environments || + !config.environments.client || + (!config.environments.ssr && !isBuild) ) { - logger.warn( - colors.yellow( - `\`resolve.browserField\` is set to false, but the option is removed in favour of ` + - `the 'browser' string in \`resolve.mainFields\`. You may want to update \`resolve.mainFields\` ` + - `to remove the 'browser' string and preserve the previous browser behaviour.`, - ), + throw new Error( + 'Required environments configuration were stripped out in the config hook', + ) + } + + // Merge default environment config values + const defaultEnvironmentOptions = getDefaultEnvironmentOptions(config) + for (const name of Object.keys(config.environments)) { + config.environments[name] = mergeConfig( + defaultEnvironmentOptions, + config.environments[name], ) } + await runConfigEnvironmentHook(config.environments, userPlugins, configEnv) + + const resolvedEnvironments: Record = {} + for (const name of Object.keys(config.environments)) { + resolvedEnvironments[name] = resolveEnvironmentOptions( + config.environments[name], + resolvedRoot, + logger, + name, + config.experimental?.skipSsrTransform, + ) + } + + const resolvedDefaultEnvironmentResolve = resolveEnvironmentResolveOptions( + config.resolve, + logger, + ) + + // Backward compatibility: merge environments.client.dev.optimizeDeps back into optimizeDeps + const resolvedConfigEnvironmentsClient = resolvedEnvironments.client + const patchedOptimizeDeps = resolvedConfigEnvironmentsClient.dev?.optimizeDeps + + const backwardCompatibleOptimizeDeps = { + holdUntilCrawlEnd: true, + ...patchedOptimizeDeps, + esbuildOptions: { + preserveSymlinks: resolvedDefaultEnvironmentResolve.preserveSymlinks, + ...patchedOptimizeDeps.esbuildOptions, + }, + } + + // TODO: Deprecate and remove resolve, dev and build options at the root level of the resolved config + + const resolvedDevEnvironmentOptions = resolveDevEnvironmentOptions( + config.dev, + resolvedDefaultEnvironmentResolve.preserveSymlinks, + undefined, // default environment + ) + + const resolvedBuildOptions = resolveBuildOptions( + config.build ?? {}, + logger, + resolvedRoot, + ) + + // Backward compatibility: merge config.environments.ssr back into config.ssr + // so ecosystem SSR plugins continue to work if only environments.ssr is configured + const patchedConfigSsr = { + ...config.ssr, + external: resolvedEnvironments.ssr?.resolve.external, + noExternal: resolvedEnvironments.ssr?.resolve.noExternal, + optimizeDeps: resolvedEnvironments.ssr?.dev?.optimizeDeps, + resolve: { + ...config.ssr?.resolve, + conditions: resolvedEnvironments.ssr?.resolve.conditions, + externalConditions: resolvedEnvironments.ssr?.resolve.externalConditions, + }, + } + const ssr = resolveSSROptions( + patchedConfigSsr, + resolvedDefaultEnvironmentResolve.preserveSymlinks, + ) + // load .env files const envDir = config.envDir ? normalizePath(path.resolve(resolvedRoot, config.envDir)) @@ -595,7 +1039,6 @@ export async function resolveConfig( const isProduction = process.env.NODE_ENV === 'production' // resolve public base url - const isBuild = command === 'build' const relativeBaseShortcut = config.base === '' || config.base === './' // During dev, we ignore relative base and fallback to '/' @@ -607,12 +1050,6 @@ export async function resolveConfig( : './' : resolveBaseUrl(config.base, isBuild, logger) ?? '/' - const resolvedBuildOptions = resolveBuildOptions( - config.build, - logger, - resolvedRoot, - ) - // resolve cache directory const pkgDir = findNearestPackageData(resolvedRoot, packageCache)?.dir const cacheDir = normalizePath( @@ -629,52 +1066,6 @@ export async function resolveConfig( ? createFilter(config.assetsInclude) : () => false - // create an internal resolver to be used in special scenarios, e.g. - // optimizer & handling css @imports - const createResolver: ResolvedConfig['createResolver'] = (options) => { - let aliasContainer: PluginContainer | undefined - let resolverContainer: PluginContainer | undefined - return async (id, importer, aliasOnly, ssr) => { - let container: PluginContainer - if (aliasOnly) { - container = - aliasContainer || - (aliasContainer = await createPluginContainer({ - ...resolved, - plugins: [aliasPlugin({ entries: resolved.resolve.alias })], - })) - } else { - container = - resolverContainer || - (resolverContainer = await createPluginContainer({ - ...resolved, - plugins: [ - aliasPlugin({ entries: resolved.resolve.alias }), - resolvePlugin({ - ...resolved.resolve, - root: resolvedRoot, - isProduction, - isBuild: command === 'build', - ssrConfig: resolved.ssr, - asSrc: true, - preferRelative: false, - tryIndex: true, - ...options, - idOnly: true, - fsUtils: getFsUtils(resolved), - }), - ], - })) - } - return ( - await container.resolveId(id, importer, { - ssr, - scan: options?.scan, - }) - )?.id - } - } - const { publicDir } = config const resolvedPublicDir = publicDir !== false && publicDir !== '' @@ -687,9 +1078,8 @@ export async function resolveConfig( : '' const server = resolveServerOptions(resolvedRoot, config.server, logger) - const ssr = resolveSSROptions(config.ssr, resolveOptions.preserveSymlinks) - const optimizeDeps = config.optimizeDeps || {} + const builder = resolveBuilderOptions(config.builder) const BASE_URL = resolvedBase @@ -708,6 +1098,7 @@ export async function resolveConfig( ) } + // TODO: Workers as environments could allow us to remove a lot of complexity const createWorkerPlugins = async function (bundleChain: string[]) { // Some plugins that aren't intended to work in the bundling of workers (doing post-processing at build time for example). // And Plugins may also have cached that could be corrupted by being used in these extra rollup calls. @@ -740,12 +1131,12 @@ export async function resolveConfig( mainConfig: resolved, bundleChain, } - const resolvedWorkerPlugins = await resolvePlugins( + const resolvedWorkerPlugins = (await resolvePlugins( workerResolved, workerPrePlugins, workerNormalPlugins, workerPostPlugins, - ) + )) as Plugin[] // TODO: worker plugins and isolated constructor // run configResolved hooks await Promise.all( @@ -754,7 +1145,7 @@ export async function resolveConfig( .map((hook) => hook(workerResolved)), ) - return resolvedWorkerPlugins + return { plugins: resolvedWorkerPlugins, config: workerResolved } } const resolvedWorkerOptions: ResolvedWorkerOptions = { @@ -772,17 +1163,15 @@ export async function resolveConfig( root: resolvedRoot, base: withTrailingSlash(resolvedBase), rawBase: resolvedBase, - resolve: resolveOptions, publicDir: resolvedPublicDir, cacheDir, command, mode, - ssr, isWorker: false, mainConfig: null, bundleChain: [], isProduction, - plugins: userPlugins, + plugins: userPlugins, // placeholder to be replaced css: resolveCSSOptions(config.css), esbuild: config.esbuild === false @@ -792,7 +1181,7 @@ export async function resolveConfig( ...config.esbuild, }, server, - build: resolvedBuildOptions, + builder, preview: resolvePreviewOptions(config.preview, server), envDir, env: { @@ -807,15 +1196,6 @@ export async function resolveConfig( }, logger, packageCache, - createResolver, - optimizeDeps: { - holdUntilCrawlEnd: true, - ...optimizeDeps, - esbuildOptions: { - preserveSymlinks: resolveOptions.preserveSymlinks, - ...optimizeDeps.esbuildOptions, - }, - }, worker: resolvedWorkerOptions, appType: config.appType ?? 'spa', experimental: { @@ -823,19 +1203,126 @@ export async function resolveConfig( hmrPartialAccept: false, ...config.experimental, }, + future: config.future, + + // Backward compatibility, users should use environment.config.dev.optimizeDeps + optimizeDeps: backwardCompatibleOptimizeDeps, + ssr, + + // TODO: deprecate and later remove from ResolvedConfig? + resolve: resolvedDefaultEnvironmentResolve, + dev: resolvedDevEnvironmentOptions, + build: resolvedBuildOptions, + + environments: resolvedEnvironments, + getSortedPlugins: undefined!, getSortedPluginHooks: undefined!, + + /** + * createResolver is deprecated. It only works for the client and ssr + * environments. The `aliasOnly` option is also not being used any more + * Plugins should move to createIdResolver(environment) instead. + * create an internal resolver to be used in special scenarios, e.g. + * optimizer & handling css @imports + * @deprecated + */ + createResolver(options) { + const alias: { + client?: EnvironmentPluginContainer + ssr?: EnvironmentPluginContainer + } = {} + const resolver: { + client?: EnvironmentPluginContainer + ssr?: EnvironmentPluginContainer + } = {} + const environments = this.environments ?? resolvedEnvironments + const createPluginContainer = async ( + environmentName: string, + plugins: Plugin[], + ) => { + // The used alias and resolve plugins only use configuration options from the + // environment so we can safely just use the FutureCompatEnvironment here + const environment = new FutureCompatEnvironment(environmentName, this) + const pluginContainer = await createEnvironmentPluginContainer( + environment, + plugins, + ) + await pluginContainer.buildStart({}) + return pluginContainer + } + async function resolve( + id: string, + importer?: string, + aliasOnly?: boolean, + ssr?: boolean, + ): Promise { + const environmentName = ssr ? 'ssr' : 'client' + let container: EnvironmentPluginContainer + if (aliasOnly) { + let aliasContainer = alias[environmentName] + if (!aliasContainer) { + aliasContainer = alias[environmentName] = + await createPluginContainer(environmentName, [ + aliasPlugin({ entries: resolved.resolve.alias }), + ]) + } + container = aliasContainer + } else { + let resolverContainer = resolver[environmentName] + if (!resolverContainer) { + resolverContainer = resolver[environmentName] = + await createPluginContainer(environmentName, [ + aliasPlugin({ entries: resolved.resolve.alias }), + resolvePlugin( + { + ...resolved.resolve, + root: resolvedRoot, + isProduction, + isBuild: command === 'build', + asSrc: true, + preferRelative: false, + tryIndex: true, + ...options, + idOnly: true, + fsUtils: getFsUtils(resolved), + }, + environments, + ), + ]) + } + container = resolverContainer + } + return await container.resolveId(id, importer, { scan: options?.scan }) + } + return async (id, importer, aliasOnly, ssr) => + (await resolve(id, importer, aliasOnly, ssr))?.id + }, } resolved = { ...config, ...resolved, } - ;(resolved.plugins as Plugin[]) = await resolvePlugins( + + // Backward compatibility hook, modify the resolved config before it is used + // to create internal plugins. For example, `config.build.ssr`. Once we rework + // internal plugins to use environment.options, we can remove the dual + // patchConfig/patchPlugins and have a single patchConfig before configResolved + // gets called + patchConfig?.(resolved) + + const resolvedPlugins = await resolvePlugins( resolved, prePlugins, normalPlugins, postPlugins, ) + + // Backward compatibility hook used in builder, opt-in to shared plugins during build + patchPlugins?.(resolvedPlugins) + ;(resolved.plugins as Plugin[]) = resolvedPlugins + + // TODO: Deprecate config.getSortedPlugins and config.getSortedPluginHooks Object.assign(resolved, createPluginHookUtils(resolved.plugins)) // call configResolved hooks @@ -863,20 +1350,6 @@ export async function resolveConfig( // validate config - if ( - config.build?.terserOptions && - config.build.minify && - config.build.minify !== 'terser' - ) { - logger.warn( - colors.yellow( - `build.terserOptions is specified but build.minify is not set to use Terser. ` + - `Note Vite now defaults to use esbuild for minification. If you still ` + - `prefer Terser, set build.minify to "terser".`, - ), - ) - } - // Check if all assetFileNames have the same reference. // If not, display a warn for user. const outputOption = config.build?.rollupOptions?.output ?? [] @@ -1098,26 +1571,26 @@ async function bundleConfigFile( importer: string, isRequire: boolean, ) => { - return tryNodeResolve( - id, - importer, - { - root: path.dirname(fileName), - isBuild: true, - isProduction: true, - preferRelative: false, - tryIndex: true, - mainFields: [], - conditions: [], - overrideConditions: ['node'], - dedupe: [], - extensions: DEFAULT_EXTENSIONS, - preserveSymlinks: false, - packageCache, - isRequire, - }, - false, - )?.id + return tryNodeResolve(id, importer, { + root: path.dirname(fileName), + isBuild: true, + isProduction: true, + preferRelative: false, + tryIndex: true, + mainFields: [], + conditions: [], + externalConditions: [], + external: [], + noExternal: [], + overrideConditions: ['node'], + dedupe: [], + extensions: DEFAULT_EXTENSIONS, + preserveSymlinks: false, + packageCache, + isRequire, + webCompatible: false, + nodeCompatible: true, + })?.id } // externalize bare imports @@ -1287,23 +1760,29 @@ async function runConfigHook( return conf } -export function getDepOptimizationConfig( - config: ResolvedConfig, - ssr: boolean, -): DepOptimizationConfig { - return ssr ? config.ssr.optimizeDeps : config.optimizeDeps -} -export function isDepsOptimizerEnabled( - config: ResolvedConfig, - ssr: boolean, -): boolean { - const optimizeDeps = getDepOptimizationConfig(config, ssr) - return !(optimizeDeps.noDiscovery && !optimizeDeps.include?.length) +async function runConfigEnvironmentHook( + environments: Record, + plugins: Plugin[], + configEnv: ConfigEnv, +): Promise { + const environmentNames = Object.keys(environments) + for (const p of getSortedPluginsByHook('configEnvironment', plugins)) { + const hook = p.configEnvironment + const handler = getHookHandler(hook) + if (handler) { + for (const name of environmentNames) { + const res = await handler(name, environments[name], configEnv) + if (res) { + environments[name] = mergeConfig(environments[name], res) + } + } + } + } } function optimizeDepsDisabledBackwardCompatibility( resolved: ResolvedConfig, - optimizeDeps: DepOptimizationConfig, + optimizeDeps: DepOptimizationOptions, optimizeDepsPath: string = '', ) { const optimizeDepsDisabled = optimizeDeps.disabled diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index f729f9d6162298..1af75b0ea601e6 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -1,11 +1,40 @@ import path, { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { readFileSync } from 'node:fs' +import type { RollupPluginHooks } from './typeUtils' const { version } = JSON.parse( readFileSync(new URL('../../package.json', import.meta.url)).toString(), ) +export const ROLLUP_HOOKS = [ + 'buildStart', + 'buildEnd', + 'renderStart', + 'renderError', + 'renderChunk', + 'writeBundle', + 'generateBundle', + 'banner', + 'footer', + 'augmentChunkHash', + 'outputOptions', + 'renderDynamicImport', + 'resolveFileUrl', + 'resolveImportMeta', + 'intro', + 'outro', + 'closeBundle', + 'closeWatcher', + 'load', + 'moduleParsed', + 'watchChange', + 'resolveDynamicImport', + 'resolveId', + 'shouldTransformCachedModule', + 'transform', +] satisfies RollupPluginHooks[] + export const VERSION = version as string export const DEFAULT_MAIN_FIELDS = [ diff --git a/packages/vite/src/node/deprecations.ts b/packages/vite/src/node/deprecations.ts new file mode 100644 index 00000000000000..43d2e59a59ea90 --- /dev/null +++ b/packages/vite/src/node/deprecations.ts @@ -0,0 +1,104 @@ +import colors from 'picocolors' +import type { ResolvedConfig } from './config' + +// TODO: switch to production docs URL +const docsURL = 'https://deploy-preview-16471--vite-docs-main.netlify.app' + +export interface FutureDeprecationWarningsOptions { + pluginHookHandleHotUpdate?: boolean + pluginHookSsrArgument?: boolean + + serverModuleGraph?: boolean + serverHot?: boolean + serverTransformRequest?: boolean + + ssrLoadModule?: boolean +} + +const deprecationCode = { + pluginHookHandleHotUpdate: 'VD001', + pluginHookSsrArgument: 'VD002', + + serverModuleGraph: 'VD003', + serverHot: 'VD003', + serverTransformRequest: 'VD003', + + ssrLoadModule: 'VD004', +} satisfies Record + +const deprecationMessages = { + pluginHookHandleHotUpdate: + 'Plugin hook `handleHotUpdate()` is replaced with `hotUpdate()`.', + pluginHookSsrArgument: + 'Plugin hook `options.ssr` is replaced with `this.environment.name !== "client"`.', + + serverModuleGraph: + 'The `server.moduleGraph` is replaced with `this.environment.moduleGraph`.', + serverHot: 'The `server.hot` is replaced with `this.environment.hot`.', + serverTransformRequest: + 'The `server.transformRequest` is replaced with `this.environment.transformRequest`.', + + ssrLoadModule: + 'The `server.ssrLoadModule` is replaced with Environment Runner.', +} satisfies Record + +let _ignoreDeprecationWarnings = false + +// Later we could have a `warnDeprecation` utils when the deprecation is landed +/** + * Warn about future deprecations. + */ +export function warnFutureDeprecation( + config: ResolvedConfig, + type: keyof FutureDeprecationWarningsOptions, + extraMessage?: string, + stacktrace = true, +): void { + if (_ignoreDeprecationWarnings) return + + if (!config.future?.deprecationWarnings) return + + if ( + config.future.deprecationWarnings !== true && + !config.future.deprecationWarnings[type] + ) + return + + let msg = `[vite future] [${deprecationCode[type]}] ${deprecationMessages[type]}` + if (extraMessage) { + msg += ` ${extraMessage}` + } + msg = colors.yellow(msg) + + const docs = `${docsURL}/deprecations/${deprecationCode[type].toLowerCase()}` + msg += + colors.gray(`\n ${stacktrace ? '├' : '└'}─── `) + + colors.underline(docs) + + '\n' + + if (stacktrace) { + const stack = new Error().stack + if (stack) { + let stacks = stack + .split('\n') + .slice(3) + .filter((i) => !i.includes('/node_modules/vite/dist/')) + if (stacks.length === 0) { + stacks.push('No stack trace found.') + } + stacks = stacks.map( + (i, idx) => ` ${idx === stacks.length - 1 ? '└' : '│'} ${i.trim()}`, + ) + msg += colors.dim(stacks.join('\n')) + '\n' + } + } + config.logger.warnOnce(msg) +} + +export function ignoreDeprecationWarnings(fn: () => T): T { + const before = _ignoreDeprecationWarnings + _ignoreDeprecationWarnings = true + const ret = fn() + _ignoreDeprecationWarnings = before + return ret +} diff --git a/packages/vite/src/node/environment.ts b/packages/vite/src/node/environment.ts new file mode 100644 index 00000000000000..638604f8e992b6 --- /dev/null +++ b/packages/vite/src/node/environment.ts @@ -0,0 +1,31 @@ +import type { DevEnvironment } from './server/environment' +import type { BuildEnvironment } from './build' +import type { ScanEnvironment } from './optimizer/scan' +import type { FutureCompatEnvironment } from './baseEnvironment' +import type { PluginContext } from './plugin' + +export type Environment = + | DevEnvironment + | BuildEnvironment + | ScanEnvironment + | FutureCompatEnvironment + +/** + * Creates a function that hides the complexities of a WeakMap with an initial value + * to implement object metadata. Used by plugins to implement cross hooks per + * environment metadata + */ +export function usePerEnvironmentState( + initial: (environment: Environment) => State, +): (context: PluginContext) => State { + const stateMap = new WeakMap() + return function (context: PluginContext) { + const { environment } = context + let state = stateMap.get(environment) + if (!state) { + state = initial(environment) + stateMap.set(environment, state) + } + return state + } +} diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/external.ts similarity index 56% rename from packages/vite/src/node/ssr/ssrExternal.ts rename to packages/vite/src/node/external.ts index 5681e000502a5f..58d35435a51959 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/external.ts @@ -1,53 +1,75 @@ import path from 'node:path' -import type { InternalResolveOptions } from '../plugins/resolve' -import { tryNodeResolve } from '../plugins/resolve' +import type { InternalResolveOptions } from './plugins/resolve' +import { tryNodeResolve } from './plugins/resolve' import { bareImportRE, createDebugger, createFilter, getNpmPackageName, isBuiltin, -} from '../utils' -import type { ResolvedConfig } from '..' +} from './utils' +import type { Environment } from './environment' +import type { PartialEnvironment } from './baseEnvironment' -const debug = createDebugger('vite:ssr-external') +const debug = createDebugger('vite:external') -const isSsrExternalCache = new WeakMap< - ResolvedConfig, +const isExternalCache = new WeakMap< + Environment, (id: string, importer?: string) => boolean | undefined >() -export function shouldExternalizeForSSR( +export function shouldExternalize( + environment: Environment, id: string, importer: string | undefined, - config: ResolvedConfig, ): boolean | undefined { - let isSsrExternal = isSsrExternalCache.get(config) - if (!isSsrExternal) { - isSsrExternal = createIsSsrExternal(config) - isSsrExternalCache.set(config, isSsrExternal) + let isExternal = isExternalCache.get(environment) + if (!isExternal) { + isExternal = createIsExternal(environment) + isExternalCache.set(environment, isExternal) } - return isSsrExternal(id, importer) + return isExternal(id, importer) } -export function createIsConfiguredAsSsrExternal( - config: ResolvedConfig, +const isConfiguredAsExternalCache = new WeakMap< + Environment, + (id: string, importer?: string) => boolean +>() + +export function isConfiguredAsExternal( + environment: Environment, + id: string, + importer?: string, +): boolean { + let isExternal = isConfiguredAsExternalCache.get(environment) + if (!isExternal) { + isExternal = createIsConfiguredAsExternal(environment) + isConfiguredAsExternalCache.set(environment, isExternal) + } + return isExternal(id, importer) +} + +export function createIsConfiguredAsExternal( + environment: PartialEnvironment, ): (id: string, importer?: string) => boolean { - const { ssr, root } = config - const noExternal = ssr?.noExternal + const { config, options } = environment + const { root } = config + const { external, noExternal } = options.resolve const noExternalFilter = - noExternal !== 'undefined' && typeof noExternal !== 'boolean' && + !(Array.isArray(noExternal) && noExternal.length === 0) && createFilter(undefined, noExternal, { resolve: false }) - const targetConditions = config.ssr.resolve?.externalConditions || [] + const targetConditions = options.resolve?.externalConditions || [] const resolveOptions: InternalResolveOptions = { - ...config.resolve, + ...options.resolve, root, isProduction: false, isBuild: true, conditions: targetConditions, + webCompatible: options.webCompatible, + nodeCompatible: options.nodeCompatible, } const isExternalizable = ( @@ -65,7 +87,6 @@ export function createIsConfiguredAsSsrExternal( // unresolvable from root (which would be unresolvable from output bundles also) config.command === 'build' ? undefined : importer, resolveOptions, - ssr?.target === 'webworker', undefined, true, // try to externalize, will return undefined or an object without @@ -89,9 +110,9 @@ export function createIsConfiguredAsSsrExternal( return (id: string, importer?: string) => { if ( // If this id is defined as external, force it as external - // Note that individual package entries are allowed in ssr.external - ssr.external !== true && - ssr.external?.includes(id) + // Note that individual package entries are allowed in `external` + external !== true && + external.includes(id) ) { return true } @@ -102,8 +123,8 @@ export function createIsConfiguredAsSsrExternal( if ( // A package name in ssr.external externalizes every // externalizable package entry - ssr.external !== true && - ssr.external?.includes(pkgName) + external !== true && + external.includes(pkgName) ) { return isExternalizable(id, importer, true) } @@ -113,28 +134,28 @@ export function createIsConfiguredAsSsrExternal( if (noExternalFilter && !noExternalFilter(pkgName)) { return false } - // If `ssr.external: true`, all will be externalized by default, regardless if + // If external is true, all will be externalized by default, regardless if // it's a linked package - return isExternalizable(id, importer, ssr.external === true) + return isExternalizable(id, importer, external === true) } } -function createIsSsrExternal( - config: ResolvedConfig, +function createIsExternal( + environment: Environment, ): (id: string, importer?: string) => boolean | undefined { const processedIds = new Map() - const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config) + const isConfiguredAsExternal = createIsConfiguredAsExternal(environment) return (id: string, importer?: string) => { if (processedIds.has(id)) { return processedIds.get(id) } - let external = false + let isExternal = false if (id[0] !== '.' && !path.isAbsolute(id)) { - external = isBuiltin(id) || isConfiguredAsExternal(id, importer) + isExternal = isBuiltin(id) || isConfiguredAsExternal(id, importer) } - processedIds.set(id, external) - return external + processedIds.set(id, isExternal) + return isExternal } } diff --git a/packages/vite/src/node/idResolver.ts b/packages/vite/src/node/idResolver.ts new file mode 100644 index 00000000000000..6cd62a93b3dcf6 --- /dev/null +++ b/packages/vite/src/node/idResolver.ts @@ -0,0 +1,92 @@ +import type { PartialResolvedId } from 'rollup' +import aliasPlugin from '@rollup/plugin-alias' +import type { ResolvedConfig } from './config' +import type { EnvironmentPluginContainer } from './server/pluginContainer' +import { createEnvironmentPluginContainer } from './server/pluginContainer' +import { resolvePlugin } from './plugins/resolve' +import type { InternalResolveOptions } from './plugins/resolve' +import { getFsUtils } from './fsUtils' +import type { Environment } from './environment' +import type { PartialEnvironment } from './baseEnvironment' + +export type ResolveIdFn = ( + environment: PartialEnvironment, + id: string, + importer?: string, + aliasOnly?: boolean, +) => Promise + +/** + * Create an internal resolver to be used in special scenarios, e.g. + * optimizer and handling css @imports + */ +export function createIdResolver( + config: ResolvedConfig, + options: Partial, +): ResolveIdFn { + const scan = options?.scan + + const pluginContainerMap = new Map< + PartialEnvironment, + EnvironmentPluginContainer + >() + async function resolve( + environment: PartialEnvironment, + id: string, + importer?: string, + ): Promise { + let pluginContainer = pluginContainerMap.get(environment) + if (!pluginContainer) { + pluginContainer = await createEnvironmentPluginContainer( + environment as Environment, + [ + aliasPlugin({ entries: config.resolve.alias }), // TODO: resolve.alias per environment? + resolvePlugin({ + root: config.root, + isProduction: config.isProduction, + isBuild: config.command === 'build', + asSrc: true, + preferRelative: false, + tryIndex: true, + ...options, + fsUtils: getFsUtils(config), + // Ignore sideEffects and other computations as we only need the id + idOnly: true, + }), + ], + ) + pluginContainerMap.set(environment, pluginContainer) + } + return await pluginContainer.resolveId(id, importer, { scan }) + } + + const aliasOnlyPluginContainerMap = new Map< + PartialEnvironment, + EnvironmentPluginContainer + >() + async function resolveAlias( + environment: PartialEnvironment, + id: string, + importer?: string, + ): Promise { + let pluginContainer = aliasOnlyPluginContainerMap.get(environment) + if (!pluginContainer) { + pluginContainer = await createEnvironmentPluginContainer( + environment as Environment, + [ + aliasPlugin({ entries: config.resolve.alias }), // TODO: resolve.alias per environment? + ], + ) + aliasOnlyPluginContainerMap.set(environment, pluginContainer) + } + return await pluginContainer.resolveId(id, importer, { scan }) + } + + return async (environment, id, importer, aliasOnly) => { + const resolveFn = aliasOnly ? resolveAlias : resolve + // aliasPlugin and resolvePlugin are implemented to function with a Environment only, + // we cast it as PluginEnvironment to be able to use the pluginContainer + const resolved = await resolveFn(environment, id, importer) + return resolved?.id + } +} diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 3b84c34b0626a8..ef1826656fc1a5 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -10,13 +10,26 @@ export { } from './config' export { createServer } from './server' export { preview } from './preview' -export { build } from './build' +export { build, createBuilder } from './build' + export { optimizeDeps } from './optimizer' +export { createIdResolver } from './idResolver' + export { formatPostcssSourceMap, preprocessCSS } from './plugins/css' export { transformWithEsbuild } from './plugins/esbuild' export { buildErrorMessage } from './server/middlewares/error' -export { fetchModule } from './ssr/fetchModule' -export type { FetchModuleOptions } from './ssr/fetchModule' + +export { RemoteEnvironmentTransport } from './server/environmentTransport' +export { createNodeDevEnvironment } from './server/environments/nodeEnvironment' +export { DevEnvironment, type DevEnvironmentSetup } from './server/environment' +export { BuildEnvironment } from './build' + +export { fetchModule, type FetchModuleOptions } from './ssr/fetchModule' +export { createServerModuleRunner } from './ssr/runtime/serverModuleRunner' +export { ServerHMRConnector } from './ssr/runtime/serverHmrConnector' +export { ssrTransform as moduleRunnerTransform } from './ssr/ssrTransform' +export type { ModuleRunnerTransformOptions } from './ssr/ssrTransform' + export * from './publicUtils' // additional types @@ -28,7 +41,6 @@ export type { InlineConfig, LegacyOptions, PluginHookUtils, - PluginOption, ResolveFn, ResolvedWorkerOptions, ResolvedConfig, @@ -37,7 +49,11 @@ export type { UserConfigFn, UserConfigFnObject, UserConfigFnPromise, + DevEnvironmentOptions, + ResolvedDevEnvironmentOptions, } from './config' +export type { Plugin, PluginOption, HookHandler } from './plugin' +export type { Environment } from './environment' export type { FilterPattern } from './utils' export type { CorsOptions, CorsOrigin, CommonServerOptions } from './http' export type { @@ -49,11 +65,15 @@ export type { ResolvedServerUrls, } from './server' export type { + ViteBuilder, + BuilderOptions, BuildOptions, + BuildEnvironmentOptions, LibraryOptions, LibraryFormats, RenderBuiltAssetUrl, ResolvedBuildOptions, + ResolvedBuildEnvironmentOptions, ModulePreloadOptions, ResolvedModulePreloadOptions, ResolveModulePreloadDependenciesFn, @@ -73,11 +93,10 @@ export type { } from './optimizer' export type { ResolvedSSROptions, - SsrDepOptimizationOptions, + SsrDepOptimizationConfig, SSROptions, SSRTarget, } from './ssr' -export type { Plugin, HookHandler } from './plugin' export type { Logger, LogOptions, @@ -113,31 +132,33 @@ export type { WebSocketCustomListener, } from './server/ws' export type { PluginContainer } from './server/pluginContainer' -export type { ModuleGraph, ModuleNode, ResolvedUrl } from './server/moduleGraph' +export type { + EnvironmentModuleGraph, + EnvironmentModuleNode, + ResolvedUrl, +} from './server/moduleGraph' export type { SendOptions } from './server/send' export type { ProxyOptions } from './server/middlewares/proxy' export type { TransformOptions, TransformResult, } from './server/transformRequest' -export type { HmrOptions, HmrContext } from './server/hmr' +export type { HmrOptions, HmrContext, HotUpdateContext } from './server/hmr' export type { - HMRBroadcaster, - HMRChannel, - ServerHMRChannel, - HMRBroadcasterClient, + HotChannel, + ServerHotChannel, + HotChannelClient, } from './server/hmr' -export type { FetchFunction } from '../runtime/index' -export { createViteRuntime } from './ssr/runtime/mainThreadRuntime' -export type { MainThreadRuntimeOptions } from './ssr/runtime/mainThreadRuntime' -export { ServerHMRConnector } from './ssr/runtime/serverHmrConnector' +export type { FetchFunction, FetchResult } from 'vite/module-runner' +export type { ServerModuleRunnerOptions } from './ssr/runtime/serverModuleRunner' export type { BindCLIShortcutsOptions, CLIShortcut } from './shortcuts' export type { HMRPayload, + HotPayload, ConnectedPayload, UpdatePayload, Update, @@ -145,7 +166,7 @@ export type { CustomPayload, PrunePayload, ErrorPayload, -} from 'types/hmrPayload' +} from 'types/hotPayload' export type { CustomEventMap, InferCustomEventPayload, @@ -180,3 +201,6 @@ export type { RollupCommonJSOptions } from 'dep-types/commonjs' export type { RollupDynamicImportVarsOptions } from 'dep-types/dynamicImportVars' export type { Matcher, AnymatchPattern, AnymatchFn } from 'dep-types/anymatch' export type { LightningCSSOptions } from 'dep-types/lightningcss' + +// Backward compatibility +export type { ModuleGraph, ModuleNode } from './server/mixedModuleGraph' diff --git a/packages/vite/src/node/logger.ts b/packages/vite/src/node/logger.ts index ddeda0755712f1..d75b7cc6b016ee 100644 --- a/packages/vite/src/node/logger.ts +++ b/packages/vite/src/node/logger.ts @@ -21,6 +21,7 @@ export interface Logger { export interface LogOptions { clear?: boolean timestamp?: boolean + environment?: string } export interface LogErrorOptions extends LogOptions { @@ -98,15 +99,17 @@ export function createLogger( ) { const msg = preventOverflow(rawMsg) if (options.timestamp) { - const tag = + const color = type === 'info' - ? colors.cyan(colors.bold(prefix)) + ? colors.cyan : type === 'warn' - ? colors.yellow(colors.bold(prefix)) - : colors.red(colors.bold(prefix)) + ? colors.yellow + : colors.red + const tag = color(colors.bold(prefix)) + const environment = options.environment ? options.environment + ' ' : '' return `${colors.dim( getTimeFormatter().format(new Date()), - )} ${tag} ${msg}` + )} ${tag} ${environment}${msg}` } else { return msg } diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts index 1f4c4dab16748d..cddac9d2a367ea 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts @@ -2,8 +2,6 @@ import path from 'node:path' import type { ImportKind, Plugin } from 'esbuild' import { KNOWN_ASSET_TYPES } from '../constants' import type { PackageCache } from '../packages' -import { getDepOptimizationConfig } from '../config' -import type { ResolvedConfig } from '../config' import { escapeRegex, flattenId, @@ -14,6 +12,8 @@ import { } from '../utils' import { browserExternalId, optionalPeerDepId } from '../plugins/resolve' import { isCSSRequest, isModuleCSSRequest } from '../plugins/css' +import type { Environment } from '../environment' +import { createIdResolver } from '../idResolver' const externalWithConversionNamespace = 'vite:dep-pre-bundle:external-conversion' @@ -48,12 +48,12 @@ const externalTypes = [ ] export function esbuildDepPlugin( + environment: Environment, qualified: Record, external: string[], - config: ResolvedConfig, - ssr: boolean, ): Plugin { - const { extensions } = getDepOptimizationConfig(config, ssr) + const config = environment.config + const { extensions } = environment.options.dev.optimizeDeps // remove optimizable extensions from `externalTypes` list const allExternalTypes = extensions @@ -66,14 +66,14 @@ export function esbuildDepPlugin( const cjsPackageCache: PackageCache = new Map() // default resolver which prefers ESM - const _resolve = config.createResolver({ + const _resolve = createIdResolver(config, { asSrc: false, scan: true, packageCache: esmPackageCache, }) // cjs resolver that prefers Node - const _resolveRequire = config.createResolver({ + const _resolveRequire = createIdResolver(config, { asSrc: false, isRequire: true, scan: true, @@ -96,7 +96,7 @@ export function esbuildDepPlugin( _importer = importer in qualified ? qualified[importer] : importer } const resolver = kind.startsWith('require') ? _resolveRequire : _resolve - return resolver(id, _importer, undefined, ssr) + return resolver(environment, id, _importer) } const resolveResult = (id: string, resolved: string) => { @@ -112,6 +112,7 @@ export function esbuildDepPlugin( namespace: 'optional-peer-dep', } } + const ssr = environment.name !== 'client' // TODO:depsOptimizer how to avoid depending on environment name? if (ssr && isBuiltin(resolved)) { return } @@ -217,7 +218,7 @@ export function esbuildDepPlugin( if (!importer) { if ((entry = resolveEntry(id))) return entry // check if this is aliased to an entry - also return entry namespace - const aliased = await _resolve(id, undefined, true) + const aliased = await _resolve(environment, id, undefined, true) if (aliased && (entry = resolveEntry(aliased))) { return entry } diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 5902aa96e9eb4f..347e66858a7f1b 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -8,7 +8,6 @@ import type { BuildContext, BuildOptions as EsbuildBuildOptions } from 'esbuild' import esbuild, { build } from 'esbuild' import { init, parse } from 'es-module-lexer' import glob from 'fast-glob' -import { getDepOptimizationConfig } from '../config' import type { ResolvedConfig } from '../config' import { createDebugger, @@ -28,14 +27,10 @@ import { } from '../plugins/esbuild' import { ESBUILD_MODULES_TARGET, METADATA_FILENAME } from '../constants' import { isWindows } from '../../shared/utils' +import type { Environment } from '../environment' import { esbuildCjsExternalPlugin, esbuildDepPlugin } from './esbuildDepPlugin' -import { scanImports } from './scan' +import { ScanEnvironment, scanImports } from './scan' import { createOptimizeDepsIncludeResolver, expandGlobIds } from './resolve' -export { - initDepsOptimizer, - initDevSsrDepsOptimizer, - getDepsOptimizer, -} from './optimizer' const debug = createDebugger('vite:deps') @@ -51,6 +46,8 @@ export type ExportsData = { } export interface DepsOptimizer { + init: () => Promise + metadata: DepOptimizationMetadata scanProcessing?: Promise registerMissingImport: (id: string, resolved: string) => OptimizedDepInfo @@ -138,7 +135,7 @@ export interface DepOptimizationConfig { * When enabled, it will hold the first optimized deps results until all static * imports are crawled on cold start. This avoids the need for full-page reloads * when new dependencies are discovered and they trigger the generation of new - * common chunks. If all dependencies are found by the scanner plus the explicitely + * common chunks. If all dependencies are found by the scanner plus the explicitly * defined ones in `include`, it is better to disable this option to let the * browser process more requests in parallel. * @default true @@ -166,6 +163,16 @@ export type DepOptimizationOptions = DepOptimizationConfig & { force?: boolean } +export function isDepOptimizationDisabled( + optimizeDeps: DepOptimizationOptions, +): boolean { + return ( + optimizeDeps.disabled === true || + optimizeDeps.disabled === 'dev' || + (!!optimizeDeps.noDiscovery && !optimizeDeps.include?.length) + ) +} + export interface DepOptimizationResult { metadata: DepOptimizationMetadata /** @@ -240,17 +247,22 @@ export interface DepOptimizationMetadata { * Scan and optimize dependencies within a project. * Used by Vite CLI when running `vite optimize`. */ +// TODO: do we need this? It is exposed for the CLI command `vite optimize` + export async function optimizeDeps( config: ResolvedConfig, force = config.optimizeDeps.force, asCommand = false, ): Promise { const log = asCommand ? config.logger.info : debug - const ssr = false + + // TODO: Could we avoid the creation of a DevEnvironment moving the plugin resolving to + // the Environment base class? + const environment = new ScanEnvironment('client', config) + await environment.init() const cachedMetadata = await loadCachedDepOptimizationMetadata( - config, - ssr, + environment, force, asCommand, ) @@ -258,30 +270,28 @@ export async function optimizeDeps( return cachedMetadata } - const deps = await discoverProjectDependencies(config).result + const deps = await discoverProjectDependencies(environment).result - await addManuallyIncludedOptimizeDeps(deps, config, ssr) + await addManuallyIncludedOptimizeDeps(environment, deps) const depsString = depsLogString(Object.keys(deps)) log?.(colors.green(`Optimizing dependencies:\n ${depsString}`)) - const depsInfo = toDiscoveredDependencies(config, deps, ssr) + const depsInfo = toDiscoveredDependencies(environment, deps) - const result = await runOptimizeDeps(config, depsInfo, ssr).result + const result = await runOptimizeDeps(environment, depsInfo).result await result.commit() return result.metadata } -export async function optimizeServerSsrDeps( - config: ResolvedConfig, +export async function optimizeExplicitEnvironmentDeps( + environment: Environment, ): Promise { - const ssr = true const cachedMetadata = await loadCachedDepOptimizationMetadata( - config, - ssr, - config.optimizeDeps.force, + environment, + environment.options.dev.optimizeDeps.force ?? false, // TODO: should force be per-environment? false, ) if (cachedMetadata) { @@ -290,11 +300,11 @@ export async function optimizeServerSsrDeps( const deps: Record = {} - await addManuallyIncludedOptimizeDeps(deps, config, ssr) + await addManuallyIncludedOptimizeDeps(environment, deps) - const depsInfo = toDiscoveredDependencies(config, deps, ssr) + const depsInfo = toDiscoveredDependencies(environment, deps) - const result = await runOptimizeDeps(config, depsInfo, ssr).result + const result = await runOptimizeDeps(environment, depsInfo).result await result.commit() @@ -302,11 +312,10 @@ export async function optimizeServerSsrDeps( } export function initDepsOptimizerMetadata( - config: ResolvedConfig, - ssr: boolean, + environment: Environment, timestamp?: string, ): DepOptimizationMetadata { - const { lockfileHash, configHash, hash } = getDepHash(config, ssr) + const { lockfileHash, configHash, hash } = getDepHash(environment) return { hash, lockfileHash, @@ -336,20 +345,19 @@ let firstLoadCachedDepOptimizationMetadata = true * if it exists and pre-bundling isn't forced */ export async function loadCachedDepOptimizationMetadata( - config: ResolvedConfig, - ssr: boolean, - force = config.optimizeDeps.force, + environment: Environment, + force = environment.config.optimizeDeps?.force ?? false, asCommand = false, ): Promise { - const log = asCommand ? config.logger.info : debug + const log = asCommand ? environment.logger.info : debug if (firstLoadCachedDepOptimizationMetadata) { firstLoadCachedDepOptimizationMetadata = false // Fire up a clean up of stale processing deps dirs if older process exited early - setTimeout(() => cleanupDepsCacheStaleDirs(config), 0) + setTimeout(() => cleanupDepsCacheStaleDirs(environment.config), 0) } - const depsCacheDir = getDepsCacheDir(config, ssr) + const depsCacheDir = getDepsCacheDir(environment) if (!force) { let cachedMetadata: DepOptimizationMetadata | undefined @@ -362,12 +370,12 @@ export async function loadCachedDepOptimizationMetadata( } catch (e) {} // hash is consistent, no need to re-bundle if (cachedMetadata) { - if (cachedMetadata.lockfileHash !== getLockfileHash(config, ssr)) { - config.logger.info( + if (cachedMetadata.lockfileHash !== getLockfileHash(environment)) { + environment.logger.info( 'Re-optimizing dependencies because lockfile has changed', ) - } else if (cachedMetadata.configHash !== getConfigHash(config, ssr)) { - config.logger.info( + } else if (cachedMetadata.configHash !== getConfigHash(environment)) { + environment.logger.info( 'Re-optimizing dependencies because vite config has changed', ) } else { @@ -378,7 +386,7 @@ export async function loadCachedDepOptimizationMetadata( } } } else { - config.logger.info('Forced re-optimization of dependencies') + environment.logger.info('Forced re-optimization of dependencies') } // Start with a fresh cache @@ -390,11 +398,13 @@ export async function loadCachedDepOptimizationMetadata( * Initial optimizeDeps at server start. Perform a fast scan using esbuild to * find deps to pre-bundle and include user hard-coded dependencies */ -export function discoverProjectDependencies(config: ResolvedConfig): { +export function discoverProjectDependencies(environment: ScanEnvironment): { cancel: () => Promise result: Promise> } { - const { cancel, result } = scanImports(config) + // Should the scanner be per-environment? + // we only use it for the client right now + const { cancel, result } = scanImports(environment) return { cancel, @@ -419,13 +429,12 @@ export function discoverProjectDependencies(config: ResolvedConfig): { } export function toDiscoveredDependencies( - config: ResolvedConfig, + environment: Environment, deps: Record, - ssr: boolean, timestamp?: string, ): Record { const browserHash = getOptimizedBrowserHash( - getDepHash(config, ssr).hash, + getDepHash(environment).hash, deps, timestamp, ) @@ -434,10 +443,10 @@ export function toDiscoveredDependencies( const src = deps[id] discovered[id] = { id, - file: getOptimizedDepPath(id, config, ssr), + file: getOptimizedDepPath(environment, id), src, browserHash: browserHash, - exportsData: extractExportsData(src, config, ssr), + exportsData: extractExportsData(environment, src), } } return discovered @@ -452,22 +461,16 @@ export function depsLogString(qualifiedIds: string[]): string { * the metadata and start the server without waiting for the optimizeDeps processing to be completed */ export function runOptimizeDeps( - resolvedConfig: ResolvedConfig, + environment: Environment, depsInfo: Record, - ssr: boolean, ): { cancel: () => Promise result: Promise } { const optimizerContext = { cancelled: false } - const config: ResolvedConfig = { - ...resolvedConfig, - command: 'build', - } - - const depsCacheDir = getDepsCacheDir(resolvedConfig, ssr) - const processingCacheDir = getProcessingDepsCacheDir(resolvedConfig, ssr) + const depsCacheDir = getDepsCacheDir(environment) + const processingCacheDir = getProcessingDepsCacheDir(environment) // Create a temporary directory so we don't need to delete optimized deps // until they have been processed. This also avoids leaving the deps cache @@ -482,7 +485,7 @@ export function runOptimizeDeps( `{\n "type": "module"\n}\n`, ) - const metadata = initDepsOptimizerMetadata(config, ssr) + const metadata = initDepsOptimizerMetadata(environment) metadata.browserHash = getOptimizedBrowserHash( metadata.hash, @@ -594,9 +597,8 @@ export function runOptimizeDeps( const start = performance.now() const preparedRun = prepareEsbuildOptimizerRun( - resolvedConfig, + environment, depsInfo, - ssr, processingCacheDir, optimizerContext, ) @@ -604,7 +606,9 @@ export function runOptimizeDeps( const runResult = preparedRun.then(({ context, idToExports }) => { function disposeContext() { return context?.dispose().catch((e) => { - config.logger.error('Failed to dispose esbuild context', { error: e }) + environment.logger.error('Failed to dispose esbuild context', { + error: e, + }) }) } if (!context || optimizerContext.cancelled) { @@ -644,8 +648,7 @@ export function runOptimizeDeps( // After bundling we have more information and can warn the user about legacy packages // that require manual configuration needsInterop: needsInterop( - config, - ssr, + environment, id, idToExports[id], output, @@ -658,7 +661,7 @@ export function runOptimizeDeps( const id = path .relative(processingCacheDirOutputPath, o) .replace(jsExtensionRE, '') - const file = getOptimizedDepPath(id, resolvedConfig, ssr) + const file = getOptimizedDepPath(environment, id) if ( !findOptimizedDepInfoInRecord( metadata.optimized, @@ -711,20 +714,14 @@ export function runOptimizeDeps( } async function prepareEsbuildOptimizerRun( - resolvedConfig: ResolvedConfig, + environment: Environment, depsInfo: Record, - ssr: boolean, processingCacheDir: string, optimizerContext: { cancelled: boolean }, ): Promise<{ context?: BuildContext idToExports: Record }> { - const config: ResolvedConfig = { - ...resolvedConfig, - command: 'build', - } - // esbuild generates nested directory output with lowest common ancestor base // this is unpredictable and makes it difficult to analyze entry / output // mapping. So what we do here is: @@ -734,7 +731,7 @@ async function prepareEsbuildOptimizerRun( const flatIdDeps: Record = {} const idToExports: Record = {} - const optimizeDeps = getDepOptimizationConfig(config, ssr) + const { optimizeDeps } = environment.options.dev const { plugins: pluginsFromConfig = [], ...esbuildOptions } = optimizeDeps?.esbuildOptions ?? {} @@ -743,7 +740,7 @@ async function prepareEsbuildOptimizerRun( Object.keys(depsInfo).map(async (id) => { const src = depsInfo[id].src! const exportsData = await (depsInfo[id].exportsData ?? - extractExportsData(src, config, ssr)) + extractExportsData(environment, src)) if (exportsData.jsxLoader && !esbuildOptions.loader?.['.js']) { // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. // This is useful for packages such as Gatsby. @@ -761,11 +758,12 @@ async function prepareEsbuildOptimizerRun( if (optimizerContext.cancelled) return { context: undefined, idToExports } const define = { - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || config.mode), + 'process.env.NODE_ENV': JSON.stringify( + process.env.NODE_ENV || environment.config.mode, + ), } - const platform = - ssr && config.ssr?.target !== 'webworker' ? 'node' : 'browser' + const platform = environment.options.webCompatible ? 'browser' : 'node' const external = [...(optimizeDeps?.exclude ?? [])] @@ -773,7 +771,7 @@ async function prepareEsbuildOptimizerRun( if (external.length) { plugins.push(esbuildCjsExternalPlugin(external, platform)) } - plugins.push(esbuildDepPlugin(flatIdDeps, external, config, ssr)) + plugins.push(esbuildDepPlugin(environment, flatIdDeps, external)) const context = await esbuild.context({ absWorkingDir: process.cwd(), @@ -812,20 +810,17 @@ async function prepareEsbuildOptimizerRun( } export async function addManuallyIncludedOptimizeDeps( + environment: Environment, deps: Record, - config: ResolvedConfig, - ssr: boolean, ): Promise { - const { logger } = config - const optimizeDeps = getDepOptimizationConfig(config, ssr) + const { logger } = environment + const { optimizeDeps } = environment.options.dev const optimizeDepsInclude = optimizeDeps?.include ?? [] if (optimizeDepsInclude.length) { const unableToOptimize = (id: string, msg: string) => { if (optimizeDepsInclude.includes(id)) { logger.warn( - `${msg}: ${colors.cyan(id)}, present in '${ - ssr ? 'ssr.' : '' - }optimizeDeps.include'`, + `${msg}: ${colors.cyan(id)}, present in ${environment.name} 'optimizeDeps.include'`, ) } } @@ -834,13 +829,13 @@ export async function addManuallyIncludedOptimizeDeps( for (let i = 0; i < includes.length; i++) { const id = includes[i] if (glob.isDynamicPattern(id)) { - const globIds = expandGlobIds(id, config) + const globIds = expandGlobIds(id, environment.config) includes.splice(i, 1, ...globIds) i += globIds.length - 1 } } - const resolve = createOptimizeDepsIncludeResolver(config, ssr) + const resolve = createOptimizeDepsIncludeResolver(environment) for (const id of includes) { // normalize 'foo >bar` as 'foo > bar' to prevent same id being added // and for pretty printing @@ -875,26 +870,27 @@ export function depsFromOptimizedDepInfo( } export function getOptimizedDepPath( + environment: Environment, id: string, - config: ResolvedConfig, - ssr: boolean, ): string { return normalizePath( - path.resolve(getDepsCacheDir(config, ssr), flattenId(id) + '.js'), + path.resolve(getDepsCacheDir(environment), flattenId(id) + '.js'), ) } -function getDepsCacheSuffix(ssr: boolean): string { - return ssr ? '_ssr' : '' +function getDepsCacheSuffix(environment: Environment): string { + return environment.name === 'client' ? '' : `_${environment.name}` } -export function getDepsCacheDir(config: ResolvedConfig, ssr: boolean): string { - return getDepsCacheDirPrefix(config) + getDepsCacheSuffix(ssr) +export function getDepsCacheDir(environment: Environment): string { + return getDepsCacheDirPrefix(environment) + getDepsCacheSuffix(environment) } -function getProcessingDepsCacheDir(config: ResolvedConfig, ssr: boolean) { +function getProcessingDepsCacheDir(environment: Environment) { return ( - getDepsCacheDirPrefix(config) + getDepsCacheSuffix(ssr) + getTempSuffix() + getDepsCacheDirPrefix(environment) + + getDepsCacheSuffix(environment) + + getTempSuffix() ) } @@ -909,22 +905,22 @@ function getTempSuffix() { ) } -function getDepsCacheDirPrefix(config: ResolvedConfig): string { - return normalizePath(path.resolve(config.cacheDir, 'deps')) +function getDepsCacheDirPrefix(environment: Environment): string { + return normalizePath(path.resolve(environment.config.cacheDir, 'deps')) } export function createIsOptimizedDepFile( - config: ResolvedConfig, + environment: Environment, ): (id: string) => boolean { - const depsCacheDirPrefix = getDepsCacheDirPrefix(config) + const depsCacheDirPrefix = getDepsCacheDirPrefix(environment) return (id) => id.startsWith(depsCacheDirPrefix) } export function createIsOptimizedDepUrl( - config: ResolvedConfig, + environment: Environment, ): (url: string) => boolean { - const { root } = config - const depsCacheDir = getDepsCacheDirPrefix(config) + const { root } = environment.config + const depsCacheDir = getDepsCacheDirPrefix(environment) // determine the url prefix of files inside cache directory const depsCacheDirRelative = normalizePath(path.relative(root, depsCacheDir)) @@ -1060,13 +1056,12 @@ function esbuildOutputFromId( } export async function extractExportsData( + environment: Environment, filePath: string, - config: ResolvedConfig, - ssr: boolean, ): Promise { await init - const optimizeDeps = getDepOptimizationConfig(config, ssr) + const { optimizeDeps } = environment.options.dev const esbuildOptions = optimizeDeps?.esbuildOptions ?? {} if (optimizeDeps.extensions?.some((ext) => filePath.endsWith(ext))) { @@ -1114,13 +1109,12 @@ export async function extractExportsData( } function needsInterop( - config: ResolvedConfig, - ssr: boolean, + environmet: Environment, id: string, exportsData: ExportsData, output?: { exports: string[] }, ): boolean { - if (getDepOptimizationConfig(config, ssr)?.needsInterop?.includes(id)) { + if (environmet.options.dev.optimizeDeps?.needsInterop?.includes(id)) { return true } const { hasModuleSyntax, exports } = exportsData @@ -1160,10 +1154,11 @@ const lockfileFormats = [ }) const lockfileNames = lockfileFormats.map((l) => l.name) -function getConfigHash(config: ResolvedConfig, ssr: boolean): string { +function getConfigHash(environment: Environment): string { // Take config into account // only a subset of config options that can affect dep optimization - const optimizeDeps = getDepOptimizationConfig(config, ssr) + const { optimizeDeps } = environment.options.dev + const { config } = environment const content = JSON.stringify( { mode: process.env.NODE_ENV || config.mode, @@ -1194,8 +1189,8 @@ function getConfigHash(config: ResolvedConfig, ssr: boolean): string { return getHash(content) } -function getLockfileHash(config: ResolvedConfig, ssr: boolean): string { - const lockfilePath = lookupFile(config.root, lockfileNames) +function getLockfileHash(environment: Environment): string { + const lockfilePath = lookupFile(environment.config.root, lockfileNames) let content = lockfilePath ? fs.readFileSync(lockfilePath, 'utf-8') : '' if (lockfilePath) { const lockfileName = path.basename(lockfilePath) @@ -1214,12 +1209,13 @@ function getLockfileHash(config: ResolvedConfig, ssr: boolean): string { return getHash(content) } -function getDepHash( - config: ResolvedConfig, - ssr: boolean, -): { lockfileHash: string; configHash: string; hash: string } { - const lockfileHash = getLockfileHash(config, ssr) - const configHash = getConfigHash(config, ssr) +function getDepHash(environment: Environment): { + lockfileHash: string + configHash: string + hash: string +} { + const lockfileHash = getLockfileHash(environment) + const configHash = getConfigHash(environment) const hash = getHash(lockfileHash + configHash) return { hash, @@ -1265,17 +1261,15 @@ function findOptimizedDepInfoInRecord( } export async function optimizedDepNeedsInterop( + environment: Environment, metadata: DepOptimizationMetadata, file: string, - config: ResolvedConfig, - ssr: boolean, ): Promise { const depInfo = optimizedDepInfoFromFile(metadata, file) if (depInfo?.src && depInfo.needsInterop === undefined) { - depInfo.exportsData ??= extractExportsData(depInfo.src, config, ssr) + depInfo.exportsData ??= extractExportsData(environment, depInfo.src) depInfo.needsInterop = needsInterop( - config, - ssr, + environment, depInfo.id, await depInfo.exportsData, ) diff --git a/packages/vite/src/node/optimizer/optimizer.ts b/packages/vite/src/node/optimizer/optimizer.ts index 096d0bef2cdd54..3a8808345bade9 100644 --- a/packages/vite/src/node/optimizer/optimizer.ts +++ b/packages/vite/src/node/optimizer/optimizer.ts @@ -1,8 +1,8 @@ import colors from 'picocolors' import { createDebugger, getHash, promiseWithResolvers } from '../utils' import type { PromiseWithResolvers } from '../utils' -import { getDepOptimizationConfig } from '../config' -import type { ResolvedConfig, ViteDevServer } from '..' +import type { DevEnvironment } from '../server/environment' +import { devToScanEnvironment } from './scan' import { addManuallyIncludedOptimizeDeps, addOptimizedDepInfo, @@ -15,11 +15,16 @@ import { getOptimizedDepPath, initDepsOptimizerMetadata, loadCachedDepOptimizationMetadata, - optimizeServerSsrDeps, + optimizeExplicitEnvironmentDeps, runOptimizeDeps, toDiscoveredDependencies, -} from '.' -import type { DepOptimizationResult, DepsOptimizer, OptimizedDepInfo } from '.' +} from './index' +import type { + DepOptimizationMetadata, + DepOptimizationResult, + DepsOptimizer, + OptimizedDepInfo, +} from './index' const debug = createDebugger('vite:deps') @@ -29,95 +34,45 @@ const debug = createDebugger('vite:deps') */ const debounceMs = 100 -const depsOptimizerMap = new WeakMap() -const devSsrDepsOptimizerMap = new WeakMap() - -export function getDepsOptimizer( - config: ResolvedConfig, - ssr?: boolean, -): DepsOptimizer | undefined { - return (ssr ? devSsrDepsOptimizerMap : depsOptimizerMap).get(config) -} - -export async function initDepsOptimizer( - config: ResolvedConfig, - server: ViteDevServer, -): Promise { - if (!getDepsOptimizer(config, false)) { - await createDepsOptimizer(config, server) - } -} - -let creatingDevSsrOptimizer: Promise | undefined -export async function initDevSsrDepsOptimizer( - config: ResolvedConfig, - server: ViteDevServer, -): Promise { - if (getDepsOptimizer(config, true)) { - // ssr - return - } - if (creatingDevSsrOptimizer) { - return creatingDevSsrOptimizer - } - creatingDevSsrOptimizer = (async function () { - // Important: scanning needs to be done before starting the SSR dev optimizer - // If ssrLoadModule is called before server.listen(), the main deps optimizer - // will not be yet created - const ssr = false - if (!getDepsOptimizer(config, ssr)) { - await initDepsOptimizer(config, server) - } - await getDepsOptimizer(config, ssr)!.scanProcessing - - await createDevSsrDepsOptimizer(config) - creatingDevSsrOptimizer = undefined - })() - return await creatingDevSsrOptimizer -} - -async function createDepsOptimizer( - config: ResolvedConfig, - server: ViteDevServer, -): Promise { - const { logger } = config - const ssr = false +export function createDepsOptimizer( + environment: DevEnvironment, +): DepsOptimizer { + const { logger } = environment const sessionTimestamp = Date.now().toString() - const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr) - let debounceProcessingHandle: NodeJS.Timeout | undefined let closed = false - let metadata = - cachedMetadata || initDepsOptimizerMetadata(config, ssr, sessionTimestamp) - - const options = getDepOptimizationConfig(config, ssr) + const options = environment.options.dev.optimizeDeps const { noDiscovery, holdUntilCrawlEnd } = options + let metadata: DepOptimizationMetadata = initDepsOptimizerMetadata( + environment, + sessionTimestamp, + ) + const depsOptimizer: DepsOptimizer = { + init, metadata, registerMissingImport, run: () => debouncedProcessing(0), - isOptimizedDepFile: createIsOptimizedDepFile(config), - isOptimizedDepUrl: createIsOptimizedDepUrl(config), + isOptimizedDepFile: createIsOptimizedDepFile(environment), + isOptimizedDepUrl: createIsOptimizedDepUrl(environment), getOptimizedDepId: (depInfo: OptimizedDepInfo) => `${depInfo.file}?v=${depInfo.browserHash}`, close, options, } - depsOptimizerMap.set(config, depsOptimizer) - let newDepsDiscovered = false let newDepsToLog: string[] = [] let newDepsToLogHandle: NodeJS.Timeout | undefined const logNewlyDiscoveredDeps = () => { if (newDepsToLog.length) { - config.logger.info( + logger.info( colors.green( `✨ new dependencies optimized: ${depsLogString(newDepsToLog)}`, ), @@ -132,7 +87,7 @@ async function createDepsOptimizer( let discoveredDepsWhileScanning: string[] = [] const logDiscoveredDepsWhileScanning = () => { if (discoveredDepsWhileScanning.length) { - config.logger.info( + logger.info( colors.green( `✨ discovered while scanning: ${depsLogString( discoveredDepsWhileScanning, @@ -159,7 +114,7 @@ async function createDepsOptimizer( let enqueuedRerun: (() => void) | undefined let currentlyProcessing = false - let firstRunCalled = !!cachedMetadata + let firstRunCalled = false let warnAboutMissedDependencies = false // If this is a cold run, we wait for static imports discovered @@ -167,10 +122,6 @@ async function createDepsOptimizer( // On warm start or after the first optimization is run, we use a simpler // debounce strategy each time a new dep is discovered. let waitingForCrawlEnd = false - if (!cachedMetadata) { - server._onCrawlEnd(onCrawlEnd) - waitingForCrawlEnd = true - } let optimizationResult: | { @@ -195,96 +146,113 @@ async function createDepsOptimizer( ]) } - if (!cachedMetadata) { - // Enter processing state until crawl of static imports ends - currentlyProcessing = true + let inited = false + async function init() { + if (inited) return + inited = true - // Initialize discovered deps with manually added optimizeDeps.include info + const cachedMetadata = await loadCachedDepOptimizationMetadata(environment) - const manuallyIncludedDeps: Record = {} - await addManuallyIncludedOptimizeDeps(manuallyIncludedDeps, config, ssr) + firstRunCalled = !!cachedMetadata - const manuallyIncludedDepsInfo = toDiscoveredDependencies( - config, - manuallyIncludedDeps, - ssr, - sessionTimestamp, - ) + metadata = depsOptimizer.metadata = + cachedMetadata || initDepsOptimizerMetadata(environment, sessionTimestamp) - for (const depInfo of Object.values(manuallyIncludedDepsInfo)) { - addOptimizedDepInfo(metadata, 'discovered', { - ...depInfo, - processing: depOptimizationProcessing.promise, - }) - newDepsDiscovered = true - } + if (!cachedMetadata) { + environment._onCrawlEnd(onCrawlEnd) + waitingForCrawlEnd = true - if (noDiscovery) { - // We don't need to scan for dependencies or wait for the static crawl to end - // Run the first optimization run immediately - runOptimizer() - } else { - // Important, the scanner is dev only - depsOptimizer.scanProcessing = new Promise((resolve) => { - // Runs in the background in case blocking high priority tasks - ;(async () => { - try { - debug?.(colors.green(`scanning for dependencies...`)) - - discover = discoverProjectDependencies(config) - const deps = await discover.result - discover = undefined - - const manuallyIncluded = Object.keys(manuallyIncludedDepsInfo) - discoveredDepsWhileScanning.push( - ...Object.keys(metadata.discovered).filter( - (dep) => !deps[dep] && !manuallyIncluded.includes(dep), - ), - ) + // Enter processing state until crawl of static imports ends + currentlyProcessing = true + + // Initialize discovered deps with manually added optimizeDeps.include info + + const manuallyIncludedDeps: Record = {} + await addManuallyIncludedOptimizeDeps(environment, manuallyIncludedDeps) - // Add these dependencies to the discovered list, as these are currently - // used by the preAliasPlugin to support aliased and optimized deps. - // This is also used by the CJS externalization heuristics in legacy mode - for (const id of Object.keys(deps)) { - if (!metadata.discovered[id]) { - addMissingDep(id, deps[id]) + const manuallyIncludedDepsInfo = toDiscoveredDependencies( + environment, + manuallyIncludedDeps, + sessionTimestamp, + ) + + for (const depInfo of Object.values(manuallyIncludedDepsInfo)) { + addOptimizedDepInfo(metadata, 'discovered', { + ...depInfo, + processing: depOptimizationProcessing.promise, + }) + newDepsDiscovered = true + } + + if (noDiscovery) { + // We don't need to scan for dependencies or wait for the static crawl to end + // Run the first optimization run immediately + runOptimizer() + } else { + // Important, the scanner is dev only + depsOptimizer.scanProcessing = new Promise((resolve) => { + // Runs in the background in case blocking high priority tasks + ;(async () => { + try { + debug?.(colors.green(`scanning for dependencies...`)) + + discover = discoverProjectDependencies( + devToScanEnvironment(environment), + ) + const deps = await discover.result + discover = undefined + + const manuallyIncluded = Object.keys(manuallyIncludedDepsInfo) + discoveredDepsWhileScanning.push( + ...Object.keys(metadata.discovered).filter( + (dep) => !deps[dep] && !manuallyIncluded.includes(dep), + ), + ) + + // Add these dependencies to the discovered list, as these are currently + // used by the preAliasPlugin to support aliased and optimized deps. + // This is also used by the CJS externalization heuristics in legacy mode + for (const id of Object.keys(deps)) { + if (!metadata.discovered[id]) { + addMissingDep(id, deps[id]) + } } - } - const knownDeps = prepareKnownDeps() - startNextDiscoveredBatch() - - // For dev, we run the scanner and the first optimization - // run on the background - optimizationResult = runOptimizeDeps(config, knownDeps, ssr) - - // If the holdUntilCrawlEnd stratey is used, we wait until crawling has - // ended to decide if we send this result to the browser or we need to - // do another optimize step - if (!holdUntilCrawlEnd) { - // If not, we release the result to the browser as soon as the scanner - // is done. If the scanner missed any dependency, and a new dependency - // is discovered while crawling static imports, then there will be a - // full-page reload if new common chunks are generated between the old - // and new optimized deps. - optimizationResult.result.then((result) => { - // Check if the crawling of static imports has already finished. In that - // case, the result is handled by the onCrawlEnd callback - if (!waitingForCrawlEnd) return - - optimizationResult = undefined // signal that we'll be using the result - - runOptimizer(result) - }) + const knownDeps = prepareKnownDeps() + startNextDiscoveredBatch() + + // For dev, we run the scanner and the first optimization + // run on the background + optimizationResult = runOptimizeDeps(environment, knownDeps) + + // If the holdUntilCrawlEnd stratey is used, we wait until crawling has + // ended to decide if we send this result to the browser or we need to + // do another optimize step + if (!holdUntilCrawlEnd) { + // If not, we release the result to the browser as soon as the scanner + // is done. If the scanner missed any dependency, and a new dependency + // is discovered while crawling static imports, then there will be a + // full-page reload if new common chunks are generated between the old + // and new optimized deps. + optimizationResult.result.then((result) => { + // Check if the crawling of static imports has already finished. In that + // case, the result is handled by the onCrawlEnd callback + if (!waitingForCrawlEnd) return + + optimizationResult = undefined // signal that we'll be using the result + + runOptimizer(result) + }) + } + } catch (e) { + logger.error(e.stack || e.message) + } finally { + resolve() + depsOptimizer.scanProcessing = undefined } - } catch (e) { - logger.error(e.stack || e.message) - } finally { - resolve() - depsOptimizer.scanProcessing = undefined - } - })() - }) + })() + }) + } } } @@ -303,6 +271,7 @@ async function createDepsOptimizer( function prepareKnownDeps() { const knownDeps: Record = {} // Clone optimized info objects, fileHash, browserHash may be changed for them + const metadata = depsOptimizer.metadata! for (const dep of Object.keys(metadata.optimized)) { knownDeps[dep] = { ...metadata.optimized[dep] } } @@ -351,7 +320,7 @@ async function createDepsOptimizer( const knownDeps = prepareKnownDeps() startNextDiscoveredBatch() - optimizationResult = runOptimizeDeps(config, knownDeps, ssr) + optimizationResult = runOptimizeDeps(environment, knownDeps) processingResult = await optimizationResult.result optimizationResult = undefined } @@ -443,7 +412,7 @@ async function createDepsOptimizer( logNewlyDiscoveredDeps() if (warnAboutMissedDependencies) { logDiscoveredDepsWhileScanning() - config.logger.info( + logger.info( colors.magenta( `❗ add these dependencies to optimizeDeps.include to speed up cold start`, ), @@ -485,7 +454,7 @@ async function createDepsOptimizer( logNewlyDiscoveredDeps() if (warnAboutMissedDependencies) { logDiscoveredDepsWhileScanning() - config.logger.info( + logger.info( colors.magenta( `❗ add these dependencies to optimizeDeps.include to avoid a full page reload during cold start`, ), @@ -502,7 +471,7 @@ async function createDepsOptimizer( }, ) if (needsInteropMismatch.length > 0) { - config.logger.warn( + logger.warn( `Mixed ESM and CJS detected in ${colors.yellow( needsInteropMismatch.join(', '), )}, add ${ @@ -537,9 +506,9 @@ async function createDepsOptimizer( // Cached transform results have stale imports (resolved to // old locations) so they need to be invalidated before the page is // reloaded. - server.moduleGraph.invalidateAll() + environment.moduleGraph.invalidateAll() - server.hot.send({ + environment.hot.send({ type: 'full-reload', path: '*', }) @@ -607,7 +576,7 @@ async function createDepsOptimizer( return addOptimizedDepInfo(metadata, 'discovered', { id, - file: getOptimizedDepPath(id, config, ssr), + file: getOptimizedDepPath(environment, id), src: resolved, // Adding a browserHash to this missing dependency that is unique to // the current state of known + missing deps. If its optimizeDeps run @@ -621,7 +590,7 @@ async function createDepsOptimizer( // loading of this pre-bundled dep needs to await for its processing // promise to be resolved processing: depOptimizationProcessing.promise, - exportsData: extractExportsData(resolved, config, ssr), + exportsData: extractExportsData(environment, resolved), }) } @@ -657,7 +626,7 @@ async function createDepsOptimizer( // It normally should be over by the time crawling of user code ended await depsOptimizer.scanProcessing - if (optimizationResult && !config.optimizeDeps.noDiscovery) { + if (optimizationResult && !options.noDiscovery) { // In the holdUntilCrawlEnd strategy, we don't release the result of the // post-scanner optimize step to the browser until we reach this point // If there are new dependencies, we do another optimize run, if not, we @@ -754,33 +723,43 @@ async function createDepsOptimizer( debouncedProcessing(0) } } -} -async function createDevSsrDepsOptimizer( - config: ResolvedConfig, -): Promise { - const metadata = await optimizeServerSsrDeps(config) + return depsOptimizer +} +export function createExplicitDepsOptimizer( + environment: DevEnvironment, +): DepsOptimizer { const depsOptimizer = { - metadata, - isOptimizedDepFile: createIsOptimizedDepFile(config), - isOptimizedDepUrl: createIsOptimizedDepUrl(config), + metadata: initDepsOptimizerMetadata(environment), + isOptimizedDepFile: createIsOptimizedDepFile(environment), + isOptimizedDepUrl: createIsOptimizedDepUrl(environment), getOptimizedDepId: (depInfo: OptimizedDepInfo) => `${depInfo.file}?v=${depInfo.browserHash}`, registerMissingImport: () => { throw new Error( - 'Vite Internal Error: registerMissingImport is not supported in dev SSR', + `Vite Internal Error: registerMissingImport is not supported in dev ${environment.name}`, ) }, + init, // noop, there is no scanning during dev SSR // the optimizer blocks the server start run: () => {}, close: async () => {}, - options: config.ssr.optimizeDeps, + options: environment.options.dev.optimizeDeps, } - devSsrDepsOptimizerMap.set(config, depsOptimizer) + + let inited = false + async function init() { + if (inited) return + inited = true + + depsOptimizer.metadata = await optimizeExplicitEnvironmentDeps(environment) + } + + return depsOptimizer } function findInteropMismatches( diff --git a/packages/vite/src/node/optimizer/resolve.ts b/packages/vite/src/node/optimizer/resolve.ts index b76634dd8ae8cf..552d22da615e76 100644 --- a/packages/vite/src/node/optimizer/resolve.ts +++ b/packages/vite/src/node/optimizer/resolve.ts @@ -5,22 +5,23 @@ import type { ResolvedConfig } from '../config' import { escapeRegex, getNpmPackageName } from '../utils' import { resolvePackageData } from '../packages' import { slash } from '../../shared/utils' +import type { Environment } from '../environment' +import { createIdResolver } from '../idResolver' export function createOptimizeDepsIncludeResolver( - config: ResolvedConfig, - ssr: boolean, + environment: Environment, ): (id: string) => Promise { - const resolve = config.createResolver({ + const { config } = environment + const resolve = createIdResolver(config, { asSrc: false, scan: true, - ssrOptimizeCheck: ssr, - ssrConfig: config.ssr, + ssrOptimizeCheck: environment.name !== 'client', // TODO:depsOptimizer packageCache: new Map(), }) return async (id: string) => { const lastArrowIndex = id.lastIndexOf('>') if (lastArrowIndex === -1) { - return await resolve(id, undefined, undefined, ssr) + return await resolve(environment, id, undefined) } // split nested selected id by last '>', for example: // 'foo > bar > baz' => 'foo > bar' & 'baz' @@ -32,10 +33,9 @@ export function createOptimizeDepsIncludeResolver( config.resolve.preserveSymlinks, ) return await resolve( + environment, nestedPath, path.resolve(basedir, 'package.json'), - undefined, - ssr, ) } } diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 1cdef6c339c103..02f547790f339f 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -11,6 +11,7 @@ import type { Plugin, } from 'esbuild' import esbuild, { formatMessages, transform } from 'esbuild' +import type { PartialResolvedId } from 'rollup' import colors from 'picocolors' import type { ResolvedConfig } from '..' import { @@ -34,13 +35,75 @@ import { virtualModulePrefix, virtualModuleRE, } from '../utils' -import type { PluginContainer } from '../server/pluginContainer' -import { createPluginContainer } from '../server/pluginContainer' +import { resolveEnvironmentPlugins } from '../plugin' +import type { EnvironmentPluginContainer } from '../server/pluginContainer' +import { createEnvironmentPluginContainer } from '../server/pluginContainer' +import { BaseEnvironment } from '../baseEnvironment' +import type { DevEnvironment } from '../server/environment' import { transformGlobImport } from '../plugins/importMetaGlob' import { cleanUrl } from '../../shared/utils' import { loadTsconfigJsonForFile } from '../plugins/esbuild' -type ResolveIdOptions = Parameters[2] +export class ScanEnvironment extends BaseEnvironment { + mode = 'scan' as const + + get pluginContainer(): EnvironmentPluginContainer { + if (!this._pluginContainer) + throw new Error( + `${this.name} environment.pluginContainer called before initialized`, + ) + return this._pluginContainer + } + /** + * @internal + */ + _pluginContainer: EnvironmentPluginContainer | undefined + + async init(): Promise { + if (this._initiated) { + return + } + this._initiated = true + this._plugins = resolveEnvironmentPlugins(this) + this._pluginContainer = await createEnvironmentPluginContainer( + this, + this.plugins, + ) + await this._pluginContainer.buildStart({}) + } +} + +// Restric access to the module graph and the server while scanning +export function devToScanEnvironment( + environment: DevEnvironment, +): ScanEnvironment { + return { + mode: 'scan', + get name() { + return environment.name + }, + get config() { + return environment.config + }, + get options() { + return environment.options + }, + get logger() { + return environment.logger + }, + get pluginContainer() { + return environment.pluginContainer + }, + get plugins() { + return environment.plugins + }, + } as unknown as ScanEnvironment +} + +type ResolveIdOptions = Omit< + Parameters[2], + 'environment' +> const debug = createDebugger('vite:deps') @@ -57,7 +120,7 @@ const htmlTypesRE = /\.(html|vue|svelte|astro|imba)$/ export const importsRE = /(? Promise result: Promise<{ deps: Record @@ -74,13 +137,16 @@ export function scanImports(config: ResolvedConfig): { const scanContext = { cancelled: false } const esbuildContext: Promise = computeEntries( - config, + environment.config, ).then((computedEntries) => { entries = computedEntries if (!entries.length) { - if (!config.optimizeDeps.entries && !config.optimizeDeps.include) { - config.logger.warn( + if ( + !environment.config.optimizeDeps.entries && + !environment.options.dev.optimizeDeps.include + ) { + environment.logger.warn( colors.yellow( '(!) Could not auto-determine entry point from rollupOptions or html files ' + 'and there are no explicit optimizeDeps.include patterns. ' + @@ -97,14 +163,22 @@ export function scanImports(config: ResolvedConfig): { .map((entry) => `\n ${colors.dim(entry)}`) .join('')}`, ) - return prepareEsbuildScanner(config, entries, deps, missing, scanContext) + return prepareEsbuildScanner( + environment, + entries, + deps, + missing, + scanContext, + ) }) const result = esbuildContext .then((context) => { function disposeContext() { return context?.dispose().catch((e) => { - config.logger.error('Failed to dispose esbuild context', { error: e }) + environment.logger.error('Failed to dispose esbuild context', { + error: e, + }) }) } if (!context || scanContext?.cancelled) { @@ -171,6 +245,7 @@ export function scanImports(config: ResolvedConfig): { async function computeEntries(config: ResolvedConfig) { let entries: string[] = [] + // TODO: Should entries be per-environment? const explicitEntryPatterns = config.optimizeDeps.entries const buildInput = config.build.rollupOptions?.input @@ -203,20 +278,18 @@ async function computeEntries(config: ResolvedConfig) { } async function prepareEsbuildScanner( - config: ResolvedConfig, + environment: ScanEnvironment, entries: string[], deps: Record, missing: Record, scanContext?: { cancelled: boolean }, ): Promise { - const container = await createPluginContainer(config) - if (scanContext?.cancelled) return - const plugin = esbuildScanPlugin(config, container, deps, missing, entries) + const plugin = esbuildScanPlugin(environment, deps, missing, entries) const { plugins = [], ...esbuildOptions } = - config.optimizeDeps?.esbuildOptions ?? {} + environment.options.dev.optimizeDeps.esbuildOptions ?? {} // The plugin pipeline automatically loads the closest tsconfig.json. // But esbuild doesn't support reading tsconfig.json if the plugin has resolved the path (https://github.com/evanw/esbuild/issues/2265). @@ -226,7 +299,7 @@ async function prepareEsbuildScanner( let tsconfigRaw = esbuildOptions.tsconfigRaw if (!tsconfigRaw && !esbuildOptions.tsconfig) { const tsconfigResult = await loadTsconfigJsonForFile( - path.join(config.root, '_dummy.js'), + path.join(environment.config.root, '_dummy.js'), ) if (tsconfigResult.compilerOptions?.experimentalDecorators) { tsconfigRaw = { compilerOptions: { experimentalDecorators: true } } @@ -287,24 +360,18 @@ const langRE = /\blang\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/i const contextRE = /\bcontext\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/i function esbuildScanPlugin( - config: ResolvedConfig, - container: PluginContainer, + environment: ScanEnvironment, depImports: Record, missing: Record, entries: string[], ): Plugin { const seen = new Map() - - const resolve = async ( + async function resolveId( id: string, importer?: string, options?: ResolveIdOptions, - ) => { - const key = id + (importer && path.dirname(importer)) - if (seen.has(key)) { - return seen.get(key) - } - const resolved = await container.resolveId( + ): Promise { + return environment.pluginContainer.resolveId( id, importer && normalizePath(importer), { @@ -312,14 +379,26 @@ function esbuildScanPlugin( scan: true, }, ) + } + const resolve = async ( + id: string, + importer?: string, + options?: ResolveIdOptions, + ) => { + const key = id + (importer && path.dirname(importer)) + if (seen.has(key)) { + return seen.get(key) + } + const resolved = await resolveId(id, importer, options) const res = resolved?.id seen.set(key, res) return res } - const include = config.optimizeDeps?.include + const optimizeDepsOptions = environment.options.dev.optimizeDeps + const include = optimizeDepsOptions.include const exclude = [ - ...(config.optimizeDeps?.exclude || []), + ...(optimizeDepsOptions.exclude ?? []), '@vite/client', '@vite/env', ] @@ -347,7 +426,7 @@ function esbuildScanPlugin( const result = await transformGlobImport( transpiledContents, id, - config.root, + environment.config.root, resolve, ) @@ -393,7 +472,7 @@ function esbuildScanPlugin( // bare import resolve, and recorded as optimization dep. if ( isInNodeModules(resolved) && - isOptimizable(resolved, config.optimizeDeps) + isOptimizable(resolved, optimizeDepsOptions) ) return return { @@ -547,11 +626,11 @@ function esbuildScanPlugin( } if (isInNodeModules(resolved) || include?.includes(id)) { // dependency or forced included, externalize and stop crawling - if (isOptimizable(resolved, config.optimizeDeps)) { + if (isOptimizable(resolved, optimizeDepsOptions)) { depImports[id] = resolved } return externalUnlessEntry({ path: id }) - } else if (isScannable(resolved, config.optimizeDeps.extensions)) { + } else if (isScannable(resolved, optimizeDepsOptions.extensions)) { const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined // linked package, keep crawling return { @@ -612,7 +691,7 @@ function esbuildScanPlugin( if (resolved) { if ( shouldExternalizeDep(resolved, id) || - !isScannable(resolved, config.optimizeDeps.extensions) + !isScannable(resolved, optimizeDepsOptions.extensions) ) { return externalUnlessEntry({ path: id }) } @@ -637,13 +716,15 @@ function esbuildScanPlugin( let ext = path.extname(id).slice(1) if (ext === 'mjs') ext = 'js' + // TODO: Why are we using config.esbuild instead of config.optimizeDeps.esbuildOptions here? + const esbuildConfig = environment.config.esbuild let contents = await fsp.readFile(id, 'utf-8') - if (ext.endsWith('x') && config.esbuild && config.esbuild.jsxInject) { - contents = config.esbuild.jsxInject + `\n` + contents + if (ext.endsWith('x') && esbuildConfig && esbuildConfig.jsxInject) { + contents = esbuildConfig.jsxInject + `\n` + contents } const loader = - config.optimizeDeps?.esbuildOptions?.loader?.[`.${ext}`] || + optimizeDepsOptions.esbuildOptions?.loader?.[`.${ext}`] ?? (ext as Loader) if (contents.includes('import.meta.glob')) { diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index 5af667d2417cc9..b03ecf37525db3 100644 --- a/packages/vite/src/node/packages.ts +++ b/packages/vite/src/node/packages.ts @@ -260,7 +260,7 @@ export function watchPackageDataPlugin(packageCache: PackageCache): Plugin { invalidatePackageData(packageCache, path.normalize(id)) } }, - handleHotUpdate({ file }) { + hotUpdate({ file }) { if (file.endsWith('/package.json')) { invalidatePackageData(packageCache, path.normalize(file)) } diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 8ef2f228115477..97b7555b4a274c 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -2,19 +2,25 @@ import type { CustomPluginOptions, LoadResult, ObjectHook, - PluginContext, ResolveIdResult, Plugin as RollupPlugin, - TransformPluginContext, + PluginContext as RollupPluginContext, + TransformPluginContext as RollupTransformPluginContext, TransformResult, } from 'rollup' -export type { PluginContext } from 'rollup' -import type { ConfigEnv, ResolvedConfig, UserConfig } from './config' -import type { ServerHook } from './server' +import type { + ConfigEnv, + EnvironmentOptions, + ResolvedConfig, + UserConfig, +} from './config' +import type { ServerHook, ViteDevServer } from './server' import type { IndexHtmlTransform } from './plugins/html' -import type { ModuleNode } from './server/moduleGraph' -import type { HmrContext } from './server/hmr' +import type { EnvironmentModuleNode } from './server/moduleGraph' +import type { ModuleNode } from './server/mixedModuleGraph' +import type { HmrContext, HotUpdateContext } from './server/hmr' import type { PreviewServerHook } from './preview' +import type { Environment } from './environment' /** * Vite plugins extends the Rollup plugin interface with a few extra @@ -36,8 +42,137 @@ import type { PreviewServerHook } from './preview' * * If a plugin should be applied only for server or build, a function format * config file can be used to conditional determine the plugins to use. + * + * The current module environment can be accessed from the context for the + * buildStart, resolveId, transform, load, and buildEnd, hooks + * + * The current environment can be accessed from the context for the + * buildStart, resolveId, transform, load, and buildEnd, hooks. It can be a dev + * or a build environment. Plugins can use this.environment.mode === 'dev' to + * check if they have access to dev specific APIs. + */ + +export interface PluginContextExtension { + /** + * Vite-specific environment instance + */ + environment: Environment +} + +export interface PluginContext + extends RollupPluginContext, + PluginContextExtension {} + +export interface ResolveIdPluginContext + extends RollupPluginContext, + PluginContextExtension {} + +export interface TransformPluginContext + extends RollupTransformPluginContext, + PluginContextExtension {} + +// Argument Rollup types to have the PluginContextExtension +declare module 'rollup' { + export interface PluginContext extends PluginContextExtension {} +} + +/** + * There are two types of plugins in Vite. App plugins and environment plugins. + * Environment Plugins are defined by a constructor function that will be called + * once per each environment allowing users to have completely different plugins + * for each of them. The constructor gets the resolved environment after the server + * and builder has already been created simplifying config access and cache + * management for for environment specific plugins. + * Environment Plugins are closer to regular rollup plugins. They can't define + * app level hooks (like config, configResolved, configureServer, etc). */ export interface Plugin extends RollupPlugin { + /** + * Perform custom handling of HMR updates. + * The handler receives a context containing changed filename, timestamp, a + * list of modules affected by the file change, and the dev server instance. + * + * - The hook can return a filtered list of modules to narrow down the update. + * e.g. for a Vue SFC, we can narrow down the part to update by comparing + * the descriptors. + * + * - The hook can also return an empty array and then perform custom updates + * by sending a custom hmr payload via server.hot.send(). + * + * - If the hook doesn't return a value, the hmr update will be performed as + * normal. + */ + hotUpdate?: ObjectHook< + ( + this: void, + ctx: HotUpdateContext, + ) => + | Array + | void + | Promise | void> + > + + /** + * extend hooks with ssr flag + */ + resolveId?: ObjectHook< + ( + this: ResolveIdPluginContext, + source: string, + importer: string | undefined, + options: { + attributes: Record + custom?: CustomPluginOptions + /** + * @deprecated use this.environment + */ + ssr?: boolean + /** + * @internal + */ + scan?: boolean + isEntry: boolean + }, + ) => Promise | ResolveIdResult + > + load?: ObjectHook< + ( + this: PluginContext, + id: string, + options?: { + /** + * @deprecated use this.environment + */ + ssr?: boolean + /** + * @internal + */ + html?: boolean + }, + ) => Promise | LoadResult + > + transform?: ObjectHook< + ( + this: TransformPluginContext, + code: string, + id: string, + options?: { + /** + * @deprecated use this.environment + */ + ssr?: boolean + }, + ) => Promise | TransformResult + > + /** + * Opt-in this plugin into the shared plugins pipeline. + * For backward-compatibility, plugins are re-recreated for each environment + * during `vite build --app` + * We have an opt-in per plugin, and a general `builder.sharedPlugins` + * In a future major, we'll flip the default to be shared by default + * @experimental + */ + sharedDuringBuild?: boolean /** * Enforce plugin invocation tier similar to webpack loaders. Hooks ordering * is still subject to the `order` property in the hook object. @@ -59,6 +194,11 @@ export interface Plugin extends RollupPlugin { | 'serve' | 'build' | ((this: void, config: UserConfig, env: ConfigEnv) => boolean) + /** + * Define environments where this plugin should be active + * By default, the plugin is active in all environments + */ + applyToEnvironment?: (environment: Environment) => boolean /** * Modify vite config before it's resolved. The hook can either mutate the * passed-in config directly, or return a partial config object that will be @@ -78,6 +218,28 @@ export interface Plugin extends RollupPlugin { | void | Promise | null | void> > + /** + * Modify environment configs before it's resolved. The hook can either mutate the + * passed-in environment config directly, or return a partial config object that will be + * deeply merged into existing config. + * This hook is called for each environment with a partially resolved environment config + * that already accounts for the default environment config values set at the root level. + * If plugins need to modify the config of a given environment, they should do it in this + * hook instead of the config hook. Leaving the config hook only for modifying the root + * default environment config. + */ + configEnvironment?: ObjectHook< + ( + this: void, + name: string, + config: EnvironmentOptions, + env: ConfigEnv, + ) => + | EnvironmentOptions + | null + | void + | Promise + > /** * Use this hook to read and store the final resolved vite config. */ @@ -120,7 +282,9 @@ export interface Plugin extends RollupPlugin { * `{ order: 'pre', handler: hook }` */ transformIndexHtml?: IndexHtmlTransform + /** + * @deprecated * Perform custom handling of HMR updates. * The handler receives a context containing changed filename, timestamp, a * list of modules affected by the file change, and the dev server instance. @@ -141,42 +305,6 @@ export interface Plugin extends RollupPlugin { ctx: HmrContext, ) => Array | void | Promise | void> > - - /** - * extend hooks with ssr flag - */ - resolveId?: ObjectHook< - ( - this: PluginContext, - source: string, - importer: string | undefined, - options: { - attributes: Record - custom?: CustomPluginOptions - ssr?: boolean - /** - * @internal - */ - scan?: boolean - isEntry: boolean - }, - ) => Promise | ResolveIdResult - > - load?: ObjectHook< - ( - this: PluginContext, - id: string, - options?: { ssr?: boolean }, - ) => Promise | LoadResult - > - transform?: ObjectHook< - ( - this: TransformPluginContext, - code: string, - id: string, - options?: { ssr?: boolean }, - ) => Promise | TransformResult - > } export type HookHandler = T extends ObjectHook ? H : T @@ -184,3 +312,16 @@ export type HookHandler = T extends ObjectHook ? H : T export type PluginWithRequiredHook = Plugin & { [P in K]: NonNullable } + +type Thenable = T | Promise + +type FalsyPlugin = false | null | undefined + +export type PluginOption = Thenable + +export function resolveEnvironmentPlugins(environment: Environment): Plugin[] { + return environment.config.plugins.filter( + (plugin) => + !plugin.applyToEnvironment || plugin.applyToEnvironment(environment), + ) +} diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 2ff5b101529982..d0c5d8dffe1361 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -3,18 +3,14 @@ import { parse as parseUrl } from 'node:url' import fsp from 'node:fs/promises' import { Buffer } from 'node:buffer' import * as mrmime from 'mrmime' -import type { - NormalizedOutputOptions, - PluginContext, - RenderedChunk, -} from 'rollup' +import type { NormalizedOutputOptions, RenderedChunk } from 'rollup' import MagicString from 'magic-string' import colors from 'picocolors' import { createToImportMetaURLBasedRelativeRuntime, toOutputFilePathInJS, } from '../build' -import type { Plugin } from '../plugin' +import type { Plugin, PluginContext } from '../plugin' import type { ResolvedConfig } from '../config' import { checkPublicFile } from '../publicDir' import { @@ -29,15 +25,15 @@ import { urlRE, } from '../utils' import { DEFAULT_ASSETS_INLINE_LIMIT, FS_PREFIX } from '../constants' -import type { ModuleGraph } from '../server/moduleGraph' import { cleanUrl, withTrailingSlash } from '../../shared/utils' +import type { Environment } from '../environment' // referenceId is base64url but replaces - with $ export const assetUrlRE = /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g const jsSourceMapRE = /\.[cm]?js\.map$/ -const assetCache = new WeakMap>() +const assetCache = new WeakMap>() // chunk.name is the basename for the asset ignoring the directory structure // For the manifest, we need to preserve the original file path and isEntry @@ -46,8 +42,8 @@ export interface GeneratedAssetMeta { originalName: string isEntry?: boolean } -export const generatedAssets = new WeakMap< - ResolvedConfig, +export const generatedAssetsMap = new WeakMap< + Environment, Map >() @@ -63,11 +59,13 @@ export function registerCustomMime(): void { export function renderAssetUrlInJS( ctx: PluginContext, - config: ResolvedConfig, chunk: RenderedChunk, opts: NormalizedOutputOptions, code: string, ): MagicString | undefined { + const environment = ctx.environment! + const { config } = environment + const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime( opts.format, config.isWorker, @@ -142,18 +140,13 @@ const viteBuildPublicIdPrefix = '\0vite:asset:public' export function assetPlugin(config: ResolvedConfig): Plugin { registerCustomMime() - let moduleGraph: ModuleGraph | undefined - return { name: 'vite:asset', buildStart() { - assetCache.set(config, new Map()) - generatedAssets.set(config, new Map()) - }, - - configureServer(server) { - moduleGraph = server.moduleGraph + if (!this.environment) return + assetCache.set(this.environment, new Map()) + generatedAssetsMap.set(this.environment, new Map()) }, resolveId(id) { @@ -170,7 +163,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } }, - async load(id) { + async load(id, options) { if (id.startsWith(viteBuildPublicIdPrefix)) { id = id.slice(viteBuildPublicIdPrefix.length) } @@ -196,14 +189,15 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } id = removeUrlQuery(id) - let url = await fileToUrl(id, config, this) + let url = await fileToUrl(this, id) // Inherit HMR timestamp if this asset was invalidated - if (moduleGraph) { - const mod = moduleGraph.getModuleById(id) - if (mod && mod.lastHMRTimestamp > 0) { - url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) - } + const environment = this.environment + const mod = + environment?.mode === 'dev' && + environment?.moduleGraph.getModuleById(id) + if (mod && mod.lastHMRTimestamp > 0) { + url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) } return { @@ -218,12 +212,12 @@ export function assetPlugin(config: ResolvedConfig): Plugin { }, renderChunk(code, chunk, opts) { - const s = renderAssetUrlInJS(this, config, chunk, opts, code) + const s = renderAssetUrlInJS(this, chunk, opts, code) if (s) { return { code: s.toString(), - map: config.build.sourcemap + map: this.environment?.options.build.sourcemap ? s.generateMap({ hires: 'boundary' }) : null, } @@ -249,8 +243,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { // do not emit assets for SSR build if ( config.command === 'build' && - config.build.ssr && - !config.build.ssrEmitAssets + !this.environment.options.build.emitAssets ) { for (const file in bundle) { if ( @@ -267,14 +260,14 @@ export function assetPlugin(config: ResolvedConfig): Plugin { } export async function fileToUrl( - id: string, - config: ResolvedConfig, ctx: PluginContext, + id: string, ): Promise { - if (config.command === 'serve') { - return fileToDevUrl(id, config) + const environment = ctx.environment! + if (environment.config.command === 'serve') { + return fileToDevUrl(id, environment.config) } else { - return fileToBuiltUrl(id, config, ctx) + return fileToBuiltUrl(ctx, id) } } @@ -342,17 +335,18 @@ function isGitLfsPlaceholder(content: Buffer): boolean { * and returns the resolved public URL */ async function fileToBuiltUrl( - id: string, - config: ResolvedConfig, pluginContext: PluginContext, + id: string, skipPublicCheck = false, forceInline?: boolean, ): Promise { + const environment = pluginContext.environment! + const { config } = environment if (!skipPublicCheck && checkPublicFile(id, config)) { return publicFileToBuiltUrl(id, config) } - const cache = assetCache.get(config)! + const cache = assetCache.get(environment)! const cached = cache.get(id) if (cached) { return cached @@ -362,7 +356,7 @@ async function fileToBuiltUrl( const content = await fsp.readFile(file) let url: string - if (shouldInline(config, file, id, content, pluginContext, forceInline)) { + if (shouldInline(pluginContext, file, id, content, forceInline)) { if (config.build.lib && isGitLfsPlaceholder(content)) { config.logger.warn( colors.yellow(`Inlined file ${id} was not downloaded via Git LFS`), @@ -389,7 +383,7 @@ async function fileToBuiltUrl( }) const originalName = normalizePath(path.relative(config.root, file)) - generatedAssets.get(config)!.set(referenceId, { originalName }) + generatedAssetsMap.get(environment)!.set(referenceId, { originalName }) url = `__VITE_ASSET__${referenceId}__${postfix ? `$_${postfix}__` : ``}` // TODO_BASE } @@ -399,12 +393,13 @@ async function fileToBuiltUrl( } export async function urlToBuiltUrl( + pluginContext: PluginContext, url: string, importer: string, - config: ResolvedConfig, - pluginContext: PluginContext, forceInline?: boolean, ): Promise { + const environment = pluginContext.environment! + const { config } = environment if (checkPublicFile(url, config)) { return publicFileToBuiltUrl(url, config) } @@ -413,9 +408,8 @@ export async function urlToBuiltUrl( ? path.join(config.root, url) : path.join(path.dirname(importer), url) return fileToBuiltUrl( - file, - config, pluginContext, + file, // skip public check since we just did it above true, forceInline, @@ -423,23 +417,25 @@ export async function urlToBuiltUrl( } const shouldInline = ( - config: ResolvedConfig, + pluginContext: PluginContext, file: string, id: string, content: Buffer, - pluginContext: PluginContext, forceInline: boolean | undefined, ): boolean => { + const environment = pluginContext.environment! + const { config } = environment + const { assetsInlineLimit } = environment.options.build if (config.build.lib) return true if (pluginContext.getModuleInfo(id)?.isEntry) return false if (forceInline !== undefined) return forceInline let limit: number - if (typeof config.build.assetsInlineLimit === 'function') { - const userShouldInline = config.build.assetsInlineLimit(file, content) + if (typeof assetsInlineLimit === 'function') { + const userShouldInline = assetsInlineLimit(file, content) if (userShouldInline != null) return userShouldInline limit = DEFAULT_ASSETS_INLINE_LIMIT } else { - limit = Number(config.build.assetsInlineLimit) + limit = Number(assetsInlineLimit) } if (file.endsWith('.html')) return false // Don't inline SVG with fragments, as they are meant to be reused diff --git a/packages/vite/src/node/plugins/assetImportMetaUrl.ts b/packages/vite/src/node/plugins/assetImportMetaUrl.ts index 588f6e08b07c3d..75d1958e184f3d 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -3,10 +3,11 @@ import MagicString from 'magic-string' import { stripLiteral } from 'strip-literal' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' -import type { ResolveFn } from '../' import { injectQuery, isParentDirectory, transformStableResult } from '../utils' import { CLIENT_ENTRY } from '../constants' import { slash } from '../../shared/utils' +import { createIdResolver } from '../idResolver' +import type { ResolveIdFn } from '../idResolver' import { fileToUrl } from './asset' import { preloadHelperId } from './importAnalysisBuild' import type { InternalResolveOptions } from './resolve' @@ -25,7 +26,7 @@ import { hasViteIgnoreRE } from './importAnalysis' */ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { const { publicDir } = config - let assetResolver: ResolveFn + let assetResolver: ResolveIdFn const fsResolveOptions: InternalResolveOptions = { ...config.resolve, @@ -33,15 +34,17 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { isProduction: config.isProduction, isBuild: config.command === 'build', packageCache: config.packageCache, - ssrConfig: config.ssr, asSrc: true, } return { name: 'vite:asset-import-meta-url', async transform(code, id, options) { + const { environment } = this if ( - !options?.ssr && + environment && + // TODO: Should this be done only for the client or for any webCompatible environment? + environment.name === 'client' && id !== preloadHelperId && id !== CLIENT_ENTRY && code.includes('new URL') && @@ -106,13 +109,13 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { file = slash(path.resolve(path.dirname(id), url)) file = tryFsResolve(file, fsResolveOptions) ?? file } else { - assetResolver ??= config.createResolver({ + assetResolver ??= createIdResolver(config, { extensions: [], mainFields: [], tryIndex: false, preferRelative: true, }) - file = await assetResolver(url, id) + file = await assetResolver(environment, url, id) file ??= url[0] === '/' ? slash(path.join(publicDir, url)) @@ -126,9 +129,9 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { try { if (publicDir && isParentDirectory(publicDir, file)) { const publicPath = '/' + path.posix.relative(publicDir, file) - builtUrl = await fileToUrl(publicPath, config, this) + builtUrl = await fileToUrl(this, publicPath) } else { - builtUrl = await fileToUrl(file, config, this) + builtUrl = await fileToUrl(this, file) } } catch { // do nothing, we'll log a warning after this diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index c66f3877eca822..1572c58fa065df 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -90,25 +90,23 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { } }, async transform(code, id, options) { + // TODO: !environment.options.nodeCompatible ? + // TODO: Remove options?.ssr, Vitest currently hijacks this plugin + const ssr = options?.ssr ?? this.environment.name !== 'client' if (id === normalizedClientEntry || id === normalizedEnvEntry) { return injectConfigValues(code) - } else if (!options?.ssr && code.includes('process.env.NODE_ENV')) { + } else if (!ssr && code.includes('process.env.NODE_ENV')) { // replace process.env.NODE_ENV instead of defining a global // for it to avoid shimming a `process` object during dev, // avoiding inconsistencies between dev and build const nodeEnv = config.define?.['process.env.NODE_ENV'] || JSON.stringify(process.env.NODE_ENV || config.mode) - return await replaceDefine( - code, - id, - { - 'process.env.NODE_ENV': nodeEnv, - 'global.process.env.NODE_ENV': nodeEnv, - 'globalThis.process.env.NODE_ENV': nodeEnv, - }, - config, - ) + return await replaceDefine(this.environment, code, id, { + 'process.env.NODE_ENV': nodeEnv, + 'global.process.env.NODE_ENV': nodeEnv, + 'globalThis.process.env.NODE_ENV': nodeEnv, + }) } }, } diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 8fbea570e1ad24..86cea710fa5223 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -26,8 +26,7 @@ import { formatMessages, transform } from 'esbuild' import type { RawSourceMap } from '@ampproject/remapping' import { WorkerWithFallback } from 'artichokie' import { getCodeWithSourcemap, injectSourcesContent } from '../server/sourcemap' -import type { ModuleNode } from '../server/moduleGraph' -import type { ResolveFn, ViteDevServer } from '../' +import type { EnvironmentModuleNode } from '../server/moduleGraph' import { createToImportMetaURLBasedRelativeRuntime, resolveUserExternal, @@ -69,12 +68,15 @@ import { } from '../utils' import type { Logger } from '../logger' import { cleanUrl, slash } from '../../shared/utils' +import { createIdResolver } from '../idResolver' +import type { ResolveIdFn } from '../idResolver' +import { PartialEnvironment } from '../baseEnvironment' import type { TransformPluginContext } from '../server/pluginContainer' import { addToHTMLProxyTransformResult } from './html' import { assetUrlRE, fileToUrl, - generatedAssets, + generatedAssetsMap, publicAssetUrlCache, publicAssetUrlRE, publicFileToBuiltUrl, @@ -124,6 +126,7 @@ export interface CSSOptions { * Enables css sourcemaps during dev * @default false * @experimental + * @deprecated use dev.sourcemap instead */ devSourcemap?: boolean @@ -256,7 +259,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { const isBuild = config.command === 'build' let moduleCache: Map> - const resolveUrl = config.createResolver({ + const idResolver = createIdResolver(config, { preferRelative: true, tryIndex: false, extensions: [], @@ -319,13 +322,18 @@ export function cssPlugin(config: ResolvedConfig): Plugin { }, async transform(raw, id) { + const { environment } = this if ( + !environment || !isCSSRequest(id) || commonjsProxyRE.test(id) || SPECIAL_QUERY_RE.test(id) ) { return } + const resolveUrl = (url: string, importer?: string) => + idResolver(environment, url, importer) + const urlReplacer: CssUrlReplacer = async (url, importer) => { const decodedUrl = decodeURI(url) if (checkPublicFile(decodedUrl, config)) { @@ -337,7 +345,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { } const resolved = await resolveUrl(decodedUrl, importer) if (resolved) { - return fileToUrl(resolved, config, this) + return fileToUrl(this, resolved) } if (config.command === 'build') { const isExternal = config.build.rollupOptions.external @@ -365,9 +373,9 @@ export function cssPlugin(config: ResolvedConfig): Plugin { deps, map, } = await compileCSS( + environment, id, raw, - config, preprocessorWorkerController!, urlReplacer, ) @@ -496,7 +504,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { if (isDirectCSSRequest(id)) { return null } - // server only + // server only, TODO: environment if (options?.ssr) { return modulesCode || `export default ${JSON.stringify(css)}` } @@ -550,6 +558,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { }, async renderChunk(code, chunk, opts) { + const generatedAssets = generatedAssetsMap.get(this.environment)! + let chunkCSS = '' // the chunk is empty if it's a dynamic entry chunk that only contains a CSS import const isJsChunkEmpty = code === '' && !chunk.isEntry @@ -706,9 +716,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { type: 'asset', source: content, }) - generatedAssets - .get(config)! - .set(referenceId, { originalName: originalFilename }) + generatedAssets.set(referenceId, { originalName: originalFilename }) const replacement = toOutputFilePathInJS( this.getFileName(referenceId), @@ -763,9 +771,10 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { type: 'asset', source: chunkCSS, }) - generatedAssets - .get(config)! - .set(referenceId, { originalName: originalFilename, isEntry }) + generatedAssets.set(referenceId, { + originalName: originalFilename, + isEntry, + }) chunk.viteMetadata!.importedCss.add(this.getFileName(referenceId)) } else if (!config.build.ssr) { // legacy build and inline css @@ -779,13 +788,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { chunkCSS = await finalizeCss(chunkCSS, true, config) let cssString = JSON.stringify(chunkCSS) cssString = - renderAssetUrlInJS( - this, - config, - chunk, - opts, - cssString, - )?.toString() || cssString + renderAssetUrlInJS(this, chunk, opts, cssString)?.toString() || + cssString const style = `__vite_style__` const injectCode = `var ${style} = document.createElement('style');` + @@ -940,15 +944,9 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { - let server: ViteDevServer - return { name: 'vite:css-analysis', - configureServer(_server) { - server = _server - }, - async transform(_, id, options) { if ( !isCSSRequest(id) || @@ -958,9 +956,10 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { return } - const ssr = options?.ssr === true - const { moduleGraph } = server - const thisModule = moduleGraph.getModuleById(id) + const environment = this.environment + const moduleGraph = + environment?.mode === 'dev' ? environment.moduleGraph : undefined + const thisModule = moduleGraph?.getModuleById(id) // Handle CSS @import dependency HMR and other added modules via this.addWatchFile. // JS-related HMR is handled in the import-analysis plugin. @@ -976,22 +975,21 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { if (pluginImports) { // record deps in the module graph so edits to @import css can trigger // main import to hot update - const depModules = new Set() + const depModules = new Set() const devBase = config.base for (const file of pluginImports) { depModules.add( isCSSRequest(file) - ? moduleGraph.createFileOnlyEntry(file) - : await moduleGraph.ensureEntryFromUrl( + ? moduleGraph!.createFileOnlyEntry(file) + : await moduleGraph!.ensureEntryFromUrl( stripBase( - await fileToUrl(file, config, this), + await fileToUrl(this, file), (config.server?.origin ?? '') + devBase, ), - ssr, ), ) } - moduleGraph.updateModuleInfo( + moduleGraph!.updateModuleInfo( thisModule, depModules, null, @@ -1000,7 +998,6 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { new Set(), null, isSelfAccepting, - ssr, ) } else { thisModule.isSelfAccepting = isSelfAccepting @@ -1046,54 +1043,45 @@ export function getEmptyChunkReplacer( } interface CSSAtImportResolvers { - css: ResolveFn - sass: ResolveFn - less: ResolveFn + css: ResolveIdFn + sass: ResolveIdFn + less: ResolveIdFn } function createCSSResolvers(config: ResolvedConfig): CSSAtImportResolvers { - let cssResolve: ResolveFn | undefined - let sassResolve: ResolveFn | undefined - let lessResolve: ResolveFn | undefined + let cssResolve: ResolveIdFn | undefined + let sassResolve: ResolveIdFn | undefined + let lessResolve: ResolveIdFn | undefined return { get css() { - return ( - cssResolve || - (cssResolve = config.createResolver({ - extensions: ['.css'], - mainFields: ['style'], - conditions: ['style'], - tryIndex: false, - preferRelative: true, - })) - ) + return (cssResolve ??= createIdResolver(config, { + extensions: ['.css'], + mainFields: ['style'], + conditions: ['style'], + tryIndex: false, + preferRelative: true, + })) }, get sass() { - return ( - sassResolve || - (sassResolve = config.createResolver({ - extensions: ['.scss', '.sass', '.css'], - mainFields: ['sass', 'style'], - conditions: ['sass', 'style'], - tryIndex: true, - tryPrefix: '_', - preferRelative: true, - })) - ) + return (sassResolve ??= createIdResolver(config, { + extensions: ['.scss', '.sass', '.css'], + mainFields: ['sass', 'style'], + conditions: ['sass', 'style'], + tryIndex: true, + tryPrefix: '_', + preferRelative: true, + })) }, get less() { - return ( - lessResolve || - (lessResolve = config.createResolver({ - extensions: ['.less', '.css'], - mainFields: ['less', 'style'], - conditions: ['less', 'style'], - tryIndex: false, - preferRelative: true, - })) - ) + return (lessResolve ??= createIdResolver(config, { + extensions: ['.less', '.css'], + mainFields: ['less', 'style'], + conditions: ['less', 'style'], + tryIndex: false, + preferRelative: true, + })) }, } } @@ -1105,12 +1093,13 @@ function getCssResolversKeys( } async function compileCSSPreprocessors( + environment: PartialEnvironment, id: string, lang: PreprocessLang, code: string, - config: ResolvedConfig, workerController: PreprocessorWorkerController, ): Promise<{ code: string; map?: ExistingRawSourceMap; deps?: Set }> { + const { config } = environment const { preprocessorOptions, devSourcemap } = config.css ?? {} const atImportResolvers = getAtImportResolvers(config) @@ -1140,6 +1129,7 @@ async function compileCSSPreprocessors( opts.enableSourcemap = devSourcemap ?? false const preprocessResult = await preProcessor( + environment, code, config.root, opts, @@ -1185,9 +1175,9 @@ function getAtImportResolvers(config: ResolvedConfig) { } async function compileCSS( + environment: PartialEnvironment, id: string, code: string, - config: ResolvedConfig, workerController: PreprocessorWorkerController, urlReplacer?: CssUrlReplacer, ): Promise<{ @@ -1197,8 +1187,9 @@ async function compileCSS( modules?: Record deps?: Set }> { + const { config } = environment if (config.css?.transformer === 'lightningcss') { - return compileLightningCSS(id, code, config, urlReplacer) + return compileLightningCSS(id, code, environment, urlReplacer) } const { modules: modulesOptions, devSourcemap } = config.css || {} @@ -1228,10 +1219,10 @@ async function compileCSS( let preprocessorMap: ExistingRawSourceMap | undefined if (isPreProcessor(lang)) { const preprocessorResult = await compileCSSPreprocessors( + environment, id, lang, code, - config, workerController, ) code = preprocessorResult.code @@ -1256,6 +1247,7 @@ async function compileCSS( } const resolved = await atImportResolvers.css( + environment, id, path.join(basedir, '*'), ) @@ -1282,10 +1274,10 @@ async function compileCSS( const lang = id.match(CSS_LANGS_RE)?.[1] as CssLang | undefined if (isPreProcessor(lang)) { const result = await compileCSSPreprocessors( + environment, id, lang, code, - config, workerController, ) result.deps?.forEach((dep) => deps.add(dep)) @@ -1327,7 +1319,11 @@ async function compileCSS( }, async resolve(id: string, importer: string) { for (const key of getCssResolversKeys(atImportResolvers)) { - const resolved = await atImportResolvers[key](id, importer) + const resolved = await atImportResolvers[key]( + environment, + id, + importer, + ) if (resolved) { return path.resolve(resolved) } @@ -1485,6 +1481,10 @@ export async function preprocessCSS( code: string, filename: string, config: ResolvedConfig, + // Backward compatibility, only the name is needed for the alias and resolve plugins used in the resolvers + // TODO: Should we use environmentName instead of environment for these APIs? + // Should the signature be preprocessCSS(code, filename, environment) or preprocessCSS(code, filename, config, environmentName)? + environment: PartialEnvironment = new PartialEnvironment('client', config), ): Promise { let workerController = preprocessorWorkerControllerCache.get(config) @@ -1496,7 +1496,7 @@ export async function preprocessCSS( workerController = alwaysFakeWorkerWorkerControllerCache } - return await compileCSS(filename, code, config, workerController) + return await compileCSS(environment, filename, code, workerController) } export async function formatPostcssSourceMap( @@ -1937,6 +1937,7 @@ type StylusStylePreprocessorOptions = StylePreprocessorOptions & { type StylePreprocessor = { process: ( + environment: PartialEnvironment, source: string, root: string, options: StylePreprocessorOptions, @@ -1947,6 +1948,7 @@ type StylePreprocessor = { type SassStylePreprocessor = { process: ( + environment: PartialEnvironment, source: string, root: string, options: SassStylePreprocessorOptions, @@ -1957,6 +1959,7 @@ type SassStylePreprocessor = { type StylusStylePreprocessor = { process: ( + environment: PartialEnvironment, source: string, root: string, options: StylusStylePreprocessorOptions, @@ -2054,6 +2057,7 @@ function fixScssBugImportValue( // #region Sass // .scss/.sass processor const makeScssWorker = ( + environment: PartialEnvironment, resolvers: CSSAtImportResolvers, alias: Alias[], maxWorkers: number | undefined, @@ -2064,10 +2068,11 @@ const makeScssWorker = ( filename: string, ) => { importer = cleanScssBugUrl(importer) - const resolved = await resolvers.sass(url, importer) + const resolved = await resolvers.sass(environment, url, importer) if (resolved) { try { const data = await rebaseUrls( + environment, resolved, filename, alias, @@ -2174,13 +2179,13 @@ const scssProcessor = ( worker.stop() } }, - async process(source, root, options, resolvers) { + async process(environment, source, root, options, resolvers) { const sassPath = loadPreprocessorPath(PreprocessLang.sass, root) if (!workerMap.has(options.alias)) { workerMap.set( options.alias, - makeScssWorker(resolvers, options.alias, maxWorkers), + makeScssWorker(environment, resolvers, options.alias, maxWorkers), ) } const worker = workerMap.get(options.alias)! @@ -2230,11 +2235,12 @@ const scssProcessor = ( * root file as base. */ async function rebaseUrls( + environment: PartialEnvironment, file: string, rootFile: string, alias: Alias[], variablePrefix: string, - resolver: ResolveFn, + resolver: ResolveIdFn, ): Promise { file = path.resolve(file) // ensure os-specific flashes // in the same dir, no need to rebase @@ -2269,7 +2275,8 @@ async function rebaseUrls( return url } } - const absolute = (await resolver(url, file)) || path.resolve(fileDir, url) + const absolute = + (await resolver(environment, url, file)) || path.resolve(fileDir, url) const relative = path.relative(rootDir, absolute) return normalizePath(relative) } @@ -2296,6 +2303,7 @@ async function rebaseUrls( // #region Less // .less const makeLessWorker = ( + environment: PartialEnvironment, resolvers: CSSAtImportResolvers, alias: Alias[], maxWorkers: number | undefined, @@ -2305,10 +2313,15 @@ const makeLessWorker = ( dir: string, rootFile: string, ) => { - const resolved = await resolvers.less(filename, path.join(dir, '*')) + const resolved = await resolvers.less( + environment, + filename, + path.join(dir, '*'), + ) if (!resolved) return undefined const result = await rebaseUrls( + environment, resolved, rootFile, alias, @@ -2426,13 +2439,13 @@ const lessProcessor = (maxWorkers: number | undefined): StylePreprocessor => { worker.stop() } }, - async process(source, root, options, resolvers) { + async process(environment, source, root, options, resolvers) { const lessPath = loadPreprocessorPath(PreprocessLang.less, root) if (!workerMap.has(options.alias)) { workerMap.set( options.alias, - makeLessWorker(resolvers, options.alias, maxWorkers), + makeLessWorker(environment, resolvers, options.alias, maxWorkers), ) } const worker = workerMap.get(options.alias)! @@ -2548,7 +2561,7 @@ const stylProcessor = ( worker.stop() } }, - async process(source, root, options, resolvers) { + async process(environment, source, root, options, resolvers) { const stylusPath = loadPreprocessorPath(PreprocessLang.stylus, root) if (!workerMap.has(options.alias)) { @@ -2657,12 +2670,14 @@ const createPreprocessorWorkerController = (maxWorkers: number | undefined) => { const styl = stylProcessor(maxWorkers) const sassProcess: StylePreprocessor['process'] = ( + environment, source, root, options, resolvers, ) => { return scss.process( + environment, source, root, { ...options, indentedSyntax: true }, @@ -2712,9 +2727,10 @@ const importLightningCSS = createCachedImport(() => import('lightningcss')) async function compileLightningCSS( id: string, src: string, - config: ResolvedConfig, + environment: PartialEnvironment, urlReplacer?: CssUrlReplacer, ): ReturnType { + const { config } = environment const deps = new Set() // Relative path is needed to get stable hash when using CSS modules const filename = cleanUrl(path.relative(config.root, id)) @@ -2752,6 +2768,7 @@ async function compileLightningCSS( } const resolved = await getAtImportResolvers(config).css( + environment, id, toAbsolute(from), ) diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 786a1038505c00..983e6dfb4fa241 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -3,6 +3,7 @@ import { TraceMap, decodedMap, encodedMap } from '@jridgewell/trace-mapping' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' import { escapeRegex, getHash } from '../utils' +import type { Environment } from '../environment' import { isCSSRequest } from './css' import { isHTMLRequest } from './html' @@ -54,8 +55,13 @@ export function definePlugin(config: ResolvedConfig): Plugin { } } - function generatePattern(ssr: boolean) { - const replaceProcessEnv = !ssr || config.ssr?.target === 'webworker' + function generatePattern(environment: Environment) { + // This is equivalent to the old `!ssr || config.ssr?.target === 'webworker'` + // TODO: We shouldn't keep options.nodeCompatible and options.webCompatible + // This is a place where using `!options.nodeCompatible` fails and it is confusing why + // Do we need a per-environment replaceProcessEnv option? + // Is it useful to have define be configured per-environment? + const replaceProcessEnv = environment.options.webCompatible const define: Record = { ...(replaceProcessEnv ? processEnv : {}), @@ -65,6 +71,11 @@ export function definePlugin(config: ResolvedConfig): Plugin { } // Additional define fixes based on `ssr` value + // Backward compatibility. Any non client environment will get import.meta.env.SSR = true + // TODO: Check if we should only do this for the SSR environment and how to abstract + // maybe we need import.meta.env.environmentName ? + const ssr = environment.name !== 'client' + if ('import.meta.env.SSR' in define) { define['import.meta.env.SSR'] = ssr + '' } @@ -91,15 +102,24 @@ export function definePlugin(config: ResolvedConfig): Plugin { return [define, pattern] as const } - const defaultPattern = generatePattern(false) - const ssrPattern = generatePattern(true) + const patternsCache = new WeakMap< + Environment, + readonly [Record, RegExp | null] + >() + function getPattern(environment: Environment) { + let pattern = patternsCache.get(environment) + if (!pattern) { + pattern = generatePattern(environment) + patternsCache.set(environment, pattern) + } + return pattern + } return { name: 'vite:define', - async transform(code, id, options) { - const ssr = options?.ssr === true - if (!ssr && !isBuild) { + async transform(code, id) { + if (this.environment.name === 'client' && !isBuild) { // for dev we inject actual global defines in the vite client to // avoid the transform cost. see the `clientInjection` and // `importAnalysis` plugin. @@ -116,23 +136,23 @@ export function definePlugin(config: ResolvedConfig): Plugin { return } - const [define, pattern] = ssr ? ssrPattern : defaultPattern + const [define, pattern] = getPattern(this.environment) if (!pattern) return // Check if our code needs any replacements before running esbuild pattern.lastIndex = 0 if (!pattern.test(code)) return - return await replaceDefine(code, id, define, config) + return await replaceDefine(this.environment, code, id, define) }, } } export async function replaceDefine( + environment: Environment, code: string, id: string, define: Record, - config: ResolvedConfig, ): Promise<{ code: string; map: string | null }> { // Because esbuild only allows JSON-serializable values, and `import.meta.env` // may contain values with raw identifiers, making it non-JSON-serializable, @@ -147,7 +167,7 @@ export async function replaceDefine( define = { ...define, 'import.meta.env': marker } } - const esbuildOptions = config.esbuild || {} + const esbuildOptions = environment.config.esbuild || {} const result = await transform(code, { loader: 'js', @@ -155,7 +175,10 @@ export async function replaceDefine( platform: 'neutral', define, sourcefile: id, - sourcemap: config.command === 'build' ? !!config.build.sourcemap : true, + sourcemap: + environment.config.command === 'build' + ? !!environment.options.build.sourcemap + : true, }) // remove esbuild's source entries diff --git a/packages/vite/src/node/plugins/dynamicImportVars.ts b/packages/vite/src/node/plugins/dynamicImportVars.ts index 8c55632a78f234..a9a49c94d23b6a 100644 --- a/packages/vite/src/node/plugins/dynamicImportVars.ts +++ b/packages/vite/src/node/plugins/dynamicImportVars.ts @@ -7,6 +7,7 @@ import { dynamicImportToGlob } from '@rollup/plugin-dynamic-import-vars' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' import { CLIENT_ENTRY } from '../constants' +import { createIdResolver } from '../idResolver' import { createFilter, normalizePath, @@ -16,6 +17,8 @@ import { transformStableResult, urlRE, } from '../utils' +import type { Environment } from '../environment' +import { usePerEnvironmentState } from '../environment' import { toAbsoluteGlob } from './importMetaGlob' import { hasViteIgnoreRE } from './importAnalysis' import { workerOrSharedWorkerRE } from './worker' @@ -161,14 +164,17 @@ export async function transformDynamicImport( } export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { - const resolve = config.createResolver({ + const resolve = createIdResolver(config, { preferRelative: true, tryIndex: false, extensions: [], }) - const { include, exclude, warnOnError } = - config.build.dynamicImportVarsOptions - const filter = createFilter(include, exclude) + + const getFilter = usePerEnvironmentState((environment: Environment) => { + const { include, exclude } = + environment.options.build.dynamicImportVarsOptions + return createFilter(include, exclude) + }) return { name: 'vite:dynamic-import-vars', @@ -186,8 +192,10 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { }, async transform(source, importer) { + const { environment } = this if ( - !filter(importer) || + !environment || + !getFilter(this)(importer) || importer === CLIENT_ENTRY || !hasDynamicImportRE.test(source) ) { @@ -234,11 +242,11 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { result = await transformDynamicImport( source.slice(start, end), importer, - resolve, + (id, importer) => resolve(environment, id, importer), config.root, ) } catch (error) { - if (warnOnError) { + if (environment.options.build.dynamicImportVarsOptions.warnOnError) { this.warn(error) } else { this.error(error) diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index 54f1796afd6381..219a1f624a5df8 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -7,6 +7,7 @@ import type { TransformResult, } from 'esbuild' import { transform } from 'esbuild' +// TODO: import type { FSWatcher } from 'chokidar' import type { RawSourceMap } from '@ampproject/remapping' import type { InternalModuleFormat, SourceMap } from 'rollup' import type { TSConfckParseResult } from 'tsconfck' @@ -41,8 +42,6 @@ export const defaultEsbuildSupported = { 'import-meta': true, } -let server: ViteDevServer - export interface ESBuildOptions extends TransformOptions { include?: string | RegExp | string[] | RegExp[] exclude?: string | RegExp | string[] | RegExp[] @@ -81,6 +80,8 @@ export async function transformWithEsbuild( filename: string, options?: TransformOptions, inMap?: object, + root?: string, + watcher?: any, // TODO: module-runner bundling issue with FSWatcher, ): Promise { let loader = options?.loader @@ -121,7 +122,11 @@ export async function transformWithEsbuild( ] const compilerOptionsForFile: TSCompilerOptions = {} if (loader === 'ts' || loader === 'tsx') { - const loadedTsconfig = await loadTsconfigJsonForFile(filename) + const loadedTsconfig = await loadTsconfigJsonForFile( + filename, + root, + watcher, + ) const loadedCompilerOptions = loadedTsconfig.compilerOptions ?? {} for (const field of meaningfulFields) { @@ -251,17 +256,6 @@ export function esbuildPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:esbuild', - configureServer(_server) { - server = _server - server.watcher - .on('add', reloadOnTsconfigChange) - .on('change', reloadOnTsconfigChange) - .on('unlink', reloadOnTsconfigChange) - }, - buildEnd() { - // recycle serve to avoid preventing Node self-exit (#6815) - server = null as any - }, async transform(code, id) { if (filter(id) || filter(cleanUrl(id))) { const result = await transformWithEsbuild(code, id, transformOptions) @@ -450,6 +444,8 @@ let tsconfckCache: TSConfckCache | undefined export async function loadTsconfigJsonForFile( filename: string, + root?: string, + watcher?: any, // TODO: module-runner issue with FSWatcher, ): Promise { try { if (!tsconfckCache) { @@ -460,30 +456,35 @@ export async function loadTsconfigJsonForFile( ignoreNodeModules: true, }) // tsconfig could be out of root, make sure it is watched on dev - if (server && result.tsconfigFile) { - ensureWatchedFile(server.watcher, result.tsconfigFile, server.config.root) + if (root && watcher && result.tsconfigFile) { + ensureWatchedFile(watcher, result.tsconfigFile, root) } return result.tsconfig } catch (e) { if (e instanceof TSConfckParseError) { // tsconfig could be out of root, make sure it is watched on dev - if (server && e.tsconfigFile) { - ensureWatchedFile(server.watcher, e.tsconfigFile, server.config.root) + if (root && watcher && e.tsconfigFile) { + ensureWatchedFile(watcher, e.tsconfigFile, root) } } throw e } } -async function reloadOnTsconfigChange(changedFile: string) { - // server could be closed externally after a file change is detected - if (!server) return +export async function reloadOnTsconfigChange( + server: ViteDevServer, + changedFile: string, +): Promise { // any tsconfig.json that's added in the workspace could be closer to a code file than a previously cached one // any json file in the tsconfig cache could have been used to compile ts if ( path.basename(changedFile) === 'tsconfig.json' || - (changedFile.endsWith('.json') && - tsconfckCache?.hasParseResult(changedFile)) + changedFile.endsWith('.json') /* + TODO: the tsconfckCache?.clear() line will make this fail if there are several servers + we may need a cache per server if we don't want all servers to share the reset + leaving it commented for now because it should still work + && tsconfckCache?.hasParseResult(changedFile) + */ ) { server.config.logger.info( `changed tsconfig file detected: ${changedFile} - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.`, @@ -491,18 +492,17 @@ async function reloadOnTsconfigChange(changedFile: string) { ) // clear module graph to remove code compiled with outdated config - server.moduleGraph.invalidateAll() + for (const environment of Object.values(server.environments)) { + environment.moduleGraph.invalidateAll() + } // reset tsconfck so that recompile works with up2date configs tsconfckCache?.clear() - // server may not be available if vite config is updated at the same time - if (server) { - // force full reload - server.hot.send({ - type: 'full-reload', - path: '*', - }) - } + // force full reload + server.ws.send({ + type: 'full-reload', + path: '*', + }) } } diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 04fd6268e95a35..277428f5f4868e 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -329,6 +329,8 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { async transform(html, id) { if (id.endsWith('.html')) { + const { modulePreload } = this.environment.options.build + id = normalizePath(id) const relativeUrlPath = path.posix.relative(config.root, id) const publicPath = `/${relativeUrlPath}` @@ -412,7 +414,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { !namedOutput.includes(removeLeadingSlash(url)) // Allow for absolute references as named output can't be an absolute path ) { try { - return await urlToBuiltUrl(url, id, config, this, shouldInline) + return await urlToBuiltUrl(this, url, id, shouldInline) } catch (e) { if (e.code !== 'ENOENT') { throw e @@ -647,7 +649,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { s.update( start, end, - partialEncodeURIPath(await urlToBuiltUrl(url, id, config, this)), + partialEncodeURIPath(await urlToBuiltUrl(this, url, id)), ) } } @@ -674,7 +676,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { processedHtml.set(id, s.toString()) // inject module preload polyfill only when configured and needed - const { modulePreload } = config.build if ( modulePreload !== false && modulePreload.polyfill && @@ -690,6 +691,8 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }, async generateBundle(options, bundle) { + const { modulePreload } = this.environment.options.build + const analyzedChunk: Map = new Map() const inlineEntryChunk = new Set() const getImportedChunks = ( @@ -836,7 +839,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { ) } else { assetTags = [toScriptTag(chunk, toOutputAssetFilePath, isAsync)] - const { modulePreload } = config.build if (modulePreload !== false) { const resolveDependencies = typeof modulePreload === 'object' && @@ -861,7 +863,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { } // inject css link when cssCodeSplit is false - if (!config.build.cssCodeSplit) { + if (this.environment?.options.build.cssCodeSplit === false) { const cssChunk = Object.values(bundle).find( (chunk) => chunk.type === 'asset' && chunk.name === 'style.css', ) as OutputAsset | undefined diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 754a800f0b55b2..adc7ea91d85c61 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -12,7 +12,6 @@ import { parseAst } from 'rollup/parseAst' import type { StaticImport } from 'mlly' import { ESM_STATIC_IMPORT_RE, parseStaticImport } from 'mlly' import { makeLegalIdentifier } from '@rollup/pluginutils' -import type { ViteDevServer } from '..' import { CLIENT_DIR, CLIENT_PUBLIC_PATH, @@ -52,11 +51,12 @@ import { } from '../utils' import { getFsUtils } from '../fsUtils' import { checkPublicFile } from '../publicDir' -import { getDepOptimizationConfig } from '../config' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' -import { shouldExternalizeForSSR } from '../ssr/ssrExternal' -import { getDepsOptimizer, optimizedDepNeedsInterop } from '../optimizer' +import type { DevEnvironment } from '../server/environment' +import { addSafeModulePath } from '../server/middlewares/static' +import { shouldExternalize } from '../external' +import { optimizedDepNeedsInterop } from '../optimizer' import { cleanUrl, unwrapId, @@ -151,7 +151,7 @@ function extractImportedBindings( } /** - * Server-only plugin that lexes, resolves, rewrites and analyzes url imports. + * Dev-only plugin that lexes, resolves, rewrites and analyzes url imports. * * - Imports are resolved to ensure they exist on disk * @@ -185,7 +185,6 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const clientPublicPath = path.posix.join(base, CLIENT_PUBLIC_PATH) const enablePartialAccept = config.experimental?.hmrPartialAccept const matchAlias = getAliasPatternMatcher(config.resolve.alias) - let server: ViteDevServer let _env: string | undefined let _ssrEnv: string | undefined @@ -216,18 +215,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:import-analysis', - configureServer(_server) { - server = _server - }, - - async transform(source, importer, options) { - // In a real app `server` is always defined, but it is undefined when - // running src/node/server/__tests__/pluginContainer.spec.ts - if (!server) { - return null - } - - const ssr = options?.ssr === true + async transform(source, importer) { + const environment = this.environment as DevEnvironment + const ssr = environment.name !== 'client' // TODO + const moduleGraph = environment.moduleGraph if (canSkipImportAnalysis(importer)) { debug?.(colors.dim(`[skipped] ${prettifyUrl(importer, root)}`)) @@ -250,12 +241,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { this.error(message, showCodeFrame ? e.idx : undefined) } - const depsOptimizer = getDepsOptimizer(config, ssr) + const depsOptimizer = environment.depsOptimizer - const { moduleGraph } = server // since we are already in the transform phase of the importer, it must // have been loaded so its entry is guaranteed in the module graph. - const importerModule = moduleGraph.getModuleById(importer)! + const importerModule = moduleGraph.getModuleById(importer) if (!importerModule) { // This request is no longer valid. It could happen for optimized deps // requests. A full reload is going to request this id again. @@ -299,20 +289,20 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { let importerFile = importer - const optimizeDeps = getDepOptimizationConfig(config, ssr) - if (moduleListContains(optimizeDeps?.exclude, url)) { - if (depsOptimizer) { - await depsOptimizer.scanProcessing - - // if the dependency encountered in the optimized file was excluded from the optimization - // the dependency needs to be resolved starting from the original source location of the optimized file - // because starting from node_modules/.vite will not find the dependency if it was not hoisted - // (that is, if it is under node_modules directory in the package source of the optimized file) - for (const optimizedModule of depsOptimizer.metadata.depInfoList) { - if (!optimizedModule.src) continue // Ignore chunks - if (optimizedModule.file === importerModule.file) { - importerFile = optimizedModule.src - } + if ( + depsOptimizer && + moduleListContains(depsOptimizer.options.exclude, url) + ) { + await depsOptimizer.scanProcessing + + // if the dependency encountered in the optimized file was excluded from the optimization + // the dependency needs to be resolved starting from the original source location of the optimized file + // because starting from node_modules/.vite will not find the dependency if it was not hoisted + // (that is, if it is under node_modules directory in the package source of the optimized file) + for (const optimizedModule of depsOptimizer.metadata.depInfoList) { + if (!optimizedModule.src) continue // Ignore chunks + if (optimizedModule.file === importerModule.file) { + importerFile = optimizedModule.src } } } @@ -370,8 +360,8 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { url = wrapId(resolved.id) } - // make the URL browser-valid if not SSR - if (!ssr) { + // make the URL browser-valid + if (environment.options.injectInvalidationTimestamp) { // mark non-js/css imports with `?import` if (isExplicitImportRequired(url)) { url = injectQuery(url, 'import') @@ -398,7 +388,6 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // We use an internal function to avoid resolving the url again const depModule = await moduleGraph._ensureEntryFromUrl( unwrapId(url), - ssr, canSkipImportAnalysis(url) || forceSkipImportAnalysis, resolved, ) @@ -413,7 +402,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { } // prepend base - url = joinUrlSegments(base, url) + if (!ssr) url = joinUrlSegments(base, url) } return [url, resolved.id] @@ -505,7 +494,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { } // skip ssr external if (ssr && !matchAlias(specifier)) { - if (shouldExternalizeForSSR(specifier, importer, config)) { + if (shouldExternalize(environment, specifier, importer)) { return } if (isBuiltin(specifier)) { @@ -543,9 +532,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // record as safe modules // safeModulesPath should not include the base prefix. // See https://github.com/vitejs/vite/issues/9438#issuecomment-1465270409 - server?.moduleGraph.safeModulesPath.add( - fsPathFromUrl(stripBase(url, base)), - ) + addSafeModulePath(config, fsPathFromUrl(stripBase(url, base))) if (url !== specifier) { let rewriteDone = false @@ -561,10 +548,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const file = cleanUrl(resolvedId) // Remove ?v={hash} const needsInterop = await optimizedDepNeedsInterop( + environment, depsOptimizer.metadata, file, - config, - ssr, ) if (needsInterop === undefined) { @@ -638,13 +624,13 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { if ( !isDynamicImport && isLocalImport && - config.server.preTransformRequests + environment.options.dev.preTransformRequests ) { // pre-transform known direct imports // These requests will also be registered in transformRequest to be awaited // by the deps optimizer const url = removeImportQuery(hmrUrl) - server.warmupRequest(url, { ssr }) + environment.warmupRequest(url) } } else if (!importer.startsWith(withTrailingSlash(clientDir))) { if (!isInNodeModules(importer)) { @@ -745,10 +731,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // normalize and rewrite accepted urls const normalizedAcceptedUrls = new Set() for (const { url, start, end } of acceptedUrls) { - const [normalized] = await moduleGraph.resolveUrl( - toAbsoluteUrl(url), - ssr, - ) + const [normalized] = await moduleGraph.resolveUrl(toAbsoluteUrl(url)) normalizedAcceptedUrls.add(normalized) str().overwrite(start, end, JSON.stringify(normalized), { contentOnly: true, @@ -793,11 +776,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { normalizedAcceptedUrls, isPartiallySelfAccepting ? acceptedExports : null, isSelfAccepting, - ssr, staticImportedUrls, ) if (hasHMR && prunedImports) { - handlePrunedModules(prunedImports, server) + handlePrunedModules(prunedImports, environment) } } diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 22a0d61ef3aae3..26a9c36c8ab558 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -15,6 +15,7 @@ import { numberToPos, } from '../utils' import type { Plugin } from '../plugin' +import type { Environment } from '../environment' import type { ResolvedConfig } from '../config' import { toOutputFilePathInJS } from '../build' import { genSourceMapUrl } from '../server/sourcemap' @@ -152,16 +153,11 @@ function preload( }) } -/** - * Build only. During serve this is performed as part of ./importAnalysis. - */ -export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { - const ssr = !!config.build.ssr - const isWorker = config.isWorker - const insertPreload = !(ssr || !!config.build.lib || isWorker) - +function getModulePreloadData(environment: Environment) { + const { modulePreload } = environment.options.build + const { config } = environment const resolveModulePreloadDependencies = - config.build.modulePreload && config.build.modulePreload.resolveDependencies + modulePreload && modulePreload.resolveDependencies const renderBuiltUrl = config.experimental.renderBuiltUrl const customModulePreloadPaths = !!( resolveModulePreloadDependencies || renderBuiltUrl @@ -169,34 +165,17 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const isRelativeBase = config.base === './' || config.base === '' const optimizeModulePreloadRelativePaths = isRelativeBase && !customModulePreloadPaths + return { + customModulePreloadPaths, + optimizeModulePreloadRelativePaths, + } +} - const { modulePreload } = config.build - const scriptRel = - modulePreload && modulePreload.polyfill - ? `'modulepreload'` - : `(${detectScriptRel.toString()})()` - - // There are three different cases for the preload list format in __vitePreload - // - // __vitePreload(() => import(asyncChunk), [ ...deps... ]) - // - // This is maintained to keep backwards compatibility as some users developed plugins - // using regex over this list to workaround the fact that module preload wasn't - // configurable. - const assetsURL = customModulePreloadPaths - ? // If `experimental.renderBuiltUrl` or `build.modulePreload.resolveDependencies` are used - // the dependencies are already resolved. To avoid the need for `new URL(dep, import.meta.url)` - // a helper `__vitePreloadRelativeDep` is used to resolve from relative paths which can be minimized. - `function(dep, importerUrl) { return dep[0] === '.' ? new URL(dep, importerUrl).href : dep }` - : optimizeModulePreloadRelativePaths - ? // If there isn't custom resolvers affecting the deps list, deps in the list are relative - // to the current chunk and are resolved to absolute URL by the __vitePreload helper itself. - // The importerUrl is passed as third parameter to __vitePreload in this case - `function(dep, importerUrl) { return new URL(dep, importerUrl).href }` - : // If the base isn't relative, then the deps are relative to the projects `outDir` and the base - // is appended inside __vitePreload too. - `function(dep) { return ${JSON.stringify(config.base)}+dep }` - const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}` +/** + * Build only. During serve this is performed as part of ./importAnalysis. + */ +export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { + const isWorker = config.isWorker return { name: 'vite:build-import-analysis', @@ -207,13 +186,49 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { }, load(id) { - if (id === preloadHelperId) { + const { environment } = this + if (environment && id === preloadHelperId) { + const { modulePreload } = environment.options.build + + const { customModulePreloadPaths, optimizeModulePreloadRelativePaths } = + getModulePreloadData(environment) + + const scriptRel = + modulePreload && modulePreload.polyfill + ? `'modulepreload'` + : `(${detectScriptRel.toString()})()` + + // There are three different cases for the preload list format in __vitePreload + // + // __vitePreload(() => import(asyncChunk), [ ...deps... ]) + // + // This is maintained to keep backwards compatibility as some users developed plugins + // using regex over this list to workaround the fact that module preload wasn't + // configurable. + const assetsURL = customModulePreloadPaths + ? // If `experimental.renderBuiltUrl` or `build.modulePreload.resolveDependencies` are used + // the dependencies are already resolved. To avoid the need for `new URL(dep, import.meta.url)` + // a helper `__vitePreloadRelativeDep` is used to resolve from relative paths which can be minimized. + `function(dep, importerUrl) { return dep[0] === '.' ? new URL(dep, importerUrl).href : dep }` + : optimizeModulePreloadRelativePaths + ? // If there isn't custom resolvers affecting the deps list, deps in the list are relative + // to the current chunk and are resolved to absolute URL by the __vitePreload helper itself. + // The importerUrl is passed as third parameter to __vitePreload in this case + `function(dep, importerUrl) { return new URL(dep, importerUrl).href }` + : // If the base isn't relative, then the deps are relative to the projects `outDir` and the base + // is appended inside __vitePreload too. + `function(dep) { return ${JSON.stringify(config.base)}+dep }` + const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}` return preloadCode } }, async transform(source, importer) { - if (isInNodeModules(importer) && !dynamicImportPrefixRE.test(source)) { + const { environment } = this + if ( + !environment || + (isInNodeModules(importer) && !dynamicImportPrefixRE.test(source)) + ) { return } @@ -239,6 +254,12 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const str = () => s || (s = new MagicString(source)) let needPreloadHelper = false + const ssr = environment.options.build.ssr + const insertPreload = !(ssr || !!config.build.lib || isWorker) + + const { customModulePreloadPaths, optimizeModulePreloadRelativePaths } = + getModulePreloadData(environment) + for (let index = 0; index < imports.length; index++) { const { s: start, @@ -288,7 +309,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (s) { return { code: s.toString(), - map: config.build.sourcemap + map: environment.options.build.sourcemap ? s.generateMap({ hires: 'boundary' }) : null, } @@ -297,10 +318,11 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { renderChunk(code, _, { format }) { // make sure we only perform the preload logic in modern builds. - if (code.indexOf(isModernFlag) > -1) { + const { environment } = this + if (environment && code.indexOf(isModernFlag) > -1) { const re = new RegExp(isModernFlag, 'g') const isModern = String(format === 'es') - if (config.build.sourcemap) { + if (environment.options.build.sourcemap) { const s = new MagicString(code) let match: RegExpExecArray | null while ((match = re.exec(code))) { @@ -318,9 +340,14 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { }, generateBundle({ format }, bundle) { + const ssr = this.environment.name !== 'client' // TODO if (format !== 'es' || ssr || isWorker) { return } + const buildSourcemap = this.environment.options.build.sourcemap + const { modulePreload } = this.environment.options.build + const { customModulePreloadPaths, optimizeModulePreloadRelativePaths } = + getModulePreloadData(this.environment) for (const file in bundle) { const chunk = bundle[file] @@ -549,7 +576,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (s.hasChanged()) { chunk.code = s.toString() - if (config.build.sourcemap && chunk.map) { + if (buildSourcemap && chunk.map) { const nextMap = s.generateMap({ source: chunk.fileName, hires: 'boundary', @@ -561,13 +588,13 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { map.toUrl = () => genSourceMapUrl(map) chunk.map = map - if (config.build.sourcemap === 'inline') { + if (buildSourcemap === 'inline') { chunk.code = chunk.code.replace( convertSourceMap.mapFileCommentRegex, '', ) chunk.code += `\n//# sourceMappingURL=${genSourceMapUrl(map)}` - } else if (config.build.sourcemap) { + } else if (buildSourcemap) { const mapAsset = bundle[chunk.fileName + '.map'] if (mapAsset && mapAsset.type === 'asset') { mapAsset.source = map.toString() diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 8f2475709003c0..33486a6ad07378 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -17,12 +17,12 @@ import { stringifyQuery } from 'ufo' import type { GeneralImportGlobOptions } from 'types/importGlob' import { parseAstAsync } from 'rollup/parseAst' import type { Plugin } from '../plugin' -import type { ViteDevServer } from '../server' -import type { ModuleNode } from '../server/moduleGraph' +import type { EnvironmentModuleNode } from '../server/moduleGraph' import type { ResolvedConfig } from '../config' import { evalValue, normalizePath, transformStableResult } from '../utils' import type { Logger } from '../logger' import { slash } from '../../shared/utils' +import type { Environment } from '../environment' const { isMatch, scan } = micromatch @@ -40,38 +40,16 @@ interface ParsedGeneralImportGlobOptions extends GeneralImportGlobOptions { query?: string } -export function getAffectedGlobModules( - file: string, - server: ViteDevServer, -): ModuleNode[] { - const modules: ModuleNode[] = [] - for (const [id, allGlobs] of server._importGlobMap!) { - // (glob1 || glob2) && !glob3 && !glob4... - if ( - allGlobs.some( - ({ affirmed, negated }) => - (!affirmed.length || affirmed.some((glob) => isMatch(file, glob))) && - (!negated.length || negated.every((glob) => isMatch(file, glob))), - ) - ) { - const mod = server.moduleGraph.getModuleById(id) - if (mod) modules.push(mod) - } - } - modules.forEach((i) => { - if (i?.file) server.moduleGraph.onFileChange(i.file) - }) - return modules -} - export function importGlobPlugin(config: ResolvedConfig): Plugin { - let server: ViteDevServer | undefined + const importGlobMaps = new Map< + Environment, + Map + >() return { name: 'vite:import-glob', - configureServer(_server) { - server = _server - server._importGlobMap.clear() + buildStart() { + importGlobMaps.clear() }, async transform(code, id) { if (!code.includes('import.meta.glob')) return @@ -85,24 +63,49 @@ export function importGlobPlugin(config: ResolvedConfig): Plugin { config.logger, ) if (result) { - if (server) { - const allGlobs = result.matches.map((i) => i.globsResolved) - server._importGlobMap.set( - id, - allGlobs.map((globs) => { - const affirmed: string[] = [] - const negated: string[] = [] - - for (const glob of globs) { - ;(glob[0] === '!' ? negated : affirmed).push(glob) - } - return { affirmed, negated } - }), - ) + const allGlobs = result.matches.map((i) => i.globsResolved) + if (!importGlobMaps.has(this.environment)) { + importGlobMaps.set(this.environment, new Map()) } + importGlobMaps.get(this.environment)!.set( + id, + allGlobs.map((globs) => { + const affirmed: string[] = [] + const negated: string[] = [] + + for (const glob of globs) { + ;(glob[0] === '!' ? negated : affirmed).push(glob) + } + return { affirmed, negated } + }), + ) + return transformStableResult(result.s, id, config) } }, + hotUpdate({ type, file, modules: oldModules, environment }) { + if (type === 'update') return + + const importGlobMap = importGlobMaps.get(environment) + if (!importGlobMap) return + + const modules: EnvironmentModuleNode[] = [] + for (const [id, allGlobs] of importGlobMap) { + // (glob1 || glob2) && !glob3 && !glob4... + if ( + allGlobs.some( + ({ affirmed, negated }) => + (!affirmed.length || + affirmed.some((glob) => isMatch(file, glob))) && + (!negated.length || negated.every((glob) => isMatch(file, glob))), + ) + ) { + const mod = environment.moduleGraph.getModuleById(id) + if (mod) modules.push(mod) + } + } + return modules.length > 0 ? [...oldModules, ...modules] : undefined + }, } } diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index fc230c686641b1..d5a7a72aad1992 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -1,10 +1,8 @@ import aliasPlugin, { type ResolverFunction } from '@rollup/plugin-alias' import type { ObjectHook } from 'rollup' import type { PluginHookUtils, ResolvedConfig } from '../config' -import { isDepsOptimizerEnabled } from '../config' +import { isDepOptimizationDisabled } from '../optimizer' import type { HookHandler, Plugin, PluginWithRequiredHook } from '../plugin' -import { getDepsOptimizer } from '../optimizer' -import { shouldExternalizeForSSR } from '../ssr/ssrExternal' import { watchPackageDataPlugin } from '../packages' import { getFsUtils } from '../fsUtils' import { jsonPlugin } from './json' @@ -26,6 +24,7 @@ import { assetImportMetaUrlPlugin } from './assetImportMetaUrl' import { metadataPlugin } from './metadata' import { dynamicImportVarsPlugin } from './dynamicImportVars' import { importGlobPlugin } from './importMetaGlob' +// TODO: import { loadFallbackPlugin } from './loadFallback' export async function resolvePlugins( config: ResolvedConfig, @@ -39,12 +38,14 @@ export async function resolvePlugins( ? await (await import('../build')).resolveBuildPlugins(config) : { pre: [], post: [] } const { modulePreload } = config.build - const depsOptimizerEnabled = + const depOptimizationEnabled = !isBuild && - (isDepsOptimizerEnabled(config, false) || - isDepsOptimizerEnabled(config, true)) + Object.values(config.environments).some( + (environment) => !isDepOptimizationDisabled(environment.dev.optimizeDeps), + ) + return [ - depsOptimizerEnabled ? optimizedDepsPlugin(config) : null, + depOptimizationEnabled ? optimizedDepsPlugin(config) : null, isBuild ? metadataPlugin() : null, !isWorker ? watchPackageDataPlugin(config.packageCache) : null, preAliasPlugin(config), @@ -52,27 +53,25 @@ export async function resolvePlugins( entries: config.resolve.alias, customResolver: viteAliasCustomResolver, }), + ...prePlugins, + modulePreload !== false && modulePreload.polyfill ? modulePreloadPolyfillPlugin(config) : null, - resolvePlugin({ - ...config.resolve, - root: config.root, - isProduction: config.isProduction, - isBuild, - packageCache: config.packageCache, - ssrConfig: config.ssr, - asSrc: true, - fsUtils: getFsUtils(config), - getDepsOptimizer: isBuild - ? undefined - : (ssr: boolean) => getDepsOptimizer(config, ssr), - shouldExternalize: - isBuild && config.build.ssr - ? (id, importer) => shouldExternalizeForSSR(id, importer, config) - : undefined, - }), + resolvePlugin( + { + root: config.root, + isProduction: config.isProduction, + isBuild, + packageCache: config.packageCache, + asSrc: true, + fsUtils: getFsUtils(config), + optimizeDeps: true, + externalize: isBuild && !!config.build.ssr, // TODO: should we do this for all environments? + }, + config.environments, + ), htmlInlineProxyPlugin(config), cssPlugin(config), config.esbuild !== false ? esbuildPlugin(config) : null, @@ -86,7 +85,9 @@ export async function resolvePlugins( wasmHelperPlugin(config), webWorkerPlugin(config), assetPlugin(config), + ...normalPlugins, + wasmFallbackPlugin(), definePlugin(config), cssPostPlugin(config), @@ -96,8 +97,11 @@ export async function resolvePlugins( ...buildPlugins.pre, dynamicImportVarsPlugin(config), importGlobPlugin(config), + ...postPlugins, + ...buildPlugins.post, + // internal server-only plugins are always applied after everything else ...(isBuild ? [] @@ -105,6 +109,7 @@ export async function resolvePlugins( clientInjectionsPlugin(config), cssAnalysisPlugin(config), importAnalysisPlugin(config), + // TODO: loadFallbackPlugin(config), ]), ].filter(Boolean) as Plugin[] } diff --git a/packages/vite/src/node/plugins/loadFallback.ts b/packages/vite/src/node/plugins/loadFallback.ts index 7d56797e48e681..34ae8a0bce0eaf 100644 --- a/packages/vite/src/node/plugins/loadFallback.ts +++ b/packages/vite/src/node/plugins/loadFallback.ts @@ -1,13 +1,108 @@ import fsp from 'node:fs/promises' -import type { Plugin } from '..' +import path from 'node:path' +import type { SourceMap } from 'rollup' import { cleanUrl } from '../../shared/utils' +import type { ResolvedConfig } from '../config' +import type { Plugin } from '../plugin' +import { extractSourcemapFromFile } from '../server/sourcemap' +import { isFileLoadingAllowed } from '../server/middlewares/static' +import type { DevEnvironment } from '../server/environment' +import type { EnvironmentModuleNode } from '../server/moduleGraph' +import { ensureWatchedFile } from '../utils' +import { checkPublicFile } from '../publicDir' /** * A plugin to provide build load fallback for arbitrary request with queries. + * + * TODO: This plugin isn't currently being use. The idea is to consolidate the way + * we handle the fallback during build (also with a plugin) instead of handling this + * in transformRequest(). There are some CI fails right now with the current + * implementation. Reverting for now to be able to merge the other changes. */ -export function loadFallbackPlugin(): Plugin { +export function loadFallbackPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:load-fallback', + async load(id, options) { + const environment = this.environment as DevEnvironment + + let code: string | null = null + let map: SourceMap | null = null + + // if this is an html request and there is no load result, skip ahead to + // SPA fallback. + if (options?.html && !id.endsWith('.html')) { + return null + } + // try fallback loading it from fs as string + // if the file is a binary, there should be a plugin that already loaded it + // as string + // only try the fallback if access is allowed, skip for out of root url + // like /service-worker.js or /api/users + const file = cleanUrl(id) + if ( + this.environment.options.nodeCompatible || + isFileLoadingAllowed(config, file) // Do we need fsPathFromId here? + ) { + try { + code = await fsp.readFile(file, 'utf-8') + } catch (e) { + if (e.code !== 'ENOENT') { + if (e.code === 'EISDIR') { + e.message = `${e.message} ${file}` + } + throw e + } + } + if (code != null && environment.watcher) { + ensureWatchedFile(environment.watcher, file, config.root) + } + } + if (code) { + try { + const extracted = await extractSourcemapFromFile(code, file) + if (extracted) { + code = extracted.code + map = extracted.map + } + } catch (e) { + environment.logger.warn( + `Failed to load source map for ${file}.\n${e}`, + { + timestamp: true, + }, + ) + } + return { code, map } + } + + const isPublicFile = checkPublicFile(id, config) + let publicDirName = path.relative(config.root, config.publicDir) + if (publicDirName[0] !== '.') publicDirName = '/' + publicDirName + const msg = isPublicFile + ? `This file is in ${publicDirName} and will be copied as-is during ` + + `build without going through the plugin transforms, and therefore ` + + `should not be imported from source code. It can only be referenced ` + + `via HTML tags.` + : `Does the file exist?` + const importerMod: EnvironmentModuleNode | undefined = + environment.moduleGraph.idToModuleMap + .get(id) + ?.importers.values() + .next().value + const importer = importerMod?.file || importerMod?.url + environment.logger.warn( + `Failed to load ${id}${importer ? ` in ${importer}` : ''}. ${msg}`, + ) + }, + } +} + +/** + * A plugin to provide build load fallback for arbitrary request with queries. + */ +export function buildLoadFallbackPlugin(): Plugin { + return { + name: 'vite:build-load-fallback', async load(id) { try { const cleanedId = cleanUrl(id) diff --git a/packages/vite/src/node/plugins/manifest.ts b/packages/vite/src/node/plugins/manifest.ts index 84602e78268866..0cf36c5060eff4 100644 --- a/packages/vite/src/node/plugins/manifest.ts +++ b/packages/vite/src/node/plugins/manifest.ts @@ -5,10 +5,9 @@ import type { OutputChunk, RenderedChunk, } from 'rollup' -import type { ResolvedConfig } from '..' import type { Plugin } from '../plugin' import { normalizePath, sortObjectKeys } from '../utils' -import { generatedAssets } from './asset' +import { generatedAssetsMap } from './asset' import type { GeneratedAssetMeta } from './asset' const endsWithJSRE = /\.[cm]?js$/ @@ -27,7 +26,7 @@ export interface ManifestChunk { dynamicImports?: string[] } -export function manifestPlugin(config: ResolvedConfig): Plugin { +export function manifestPlugin(): Plugin { const manifest: Manifest = {} let outputCount: number @@ -40,8 +39,11 @@ export function manifestPlugin(config: ResolvedConfig): Plugin { }, generateBundle({ format }, bundle) { + const { root } = this.environment.config + const buildOptions = this.environment.options.build + function getChunkName(chunk: OutputChunk) { - return getChunkOriginalFileName(chunk, config.root, format) + return getChunkOriginalFileName(chunk, root, format) } function getInternalImports(imports: string[]): string[] { @@ -112,7 +114,7 @@ export function manifestPlugin(config: ResolvedConfig): Plugin { } const fileNameToAssetMeta = new Map() - const assets = generatedAssets.get(config)! + const assets = generatedAssetsMap.get(this.environment)! assets.forEach((asset, referenceId) => { try { const fileName = this.getFileName(referenceId) @@ -158,13 +160,13 @@ export function manifestPlugin(config: ResolvedConfig): Plugin { }) outputCount++ - const output = config.build.rollupOptions?.output + const output = buildOptions.rollupOptions?.output const outputLength = Array.isArray(output) ? output.length : 1 if (outputCount >= outputLength) { this.emitFile({ fileName: - typeof config.build.manifest === 'string' - ? config.build.manifest + typeof buildOptions.manifest === 'string' + ? buildOptions.manifest : '.vite/manifest.json', type: 'asset', source: JSON.stringify(sortObjectKeys(manifest), undefined, 2), diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index 6d6a8d22eb9468..5d6a76c90e62e8 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -1,10 +1,10 @@ import fsp from 'node:fs/promises' import colors from 'picocolors' -import type { ResolvedConfig } from '..' +import type { DevEnvironment, ResolvedConfig } from '..' import type { Plugin } from '../plugin' import { DEP_VERSION_RE } from '../constants' import { createDebugger } from '../utils' -import { getDepsOptimizer, optimizedDepInfoFromFile } from '../optimizer' +import { optimizedDepInfoFromFile } from '../optimizer' import { cleanUrl } from '../../shared/utils' export const ERR_OPTIMIZE_DEPS_PROCESSING_ERROR = @@ -19,8 +19,9 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:optimized-deps', - resolveId(id, source, { ssr }) { - if (getDepsOptimizer(config, ssr)?.isOptimizedDepFile(id)) { + resolveId(id) { + const environment = this.environment as DevEnvironment + if (environment?.depsOptimizer?.isOptimizedDepFile(id)) { return id } }, @@ -29,9 +30,9 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin { // The logic to register an id to wait until it is processed // is in importAnalysis, see call to delayDepsOptimizerUntil - async load(id, options) { - const ssr = options?.ssr === true - const depsOptimizer = getDepsOptimizer(config, ssr) + async load(id) { + const environment = this.environment as DevEnvironment + const depsOptimizer = environment?.depsOptimizer if (depsOptimizer?.isOptimizedDepFile(id)) { const metadata = depsOptimizer.metadata const file = cleanUrl(id) diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index eaefdb7e6eb65d..1e9edf0991915d 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -6,7 +6,7 @@ import type { ResolvedConfig, } from '..' import type { Plugin } from '../plugin' -import { createIsConfiguredAsSsrExternal } from '../ssr/ssrExternal' +import { isConfiguredAsExternal } from '../external' import { bareImportRE, isInNodeModules, @@ -14,7 +14,6 @@ import { moduleListContains, } from '../utils' import { getFsUtils } from '../fsUtils' -import { getDepsOptimizer } from '../optimizer' import { cleanUrl, withTrailingSlash } from '../../shared/utils' import { tryOptimizedResolve } from './resolve' @@ -23,15 +22,17 @@ import { tryOptimizedResolve } from './resolve' */ export function preAliasPlugin(config: ResolvedConfig): Plugin { const findPatterns = getAliasPatterns(config.resolve.alias) - const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config) const isBuild = config.command === 'build' const fsUtils = getFsUtils(config) return { name: 'vite:pre-alias', async resolveId(id, importer, options) { + const { environment } = this const ssr = options?.ssr === true - const depsOptimizer = !isBuild && getDepsOptimizer(config, ssr) + const depsOptimizer = + environment?.mode === 'dev' ? environment.depsOptimizer : undefined if ( + environment && importer && depsOptimizer && bareImportRE.test(id) && @@ -69,7 +70,11 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { (isInNodeModules(resolvedId) || optimizeDeps.include?.includes(id)) && isOptimizable(resolvedId, optimizeDeps) && - !(isBuild && ssr && isConfiguredAsExternal(id, importer)) && + !( + isBuild && + ssr && + isConfiguredAsExternal(environment, id, importer) + ) && (!ssr || optimizeAliasReplacementForSSR(resolvedId, optimizeDeps)) ) { // aliased dep has not yet been optimized @@ -87,6 +92,7 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { } } +// TODO: environment? function optimizeAliasReplacementForSSR( id: string, optimizeDeps: DepOptimizationOptions, diff --git a/packages/vite/src/node/plugins/reporter.ts b/packages/vite/src/node/plugins/reporter.ts index 285d9baa7daba2..77626ef57c1c09 100644 --- a/packages/vite/src/node/plugins/reporter.ts +++ b/packages/vite/src/node/plugins/reporter.ts @@ -2,8 +2,11 @@ import path from 'node:path' import { gzip } from 'node:zlib' import { promisify } from 'node:util' import colors from 'picocolors' -import type { Plugin } from 'rollup' +import type { OutputBundle } from 'rollup' +import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' +import type { Environment } from '../environment' +import { usePerEnvironmentState } from '../environment' import { isDefined, isInNodeModules, normalizePath } from '../utils' import { LogLevels } from '../logger' import { withTrailingSlash } from '../../shared/utils' @@ -25,7 +28,6 @@ const COMPRESSIBLE_ASSETS_RE = /\.(?:html|json|svg|txt|xml|xhtml)$/ export function buildReporterPlugin(config: ResolvedConfig): Plugin { const compress = promisify(gzip) - const chunkLimit = config.build.chunkSizeWarningLimit const numberFormatter = new Intl.NumberFormat('en', { maximumFractionDigits: 2, @@ -37,85 +39,258 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { const tty = process.stdout.isTTY && !process.env.CI const shouldLogInfo = LogLevels[config.logLevel || 'info'] >= LogLevels.info - let hasTransformed = false - let hasRenderedChunk = false - let hasCompressChunk = false - let transformedCount = 0 - let chunkCount = 0 - let compressedCount = 0 - async function getCompressedSize( - code: string | Uint8Array, - ): Promise { - if (config.build.ssr || !config.build.reportCompressedSize) { - return null + const modulesReporter = usePerEnvironmentState((environment: Environment) => { + let hasTransformed = false + let transformedCount = 0 + + const logTransform = throttle((id: string) => { + writeLine( + `transforming (${transformedCount}) ${colors.dim( + path.relative(config.root, id), + )}`, + ) + }) + + return { + reset() { + transformedCount = 0 + }, + register(id: string) { + transformedCount++ + if (shouldLogInfo) { + if (!tty) { + if (!hasTransformed) { + config.logger.info(`transforming...`) + } + } else { + if (id.includes(`?`)) return + logTransform(id) + } + hasTransformed = true + } + }, + log() { + if (shouldLogInfo) { + if (tty) { + clearLine() + } + environment.logger.info( + `${colors.green(`✓`)} ${transformedCount} modules transformed.`, + ) + } + }, } - if (shouldLogInfo && !hasCompressChunk) { - if (!tty) { - config.logger.info('computing gzip size...') - } else { - writeLine('computing gzip size (0)...') + }) + + const chunksReporter = usePerEnvironmentState((environment: Environment) => { + let hasRenderedChunk = false + let hasCompressChunk = false + let chunkCount = 0 + let compressedCount = 0 + + async function getCompressedSize( + code: string | Uint8Array, + ): Promise { + if ( + environment.options.build.ssr || + !environment.options.build.reportCompressedSize + ) { + return null } - hasCompressChunk = true - } - const compressed = await compress( - typeof code === 'string' ? code : Buffer.from(code), - ) - compressedCount++ - if (shouldLogInfo && tty) { - writeLine(`computing gzip size (${compressedCount})...`) + if (shouldLogInfo && !hasCompressChunk) { + if (!tty) { + config.logger.info('computing gzip size...') + } else { + writeLine('computing gzip size (0)...') + } + hasCompressChunk = true + } + const compressed = await compress( + typeof code === 'string' ? code : Buffer.from(code), + ) + compressedCount++ + if (shouldLogInfo && tty) { + writeLine(`computing gzip size (${compressedCount})...`) + } + return compressed.length } - return compressed.length - } - const logTransform = throttle((id: string) => { - writeLine( - `transforming (${transformedCount}) ${colors.dim( - path.relative(config.root, id), - )}`, - ) + return { + reset() { + chunkCount = 0 + compressedCount = 0 + }, + register() { + chunkCount++ + if (shouldLogInfo) { + if (!tty) { + if (!hasRenderedChunk) { + environment.logger.info('rendering chunks...') + } + } else { + writeLine(`rendering chunks (${chunkCount})...`) + } + hasRenderedChunk = true + } + }, + async log(output: OutputBundle, outDir?: string) { + const chunkLimit = environment.options.build.chunkSizeWarningLimit + + let hasLargeChunks = false + + if (shouldLogInfo) { + const entries = ( + await Promise.all( + Object.values(output).map( + async (chunk): Promise => { + if (chunk.type === 'chunk') { + return { + name: chunk.fileName, + group: 'JS', + size: chunk.code.length, + compressedSize: await getCompressedSize(chunk.code), + mapSize: chunk.map ? chunk.map.toString().length : null, + } + } else { + if (chunk.fileName.endsWith('.map')) return null + const isCSS = chunk.fileName.endsWith('.css') + const isCompressible = + isCSS || COMPRESSIBLE_ASSETS_RE.test(chunk.fileName) + return { + name: chunk.fileName, + group: isCSS ? 'CSS' : 'Assets', + size: chunk.source.length, + mapSize: null, // Rollup doesn't support CSS maps? + compressedSize: isCompressible + ? await getCompressedSize(chunk.source) + : null, + } + } + }, + ), + ) + ).filter(isDefined) + if (tty) clearLine() + + let longest = 0 + let biggestSize = 0 + let biggestMap = 0 + let biggestCompressSize = 0 + for (const entry of entries) { + if (entry.name.length > longest) longest = entry.name.length + if (entry.size > biggestSize) biggestSize = entry.size + if (entry.mapSize && entry.mapSize > biggestMap) { + biggestMap = entry.mapSize + } + if ( + entry.compressedSize && + entry.compressedSize > biggestCompressSize + ) { + biggestCompressSize = entry.compressedSize + } + } + + const sizePad = displaySize(biggestSize).length + const mapPad = displaySize(biggestMap).length + const compressPad = displaySize(biggestCompressSize).length + + const relativeOutDir = normalizePath( + path.relative( + config.root, + path.resolve( + config.root, + outDir ?? environment.options.build.outDir, + ), + ), + ) + const assetsDir = path.join(environment.options.build.assetsDir, '/') + + for (const group of groups) { + const filtered = entries.filter((e) => e.group === group.name) + if (!filtered.length) continue + for (const entry of filtered.sort((a, z) => a.size - z.size)) { + const isLarge = + group.name === 'JS' && entry.size / 1000 > chunkLimit + if (isLarge) hasLargeChunks = true + const sizeColor = isLarge ? colors.yellow : colors.dim + let log = colors.dim(withTrailingSlash(relativeOutDir)) + log += + !config.build.lib && + entry.name.startsWith(withTrailingSlash(assetsDir)) + ? colors.dim(assetsDir) + + group.color( + entry.name + .slice(assetsDir.length) + .padEnd(longest + 2 - assetsDir.length), + ) + : group.color(entry.name.padEnd(longest + 2)) + log += colors.bold( + sizeColor(displaySize(entry.size).padStart(sizePad)), + ) + if (entry.compressedSize) { + log += colors.dim( + ` │ gzip: ${displaySize(entry.compressedSize).padStart( + compressPad, + )}`, + ) + } + if (entry.mapSize) { + log += colors.dim( + ` │ map: ${displaySize(entry.mapSize).padStart(mapPad)}`, + ) + } + config.logger.info(log) + } + } + } else { + hasLargeChunks = Object.values(output).some((chunk) => { + return ( + chunk.type === 'chunk' && chunk.code.length / 1000 > chunkLimit + ) + }) + } + + if ( + hasLargeChunks && + environment.options.build.minify && + !config.build.lib && + !environment.options.build.ssr + ) { + environment.logger.warn( + colors.yellow( + `\n(!) Some chunks are larger than ${chunkLimit} kB after minification. Consider:\n` + + `- Using dynamic import() to code-split the application\n` + + `- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks\n` + + `- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.`, + ), + ) + } + }, + } }) return { name: 'vite:reporter', + sharedDuringBuild: true, transform(_, id) { - transformedCount++ - if (shouldLogInfo) { - if (!tty) { - if (!hasTransformed) { - config.logger.info(`transforming...`) - } - } else { - if (id.includes(`?`)) return - logTransform(id) - } - hasTransformed = true - } - return null + modulesReporter(this).register(id) }, buildStart() { - transformedCount = 0 + modulesReporter(this).reset() }, buildEnd() { - if (shouldLogInfo) { - if (tty) { - clearLine() - } - config.logger.info( - `${colors.green(`✓`)} ${transformedCount} modules transformed.`, - ) - } + modulesReporter(this).log() }, renderStart() { - chunkCount = 0 - compressedCount = 0 + chunksReporter(this).reset() }, - renderChunk(code, chunk, options) { + renderChunk(_, chunk, options) { if (!options.inlineDynamicImports) { for (const id of chunk.moduleIds) { const module = this.getModuleInfo(id) @@ -146,149 +321,15 @@ export function buildReporterPlugin(config: ResolvedConfig): Plugin { } } - chunkCount++ - if (shouldLogInfo) { - if (!tty) { - if (!hasRenderedChunk) { - config.logger.info('rendering chunks...') - } - } else { - writeLine(`rendering chunks (${chunkCount})...`) - } - hasRenderedChunk = true - } - return null + chunksReporter(this).register() }, generateBundle() { if (shouldLogInfo && tty) clearLine() }, - async writeBundle({ dir: outDir }, output) { - let hasLargeChunks = false - - if (shouldLogInfo) { - const entries = ( - await Promise.all( - Object.values(output).map( - async (chunk): Promise => { - if (chunk.type === 'chunk') { - return { - name: chunk.fileName, - group: 'JS', - size: chunk.code.length, - compressedSize: await getCompressedSize(chunk.code), - mapSize: chunk.map ? chunk.map.toString().length : null, - } - } else { - if (chunk.fileName.endsWith('.map')) return null - const isCSS = chunk.fileName.endsWith('.css') - const isCompressible = - isCSS || COMPRESSIBLE_ASSETS_RE.test(chunk.fileName) - return { - name: chunk.fileName, - group: isCSS ? 'CSS' : 'Assets', - size: chunk.source.length, - mapSize: null, // Rollup doesn't support CSS maps? - compressedSize: isCompressible - ? await getCompressedSize(chunk.source) - : null, - } - } - }, - ), - ) - ).filter(isDefined) - if (tty) clearLine() - - let longest = 0 - let biggestSize = 0 - let biggestMap = 0 - let biggestCompressSize = 0 - for (const entry of entries) { - if (entry.name.length > longest) longest = entry.name.length - if (entry.size > biggestSize) biggestSize = entry.size - if (entry.mapSize && entry.mapSize > biggestMap) { - biggestMap = entry.mapSize - } - if ( - entry.compressedSize && - entry.compressedSize > biggestCompressSize - ) { - biggestCompressSize = entry.compressedSize - } - } - - const sizePad = displaySize(biggestSize).length - const mapPad = displaySize(biggestMap).length - const compressPad = displaySize(biggestCompressSize).length - - const relativeOutDir = normalizePath( - path.relative( - config.root, - path.resolve(config.root, outDir ?? config.build.outDir), - ), - ) - const assetsDir = path.join(config.build.assetsDir, '/') - - for (const group of groups) { - const filtered = entries.filter((e) => e.group === group.name) - if (!filtered.length) continue - for (const entry of filtered.sort((a, z) => a.size - z.size)) { - const isLarge = - group.name === 'JS' && entry.size / 1000 > chunkLimit - if (isLarge) hasLargeChunks = true - const sizeColor = isLarge ? colors.yellow : colors.dim - let log = colors.dim(withTrailingSlash(relativeOutDir)) - log += - !config.build.lib && - entry.name.startsWith(withTrailingSlash(assetsDir)) - ? colors.dim(assetsDir) + - group.color( - entry.name - .slice(assetsDir.length) - .padEnd(longest + 2 - assetsDir.length), - ) - : group.color(entry.name.padEnd(longest + 2)) - log += colors.bold( - sizeColor(displaySize(entry.size).padStart(sizePad)), - ) - if (entry.compressedSize) { - log += colors.dim( - ` │ gzip: ${displaySize(entry.compressedSize).padStart( - compressPad, - )}`, - ) - } - if (entry.mapSize) { - log += colors.dim( - ` │ map: ${displaySize(entry.mapSize).padStart(mapPad)}`, - ) - } - config.logger.info(log) - } - } - } else { - hasLargeChunks = Object.values(output).some((chunk) => { - return chunk.type === 'chunk' && chunk.code.length / 1000 > chunkLimit - }) - } - - if ( - hasLargeChunks && - config.build.minify && - !config.build.lib && - !config.build.ssr - ) { - config.logger.warn( - colors.yellow( - `\n(!) Some chunks are larger than ${chunkLimit} kB after minification. Consider:\n` + - `- Using dynamic import() to code-split the application\n` + - `- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks\n` + - `- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.`, - ), - ) - } + async writeBundle({ dir }, output) { + await chunksReporter(this).log(output, dir) }, } } diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index ccffd1c152972c..edd8bc8d8e16bc 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -35,12 +35,14 @@ import { safeRealpathSync, tryStatSync, } from '../utils' +import type { ResolvedEnvironmentOptions } from '../config' import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' -import type { SSROptions } from '..' +import type { DepOptimizationOptions, SSROptions } from '..' import type { PackageCache, PackageData } from '../packages' import type { FsUtils } from '../fsUtils' import { commonFsUtils } from '../fsUtils' +import { shouldExternalize } from '../external' import { findNearestMainPackageData, findNearestPackageData, @@ -79,6 +81,7 @@ export interface ResolveOptions { */ mainFields?: string[] conditions?: string[] + externalConditions?: string[] /** * @default ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'] */ @@ -88,13 +91,19 @@ export interface ResolveOptions { * @default false */ preserveSymlinks?: boolean + /** + * external/noExternal logic, this only works for certain environments + * Previously this was ssr.external/ssr.noExternal + * TODO: better abstraction that works for the client environment too? + */ + noExternal?: string | RegExp | (string | RegExp)[] | true + external?: string[] | true } -export interface InternalResolveOptions extends Required { +interface ResolvePluginOptions { root: string isBuild: boolean isProduction: boolean - ssrConfig?: SSROptions packageCache?: PackageCache fsUtils?: FsUtils /** @@ -107,6 +116,8 @@ export interface InternalResolveOptions extends Required { tryPrefix?: string preferRelative?: boolean isRequire?: boolean + nodeCompatible?: boolean + webCompatible?: boolean // #3040 // when the importer is a ts module, // if the specifier requests a non-existent `.js/jsx/mjs/cjs` file, @@ -117,8 +128,31 @@ export interface InternalResolveOptions extends Required { scan?: boolean // Appends ?__vite_skip_optimization to the resolved id if shouldn't be optimized ssrOptimizeCheck?: boolean - // Resolve using esbuild deps optimization + + /** + * Optimize deps during dev, defaults to false // TODO: Review default + * @internal + */ + optimizeDeps?: boolean + + /** + * externalize using external/noExternal, defaults to false // TODO: Review default + * @internal + */ + externalize?: boolean + + /** + * Previous deps optimizer logic + * @internal + * @deprecated + */ getDepsOptimizer?: (ssr: boolean) => DepsOptimizer | undefined + + /** + * Externalize logic for SSR builds + * @internal + * @deprecated + */ shouldExternalize?: (id: string, importer?: string) => boolean | undefined /** @@ -127,22 +161,36 @@ export interface InternalResolveOptions extends Required { * @internal */ idOnly?: boolean + + /** + * @deprecated environment.options are used instead + */ + ssrConfig?: SSROptions } -export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { - const { - root, - isProduction, - asSrc, - ssrConfig, - preferRelative = false, - } = resolveOptions - - const { - target: ssrTarget, - noExternal: ssrNoExternal, - external: ssrExternal, - } = ssrConfig ?? {} +export interface InternalResolveOptions + extends Required, + ResolvePluginOptions {} + +// Defined ResolveOptions are used to overwrite the values for all environments +// It is used when creating custom resolvers (for CSS, scanning, etc) +// TODO: It could be more clear to make the plugin constructor be: +// resolvePlugin(pluginOptions: ResolvePluginOptions, overrideResolveOptions?: ResolveOptions) +export interface ResolvePluginOptionsWithOverrides + extends ResolveOptions, + ResolvePluginOptions {} + +export function resolvePlugin( + resolveOptions: ResolvePluginOptionsWithOverrides, + /** + * @internal + * The deprecated config.createResolver creates a pluginContainer before + * environments are created. The resolve plugin is especial as it works without + * environments to enable this use case. It only needs access to the resolve options. + */ + environmentsOptions?: Record, +): Plugin { + const { root, isProduction, asSrc, preferRelative = false } = resolveOptions // In unix systems, absolute paths inside root first needs to be checked as an // absolute URL (/root/root/path-to-file) resulting in failed checks before falling @@ -166,39 +214,42 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { const ssr = resolveOpts?.ssr === true - // We need to delay depsOptimizer until here instead of passing it as an option - // the resolvePlugin because the optimizer is created on server listen during dev - const depsOptimizer = resolveOptions.getDepsOptimizer?.(ssr) + // The resolve plugin is used for createIdResolver and the depsOptimizer should be + // disabled in that case, so deps optimization is opt-in when creating the plugin. + const depsOptimizer = + resolveOptions.optimizeDeps && this.environment.mode === 'dev' + ? this.environment?.depsOptimizer + : undefined if (id.startsWith(browserExternalId)) { return id } - const targetWeb = !ssr || ssrTarget === 'webworker' - // this is passed by @rollup/plugin-commonjs const isRequire: boolean = resolveOpts?.custom?.['node-resolve']?.isRequire ?? false - // end user can configure different conditions for ssr and client. - // falls back to client conditions if no ssr conditions supplied - const ssrConditions = - resolveOptions.ssrConfig?.resolve?.conditions || - resolveOptions.conditions - + const environmentName = this.environment.name ?? (ssr ? 'ssr' : 'client') + const currentEnvironmentOptions = + this.environment?.options || environmentsOptions?.[environmentName] + const environmentResolveOptions = currentEnvironmentOptions?.resolve + if (!environmentResolveOptions) { + throw new Error( + `Missing ResolveOptions for ${environmentName} environment`, + ) + } const options: InternalResolveOptions = { isRequire, - ...resolveOptions, + ...environmentResolveOptions, + nodeCompatible: currentEnvironmentOptions.nodeCompatible, + webCompatible: currentEnvironmentOptions.webCompatible, + ...resolveOptions, // plugin options + resolve options overrides scan: resolveOpts?.scan ?? resolveOptions.scan, - conditions: ssr ? ssrConditions : resolveOptions.conditions, } - const resolvedImports = resolveSubpathImports( - id, - importer, - options, - targetWeb, - ) + const depsOptimizerOptions = this.environment.options.dev.optimizeDeps + + const resolvedImports = resolveSubpathImports(id, importer, options) if (resolvedImports) { id = resolvedImports @@ -238,7 +289,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { // always return here even if res doesn't exist since /@fs/ is explicit // if the file doesn't exist it should be a 404. debug?.(`[@fs] ${colors.cyan(id)} -> ${colors.dim(res)}`) - return ensureVersionQuery(res, id, options, depsOptimizer) + return ensureVersionQuery(res, id, options, ssr, depsOptimizer) } // URL @@ -251,7 +302,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { const fsPath = path.resolve(root, id.slice(1)) if ((res = tryFsResolve(fsPath, options))) { debug?.(`[url] ${colors.cyan(id)} -> ${colors.dim(res)}`) - return ensureVersionQuery(res, id, options, depsOptimizer) + return ensureVersionQuery(res, id, options, ssr, depsOptimizer) } } @@ -270,10 +321,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { if (depsOptimizer?.isOptimizedDepFile(normalizedFsPath)) { // Optimized files could not yet exist in disk, resolve to the full path // Inject the current browserHash version if the path doesn't have one - if ( - !resolveOptions.isBuild && - !DEP_VERSION_RE.test(normalizedFsPath) - ) { + if (!options.isBuild && !DEP_VERSION_RE.test(normalizedFsPath)) { const browserHash = optimizedDepInfoFromFile( depsOptimizer.metadata, normalizedFsPath, @@ -286,15 +334,22 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { } if ( - targetWeb && + options.webCompatible && options.mainFields.includes('browser') && - (res = tryResolveBrowserMapping(fsPath, importer, options, true)) + (res = tryResolveBrowserMapping( + fsPath, + importer, + options, + true, + undefined, + depsOptimizerOptions, + )) ) { return res } if ((res = tryFsResolve(fsPath, options))) { - res = ensureVersionQuery(res, id, options, depsOptimizer) + res = ensureVersionQuery(res, id, options, ssr, depsOptimizer) debug?.(`[relative] ${colors.cyan(id)} -> ${colors.dim(res)}`) // If this isn't a script imported from a .html file, include side effects @@ -326,7 +381,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { const fsPath = path.resolve(basedir, id) if ((res = tryFsResolve(fsPath, options))) { debug?.(`[drive-relative] ${colors.cyan(id)} -> ${colors.dim(res)}`) - return ensureVersionQuery(res, id, options, depsOptimizer) + return ensureVersionQuery(res, id, options, ssr, depsOptimizer) } } @@ -336,7 +391,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { (res = tryFsResolve(id, options)) ) { debug?.(`[fs] ${colors.cyan(id)} -> ${colors.dim(res)}`) - return ensureVersionQuery(res, id, options, depsOptimizer) + return ensureVersionQuery(res, id, options, ssr, depsOptimizer) } // external @@ -352,7 +407,9 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { // bare package imports, perform node resolve if (bareImportRE.test(id)) { - const external = options.shouldExternalize?.(id, importer) + const external = + options.externalize && + shouldExternalize(this.environment, id, importer) if ( !external && asSrc && @@ -370,7 +427,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { } if ( - targetWeb && + options.webCompatible && options.mainFields.includes('browser') && (res = tryResolveBrowserMapping( id, @@ -378,6 +435,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { options, false, external, + depsOptimizerOptions, )) ) { return res @@ -388,25 +446,26 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { id, importer, options, - targetWeb, depsOptimizer, ssr, external, + undefined, + depsOptimizerOptions, )) ) { return res } // node built-ins. - // externalize if building for SSR, otherwise redirect to empty module + // externalize if building for a node compatible environment, otherwise redirect to empty module if (isBuiltin(id)) { - if (ssr) { + if (options.nodeCompatible) { if ( - targetWeb && - ssrNoExternal === true && + options.webCompatible && + options.noExternal === true && // if both noExternal and external are true, noExternal will take the higher priority and bundle it. // only if the id is explicitly listed in external, we will externalize it and skip this error. - (ssrExternal === true || !ssrExternal?.includes(id)) + (options.external === true || !options.external.includes(id)) ) { let message = `Cannot bundle Node.js built-in "${id}"` if (importer) { @@ -415,7 +474,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { importer, )}"` } - message += `. Consider disabling ssr.noExternal or remove the built-in dependency.` + message += `. Consider disabling environments.${environmentName}.noExternal or remove the built-in dependency.` this.error(message) } @@ -474,7 +533,6 @@ function resolveSubpathImports( id: string, importer: string | undefined, options: InternalResolveOptions, - targetWeb: boolean, ) { if (!importer || !id.startsWith(subpathImportsPrefix)) return const basedir = path.dirname(importer) @@ -488,7 +546,6 @@ function resolveSubpathImports( pkgData.data, idWithoutPostfix, options, - targetWeb, 'imports', ) @@ -507,9 +564,11 @@ function ensureVersionQuery( resolved: string, id: string, options: InternalResolveOptions, + ssr: boolean, depsOptimizer?: DepsOptimizer, ): string { if ( + !ssr && !options.isBuild && !options.scan && depsOptimizer && @@ -665,7 +724,7 @@ function tryCleanFsResolve( } // path points to a node package const pkg = loadPackageData(pkgPath) - return resolvePackageEntry(dirPath, pkg, targetWeb, options) + return resolvePackageEntry(dirPath, pkg, options) } } catch (e) { // This check is best effort, so if an entry is not found, skip error for now @@ -699,6 +758,7 @@ function tryCleanFsResolve( export type InternalResolveOptionsWithOverrideConditions = InternalResolveOptions & { /** + * TODO: Is this needed if we have `externalConditions` in `resolve`? * @internal */ overrideConditions?: string[] @@ -708,11 +768,11 @@ export function tryNodeResolve( id: string, importer: string | null | undefined, options: InternalResolveOptionsWithOverrideConditions, - targetWeb: boolean, depsOptimizer?: DepsOptimizer, ssr: boolean = false, externalize?: boolean, allowLinkedExternal: boolean = true, + depsOptimizerOptions?: DepOptimizationOptions, ): PartialResolvedId | undefined { const { root, dedupe, isBuild, preserveSymlinks, packageCache } = options @@ -780,14 +840,14 @@ export function tryNodeResolve( let resolved: string | undefined try { - resolved = resolveId(unresolvedId, pkg, targetWeb, options) + resolved = resolveId(unresolvedId, pkg, options) } catch (err) { if (!options.tryEsmOnly) { throw err } } if (!resolved && options.tryEsmOnly) { - resolved = resolveId(unresolvedId, pkg, targetWeb, { + resolved = resolveId(unresolvedId, pkg, { ...options, isRequire: false, mainFields: DEFAULT_MAIN_FIELDS, @@ -861,8 +921,8 @@ export function tryNodeResolve( let include = depsOptimizer?.options.include if (options.ssrOptimizeCheck) { // we don't have the depsOptimizer - exclude = options.ssrConfig?.optimizeDeps?.exclude - include = options.ssrConfig?.optimizeDeps?.include + exclude = depsOptimizerOptions?.exclude + include = depsOptimizerOptions?.include } const skipOptimization = @@ -978,12 +1038,11 @@ export async function tryOptimizedResolve( export function resolvePackageEntry( id: string, { dir, data, setResolvedCache, getResolvedCache }: PackageData, - targetWeb: boolean, options: InternalResolveOptions, ): string | undefined { const { file: idWithoutPostfix, postfix } = splitFileAndPostfix(id) - const cached = getResolvedCache('.', targetWeb) + const cached = getResolvedCache('.', !!options.webCompatible) if (cached) { return cached + postfix } @@ -994,20 +1053,14 @@ export function resolvePackageEntry( // resolve exports field with highest priority // using https://github.com/lukeed/resolve.exports if (data.exports) { - entryPoint = resolveExportsOrImports( - data, - '.', - options, - targetWeb, - 'exports', - ) + entryPoint = resolveExportsOrImports(data, '.', options, 'exports') } // fallback to mainFields if still not resolved if (!entryPoint) { for (const field of options.mainFields) { if (field === 'browser') { - if (targetWeb) { + if (options.webCompatible) { entryPoint = tryResolveBrowserEntry(dir, data, options) if (entryPoint) { break @@ -1040,7 +1093,7 @@ export function resolvePackageEntry( // resolve object browser field in package.json const { browser: browserField } = data if ( - targetWeb && + options.webCompatible && options.mainFields.includes('browser') && isObject(browserField) ) { @@ -1062,7 +1115,7 @@ export function resolvePackageEntry( resolvedEntryPoint, )}${postfix !== '' ? ` (postfix: ${postfix})` : ''}`, ) - setResolvedCache('.', resolvedEntryPoint, targetWeb) + setResolvedCache('.', resolvedEntryPoint, !!options.webCompatible) return resolvedEntryPoint + postfix } } @@ -1086,7 +1139,6 @@ function resolveExportsOrImports( pkg: PackageData['data'], key: string, options: InternalResolveOptionsWithOverrideConditions, - targetWeb: boolean, type: 'imports' | 'exports', ) { const additionalConditions = new Set( @@ -1110,7 +1162,7 @@ function resolveExportsOrImports( const fn = type === 'imports' ? imports : exports const result = fn(pkg, key, { - browser: targetWeb && !additionalConditions.has('node'), + browser: options.webCompatible && !additionalConditions.has('node'), require: options.isRequire && !additionalConditions.has('import'), conditions, }) @@ -1127,10 +1179,9 @@ function resolveDeepImport( dir, data, }: PackageData, - targetWeb: boolean, options: InternalResolveOptions, ): string | undefined { - const cache = getResolvedCache(id, targetWeb) + const cache = getResolvedCache(id, !!options.webCompatible) if (cache) { return cache } @@ -1143,13 +1194,7 @@ function resolveDeepImport( if (isObject(exportsField) && !Array.isArray(exportsField)) { // resolve without postfix (see #7098) const { file, postfix } = splitFileAndPostfix(relativeId) - const exportsId = resolveExportsOrImports( - data, - file, - options, - targetWeb, - 'exports', - ) + const exportsId = resolveExportsOrImports(data, file, options, 'exports') if (exportsId !== undefined) { relativeId = exportsId + postfix } else { @@ -1166,7 +1211,7 @@ function resolveDeepImport( ) } } else if ( - targetWeb && + options.webCompatible && options.mainFields.includes('browser') && isObject(browserField) ) { @@ -1185,13 +1230,13 @@ function resolveDeepImport( path.join(dir, relativeId), options, !exportsField, // try index only if no exports field - targetWeb, + !!options.webCompatible, ) if (resolved) { debug?.( `[node/deep-import] ${colors.cyan(id)} -> ${colors.dim(resolved)}`, ) - setResolvedCache(id, resolved, targetWeb) + setResolvedCache(id, resolved, !!options.webCompatible) return resolved } } @@ -1203,6 +1248,7 @@ function tryResolveBrowserMapping( options: InternalResolveOptions, isFilePath: boolean, externalize?: boolean, + depsOptimizerOptions?: DepOptimizationOptions, ) { let res: string | undefined const pkg = @@ -1214,7 +1260,16 @@ function tryResolveBrowserMapping( if (browserMappedPath) { if ( (res = bareImportRE.test(browserMappedPath) - ? tryNodeResolve(browserMappedPath, importer, options, true)?.id + ? tryNodeResolve( + browserMappedPath, + importer, + options, + undefined, + undefined, + undefined, + undefined, + depsOptimizerOptions, + )?.id : tryFsResolve(path.join(pkg.dir, browserMappedPath), options)) ) { debug?.(`[browser mapped] ${colors.cyan(id)} -> ${colors.dim(res)}`) diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index 407ea5f0009a9e..d3e9c272c89b50 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -66,7 +66,7 @@ export const wasmHelperPlugin = (config: ResolvedConfig): Plugin => { return } - const url = await fileToUrl(id, config, this) + const url = await fileToUrl(this, id) return ` import initWasm from "${wasmHelperId}" diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 4094b581a52b63..0daeda88fd809a 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -3,7 +3,6 @@ import MagicString from 'magic-string' import type { OutputChunk } from 'rollup' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' -import type { ViteDevServer } from '../server' import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants' import { encodeURIPath, @@ -13,7 +12,9 @@ import { urlRE, } from '../utils' import { + BuildEnvironment, createToImportMetaURLBasedRelativeRuntime, + injectEnvironmentToHooks, onRollupWarning, toOutputFilePathInJS, } from '../build' @@ -68,10 +69,15 @@ async function bundleWorkerEntry( // bundle the file as entry to support imports const { rollup } = await import('rollup') const { plugins, rollupOptions, format } = config.worker + const { plugins: resolvedPlugins, config: workerConfig } = + await plugins(newBundleChain) + const workerEnvironment = new BuildEnvironment('client', workerConfig) // TODO: should this be 'worker'? const bundle = await rollup({ ...rollupOptions, input, - plugins: await plugins(newBundleChain), + plugins: resolvedPlugins.map((p) => + injectEnvironmentToHooks(workerEnvironment, p), + ), onwarn(warning, warn) { onRollupWarning(warning, warn, config) }, @@ -203,16 +209,11 @@ export function webWorkerPostPlugin(): Plugin { export function webWorkerPlugin(config: ResolvedConfig): Plugin { const isBuild = config.command === 'build' - let server: ViteDevServer const isWorker = config.isWorker return { name: 'vite:worker', - configureServer(_server) { - server = _server - }, - buildStart() { if (isWorker) { return @@ -236,7 +237,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { } }, - async transform(raw, id) { + async transform(raw, id, options) { const workerFileMatch = workerFileRE.exec(id) if (workerFileMatch) { // if import worker by worker constructor will have query.type @@ -255,11 +256,13 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { } else if (workerType === 'ignore') { if (isBuild) { injectEnv = '' - } else if (server) { + } else { // dynamic worker type we can't know how import the env // so we copy /@vite/env code of server transform result into file header - const { moduleGraph } = server - const module = moduleGraph.getModuleById(ENV_ENTRY) + const environment = this.environment + const moduleGraph = + environment?.mode === 'dev' ? environment.moduleGraph : undefined + const module = moduleGraph?.getModuleById(ENV_ENTRY) injectEnv = module?.transformResult?.code || '' } } @@ -354,7 +357,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { urlCode = JSON.stringify(await workerFileToUrl(config, id)) } } else { - let url = await fileToUrl(cleanUrl(id), config, this) + let url = await fileToUrl(this, cleanUrl(id)) url = injectQuery(url, `${WORKER_FILE_ID}&type=${workerType}`) urlCode = JSON.stringify(url) } diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index 22a5de2ec67b06..a076837cfc2b82 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -5,7 +5,8 @@ import { stripLiteral } from 'strip-literal' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' import { evalValue, injectQuery, transformStableResult } from '../utils' -import type { ResolveFn } from '..' +import { createIdResolver } from '../idResolver' +import type { ResolveIdFn } from '../idResolver' import { cleanUrl, slash } from '../../shared/utils' import type { WorkerType } from './worker' import { WORKER_FILE_ID, workerFileToUrl } from './worker' @@ -102,7 +103,7 @@ function isIncludeWorkerImportMetaUrl(code: string): boolean { export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { const isBuild = config.command === 'build' - let workerResolver: ResolveFn + let workerResolver: ResolveIdFn const fsResolveOptions: InternalResolveOptions = { ...config.resolve, @@ -110,7 +111,6 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { isProduction: config.isProduction, isBuild: config.command === 'build', packageCache: config.packageCache, - ssrConfig: config.ssr, asSrc: true, } @@ -123,8 +123,14 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { } }, - async transform(code, id, options) { - if (!options?.ssr && isIncludeWorkerImportMetaUrl(code)) { + async transform(code, id) { + const { environment } = this + // TODO: environment, same as with assetImportMetaUrlPlugin + if ( + environment && + environment.name === 'client' && + isIncludeWorkerImportMetaUrl(code) + ) { let s: MagicString | undefined const cleanString = stripLiteral(code) const workerImportMetaUrlRE = @@ -153,12 +159,12 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { file = path.resolve(path.dirname(id), url) file = tryFsResolve(file, fsResolveOptions) ?? file } else { - workerResolver ??= config.createResolver({ + workerResolver ??= createIdResolver(config, { extensions: [], tryIndex: false, preferRelative: true, }) - file = await workerResolver(url, id) + file = await workerResolver(environment, url, id) file ??= url[0] === '/' ? slash(path.join(config.publicDir, url)) @@ -176,7 +182,7 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { if (isBuild) { builtUrl = await workerFileToUrl(config, file) } else { - builtUrl = await fileToUrl(cleanUrl(file), config, this) + builtUrl = await fileToUrl(this, cleanUrl(file)) builtUrl = injectQuery( builtUrl, `${WORKER_FILE_ID}&type=${workerType}`, diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 4d2e1e645bbcdc..566b5d886e790f 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -111,7 +111,9 @@ export async function preview( true, ) - const distDir = path.resolve(config.root, config.build.outDir) + const clientOutDir = + config.environments.client.build.outDir ?? config.build.outDir + const distDir = path.resolve(config.root, clientOutDir) if ( !fs.existsSync(distDir) && // error if no plugins implement `configurePreviewServer` @@ -122,7 +124,7 @@ export async function preview( process.argv[2] === 'preview' ) { throw new Error( - `The directory "${config.build.outDir}" does not exist. Did you build your project?`, + `The directory "${clientOutDir}" does not exist. Did you build your project?`, ) } diff --git a/packages/vite/src/node/publicUtils.ts b/packages/vite/src/node/publicUtils.ts index 318c904047b2c0..a9cad7a5106db6 100644 --- a/packages/vite/src/node/publicUtils.ts +++ b/packages/vite/src/node/publicUtils.ts @@ -20,5 +20,9 @@ export { export { send } from './server/send' export { createLogger } from './logger' export { searchForWorkspaceRoot } from './server/searchRoot' -export { isFileServingAllowed } from './server/middlewares/static' + +export { + isFileServingAllowed, + isFileLoadingAllowed, +} from './server/middlewares/static' export { loadEnv, resolveEnvPrefix } from './env' diff --git a/packages/vite/src/node/server/__tests__/moduleGraph.spec.ts b/packages/vite/src/node/server/__tests__/moduleGraph.spec.ts index 2285d2fa4fa8b9..91c933c789b54f 100644 --- a/packages/vite/src/node/server/__tests__/moduleGraph.spec.ts +++ b/packages/vite/src/node/server/__tests__/moduleGraph.spec.ts @@ -1,10 +1,14 @@ import { describe, expect, it } from 'vitest' -import { ModuleGraph } from '../moduleGraph' +import { EnvironmentModuleGraph } from '../moduleGraph' +import type { ModuleNode } from '../mixedModuleGraph' +import { ModuleGraph } from '../mixedModuleGraph' describe('moduleGraph', () => { describe('invalidateModule', () => { - it('removes an ssrError', async () => { - const moduleGraph = new ModuleGraph(async (url) => ({ id: url })) + it('removes an ssr error', async () => { + const moduleGraph = new EnvironmentModuleGraph('client', async (url) => ({ + id: url, + })) const entryUrl = '/x.js' const entryModule = await moduleGraph.ensureEntryFromUrl(entryUrl, false) @@ -16,7 +20,7 @@ describe('moduleGraph', () => { }) it('ensureEntryFromUrl should based on resolvedId', async () => { - const moduleGraph = new ModuleGraph(async (url) => { + const moduleGraph = new EnvironmentModuleGraph('client', async (url) => { if (url === '/xx.js') { return { id: '/x.js' } } else { @@ -30,5 +34,75 @@ describe('moduleGraph', () => { const mod2 = await moduleGraph.ensureEntryFromUrl('/xx.js', false) expect(mod2.meta).to.equal(meta) }) + + it('ensure backward compatibility', async () => { + const clientModuleGraph = new EnvironmentModuleGraph( + 'client', + async (url) => ({ id: url }), + ) + const ssrModuleGraph = new EnvironmentModuleGraph('ssr', async (url) => ({ + id: url, + })) + const moduleGraph = new ModuleGraph({ + client: () => clientModuleGraph, + ssr: () => ssrModuleGraph, + }) + + const addBrowserModule = (url: string) => + clientModuleGraph.ensureEntryFromUrl(url) + const getBrowserModule = (url: string) => + clientModuleGraph.getModuleById(url) + + const addServerModule = (url: string) => + ssrModuleGraph.ensureEntryFromUrl(url) + const getServerModule = (url: string) => ssrModuleGraph.getModuleById(url) + + const clientModule1 = await addBrowserModule('/1.js') + const ssrModule1 = await addServerModule('/1.js') + const module1 = moduleGraph.getModuleById('/1.js')! + expect(module1._clientModule).toBe(clientModule1) + expect(module1._ssrModule).toBe(ssrModule1) + + const module2b = await moduleGraph.ensureEntryFromUrl('/b/2.js') + const module2s = await moduleGraph.ensureEntryFromUrl('/s/2.js') + expect(module2b._clientModule).toBe(getBrowserModule('/b/2.js')) + expect(module2s._ssrModule).toBe(getServerModule('/s/2.js')) + + const importersUrls = ['/1/a.js', '/1/b.js', '/1/c.js'] + ;(await Promise.all(importersUrls.map(addBrowserModule))).forEach((mod) => + clientModule1.importers.add(mod), + ) + ;(await Promise.all(importersUrls.map(addServerModule))).forEach((mod) => + ssrModule1.importers.add(mod), + ) + + expect(module1.importers.size).toBe(importersUrls.length) + + const clientModule1importersValues = [...clientModule1.importers] + const ssrModule1importersValues = [...ssrModule1.importers] + + const module1importers = module1.importers + const module1importersValues = [...module1importers.values()] + expect(module1importersValues.length).toBe(importersUrls.length) + expect(module1importersValues[1]._clientModule).toBe( + clientModule1importersValues[1], + ) + expect(module1importersValues[1]._ssrModule).toBe( + ssrModule1importersValues[1], + ) + + const module1importersFromForEach: ModuleNode[] = [] + module1.importers.forEach((imp) => { + moduleGraph.invalidateModule(imp) + module1importersFromForEach.push(imp) + }) + expect(module1importersFromForEach.length).toBe(importersUrls.length) + expect(module1importersFromForEach[1]._clientModule).toBe( + clientModule1importersValues[1], + ) + expect(module1importersFromForEach[1]._ssrModule).toBe( + ssrModule1importersValues[1], + ) + }) }) }) diff --git a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts index 070dedd2acb463..b492d6d4494856 100644 --- a/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts +++ b/packages/vite/src/node/server/__tests__/pluginContainer.spec.ts @@ -1,20 +1,11 @@ -import { beforeEach, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' import type { UserConfig } from '../../config' import { resolveConfig } from '../../config' import type { Plugin } from '../../plugin' -import { ModuleGraph } from '../moduleGraph' -import type { PluginContainer } from '../pluginContainer' -import { createPluginContainer } from '../pluginContainer' - -let resolveId: (id: string) => any -let moduleGraph: ModuleGraph +import { DevEnvironment } from '../environment' describe('plugin container', () => { describe('getModuleInfo', () => { - beforeEach(() => { - moduleGraph = new ModuleGraph((id) => resolveId(id)) - }) - it('can pass metadata between hooks', async () => { const entryUrl = '/x.js' @@ -46,26 +37,25 @@ describe('plugin container', () => { return { meta: { x: 3 } } } }, - buildEnd() { - const { meta } = this.getModuleInfo(entryUrl) ?? {} - metaArray.push(meta) - }, } - const container = await getPluginContainer({ + const environment = await getDevEnvironment({ plugins: [plugin], }) - const entryModule = await moduleGraph.ensureEntryFromUrl(entryUrl, false) + const entryModule = await environment.moduleGraph.ensureEntryFromUrl( + entryUrl, + false, + ) expect(entryModule.meta).toEqual({ x: 1 }) - const loadResult: any = await container.load(entryUrl) + const loadResult: any = await environment.pluginContainer.load(entryUrl) expect(loadResult?.meta).toEqual({ x: 2 }) - await container.transform(loadResult.code, entryUrl) - await container.close() + await environment.pluginContainer.transform(loadResult.code, entryUrl) + await environment.pluginContainer.close() - expect(metaArray).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]) + expect(metaArray).toEqual([{ x: 1 }, { x: 2 }]) }) it('can pass metadata between plugins', async () => { @@ -91,12 +81,12 @@ describe('plugin container', () => { }, } - const container = await getPluginContainer({ + const environment = await getDevEnvironment({ plugins: [plugin1, plugin2], }) - await moduleGraph.ensureEntryFromUrl(entryUrl, false) - await container.load(entryUrl) + await environment.moduleGraph.ensureEntryFromUrl(entryUrl, false) + await environment.pluginContainer.load(entryUrl) expect.assertions(1) }) @@ -137,22 +127,18 @@ describe('plugin container', () => { }, } - const container = await getPluginContainer({ + const environment = await getDevEnvironment({ plugins: [plugin1, plugin2], }) - await moduleGraph.ensureEntryFromUrl(entryUrl, false) - await container.load(entryUrl) + await environment.moduleGraph.ensureEntryFromUrl(entryUrl, false) + await environment.pluginContainer.load(entryUrl) expect.assertions(2) }) }) describe('load', () => { - beforeEach(() => { - moduleGraph = new ModuleGraph((id) => resolveId(id)) - }) - it('can resolve a secondary module', async () => { const entryUrl = '/x.js' @@ -176,12 +162,15 @@ describe('plugin container', () => { }, } - const container = await getPluginContainer({ + const environment = await getDevEnvironment({ plugins: [plugin], }) - await moduleGraph.ensureEntryFromUrl(entryUrl, false) - const loadResult: any = await container.load(entryUrl) - const result: any = await container.transform(loadResult.code, entryUrl) + await environment.moduleGraph.ensureEntryFromUrl(entryUrl, false) + const loadResult: any = await environment.pluginContainer.load(entryUrl) + const result: any = await environment.pluginContainer.transform( + loadResult.code, + entryUrl, + ) expect(result.code).equals('2') }) @@ -208,20 +197,23 @@ describe('plugin container', () => { }, } - const container = await getPluginContainer({ + const environment = await getDevEnvironment({ plugins: [plugin], }) - await moduleGraph.ensureEntryFromUrl(entryUrl, false) - const loadResult: any = await container.load(entryUrl) - const result: any = await container.transform(loadResult.code, entryUrl) + await environment.moduleGraph.ensureEntryFromUrl(entryUrl, false) + const loadResult: any = await environment.pluginContainer.load(entryUrl) + const result: any = await environment.pluginContainer.transform( + loadResult.code, + entryUrl, + ) expect(result.code).equals('3') }) }) }) -async function getPluginContainer( +async function getDevEnvironment( inlineConfig?: UserConfig, -): Promise { +): Promise { const config = await resolveConfig( { configFile: false, ...inlineConfig }, 'serve', @@ -230,7 +222,8 @@ async function getPluginContainer( // @ts-expect-error This plugin requires a ViteDevServer instance. config.plugins = config.plugins.filter((p) => !p.name.includes('pre-alias')) - resolveId = (id) => container.resolveId(id) - const container = await createPluginContainer(config, moduleGraph) - return container + const environment = new DevEnvironment('client', config, { hot: false }) + await environment.init() + + return environment } diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts new file mode 100644 index 00000000000000..c767a59254f049 --- /dev/null +++ b/packages/vite/src/node/server/environment.ts @@ -0,0 +1,364 @@ +import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner' +import type { FSWatcher } from 'dep-types/chokidar' +import colors from 'picocolors' +import { BaseEnvironment } from '../baseEnvironment' +import { ERR_OUTDATED_OPTIMIZED_DEP } from '../plugins/optimizedDeps' +import type { + EnvironmentOptions, + ResolvedConfig, + ResolvedEnvironmentOptions, +} from '../config' +import { getDefaultResolvedEnvironmentOptions } from '../config' +import { mergeConfig, promiseWithResolvers } from '../utils' +import type { FetchModuleOptions } from '../ssr/fetchModule' +import { fetchModule } from '../ssr/fetchModule' +import type { DepsOptimizer } from '../optimizer' +import { isDepOptimizationDisabled } from '../optimizer' +import { + createDepsOptimizer, + createExplicitDepsOptimizer, +} from '../optimizer/optimizer' +import { resolveEnvironmentPlugins } from '../plugin' +import { EnvironmentModuleGraph } from './moduleGraph' +import type { HotChannel } from './hmr' +import { createNoopHotChannel, getShortName, updateModules } from './hmr' +import type { TransformResult } from './transformRequest' +import { transformRequest } from './transformRequest' +import type { EnvironmentPluginContainer } from './pluginContainer' +import { + ERR_CLOSED_SERVER, + createEnvironmentPluginContainer, +} from './pluginContainer' +import type { RemoteEnvironmentTransport } from './environmentTransport' + +export interface DevEnvironmentSetup { + hot: false | HotChannel + watcher?: FSWatcher + options?: EnvironmentOptions + runner?: FetchModuleOptions & { + transport?: RemoteEnvironmentTransport + } + depsOptimizer?: DepsOptimizer +} + +export class DevEnvironment extends BaseEnvironment { + mode = 'dev' as const // TODO: should this be 'serve'? + moduleGraph: EnvironmentModuleGraph + + watcher?: FSWatcher + depsOptimizer?: DepsOptimizer + /** + * @internal + */ + _ssrRunnerOptions: FetchModuleOptions | undefined + + get pluginContainer(): EnvironmentPluginContainer { + if (!this._pluginContainer) + throw new Error( + `${this.name} environment.pluginContainer called before initialized`, + ) + return this._pluginContainer + } + /** + * @internal + */ + _pluginContainer: EnvironmentPluginContainer | undefined + + /** + * TODO: should this be public? + * @internal + */ + _closing: boolean = false + /** + * @internal + */ + _pendingRequests: Map< + string, + { + request: Promise + timestamp: number + abort: () => void + } + > + /** + * @internal + */ + _onCrawlEndCallbacks: (() => void)[] + /** + * @internal + */ + _crawlEndFinder: CrawlEndFinder + + /** + * Hot channel for this environment. If not provided or disabled, + * it will be a noop channel that does nothing. + * + * @example + * environment.hot.send({ type: 'full-reload' }) + */ + hot: HotChannel + constructor( + name: string, + config: ResolvedConfig, + setup: DevEnvironmentSetup, + ) { + let options = + config.environments[name] ?? getDefaultResolvedEnvironmentOptions(config) + if (setup.options) { + options = mergeConfig( + options, + setup.options, + ) as ResolvedEnvironmentOptions + } + super(name, config, options) + + this._pendingRequests = new Map() + + this.moduleGraph = new EnvironmentModuleGraph(name, (url: string) => + this.pluginContainer!.resolveId(url, undefined), + ) + + this.hot = setup.hot || createNoopHotChannel() + this.watcher = setup.watcher + + this._onCrawlEndCallbacks = [] + this._crawlEndFinder = setupOnCrawlEnd(() => { + this._onCrawlEndCallbacks.forEach((cb) => cb()) + }) + + this._ssrRunnerOptions = setup.runner || {} + setup.runner?.transport?.register(this) + + this.hot.on('vite:invalidate', async ({ path, message }) => { + invalidateModule(this, { + path, + message, + }) + }) + + const { optimizeDeps } = this.options.dev + if (setup.depsOptimizer) { + this.depsOptimizer = setup.depsOptimizer + } else if (isDepOptimizationDisabled(optimizeDeps)) { + this.depsOptimizer = undefined + } else { + // We only support auto-discovery for the client environment, for all other + // environments `noDiscovery` has no effect and a simpler explicit deps + // optimizer is used that only optimizes explicitly included dependencies + // so it doesn't need to reload the environment. Now that we have proper HMR + // and full reload for general environments, we can enable autodiscovery for + // them in the future + this.depsOptimizer = ( + optimizeDeps.noDiscovery || name !== 'client' + ? createExplicitDepsOptimizer + : createDepsOptimizer + )(this) + } + } + + async init(): Promise { + if (this._initiated) { + return + } + this._initiated = true + this._plugins = resolveEnvironmentPlugins(this) + this._pluginContainer = await createEnvironmentPluginContainer( + this, + this._plugins, + ) + + // TODO: Should buildStart be called here? It break backward compatibility if we do, + // and it may be better to delay it for performance + + // The deps optimizer init is delayed. TODO: add internal option? + + // TODO: move warmup here + } + + fetchModule( + id: string, + importer?: string, + options?: FetchFunctionOptions, + ): Promise { + return fetchModule(this, id, importer, { + ...this._ssrRunnerOptions, + ...options, + }) + } + + transformRequest(url: string): Promise { + return transformRequest(this, url) + } + + async warmupRequest(url: string): Promise { + await transformRequest(this, url).catch((e) => { + if ( + e?.code === ERR_OUTDATED_OPTIMIZED_DEP || + e?.code === ERR_CLOSED_SERVER + ) { + // these are expected errors + return + } + // Unexpected error, log the issue but avoid an unhandled exception + this.logger.error(`Pre-transform error: ${e.message}`, { + error: e, + timestamp: true, + }) + }) + } + + async close(): Promise { + this._closing = true + + this._crawlEndFinder?.cancel() + await Promise.allSettled([ + this.pluginContainer.close(), + this.depsOptimizer?.close(), + (async () => { + while (this._pendingRequests.size > 0) { + await Promise.allSettled( + [...this._pendingRequests.values()].map( + (pending) => pending.request, + ), + ) + } + })(), + ]) + } + + /** + * Calling `await environment.waitForRequestsIdle(id)` will wait until all static imports + * are processed after the first transformRequest call. If called from a load or transform + * plugin hook, the id needs to be passed as a parameter to avoid deadlocks. + * Calling this function after the first static imports section of the module graph has been + * processed will resolve immediately. + * @experimental + */ + waitForRequestsIdle(ignoredId?: string): Promise { + return this._crawlEndFinder.waitForRequestsIdle(ignoredId) + } + + /** + * @internal + */ + _registerRequestProcessing(id: string, done: () => Promise): void { + this._crawlEndFinder.registerRequestProcessing(id, done) + } + /** + * @internal + * TODO: use waitForRequestsIdle in the optimizer instead of this function + */ + _onCrawlEnd(cb: () => void): void { + this._onCrawlEndCallbacks.push(cb) + } +} + +function invalidateModule( + environment: DevEnvironment, + m: { + path: string + message?: string + }, +) { + const mod = environment.moduleGraph.urlToModuleMap.get(m.path) + if ( + mod && + mod.isSelfAccepting && + mod.lastHMRTimestamp > 0 && + !mod.lastHMRInvalidationReceived + ) { + mod.lastHMRInvalidationReceived = true + environment.logger.info( + colors.yellow(`hmr invalidate `) + + colors.dim(m.path) + + (m.message ? ` ${m.message}` : ''), + { timestamp: true }, + ) + const file = getShortName(mod.file!, environment.config.root) + updateModules( + environment, + file, + [...mod.importers], + mod.lastHMRTimestamp, + true, + ) + } +} + +const callCrawlEndIfIdleAfterMs = 50 + +interface CrawlEndFinder { + registerRequestProcessing: (id: string, done: () => Promise) => void + waitForRequestsIdle: (ignoredId?: string) => Promise + cancel: () => void +} + +function setupOnCrawlEnd(onCrawlEnd: () => void): CrawlEndFinder { + const registeredIds = new Set() + const seenIds = new Set() + const onCrawlEndPromiseWithResolvers = promiseWithResolvers() + + let timeoutHandle: NodeJS.Timeout | undefined + + let cancelled = false + function cancel() { + cancelled = true + } + + let crawlEndCalled = false + function callOnCrawlEnd() { + if (!cancelled && !crawlEndCalled) { + crawlEndCalled = true + onCrawlEnd() + } + onCrawlEndPromiseWithResolvers.resolve() + } + + function registerRequestProcessing( + id: string, + done: () => Promise, + ): void { + if (!seenIds.has(id)) { + seenIds.add(id) + registeredIds.add(id) + done() + .catch(() => {}) + .finally(() => markIdAsDone(id)) + } + } + + function waitForRequestsIdle(ignoredId?: string): Promise { + if (ignoredId) { + seenIds.add(ignoredId) + markIdAsDone(ignoredId) + } + return onCrawlEndPromiseWithResolvers.promise + } + + function markIdAsDone(id: string): void { + if (registeredIds.has(id)) { + registeredIds.delete(id) + checkIfCrawlEndAfterTimeout() + } + } + + function checkIfCrawlEndAfterTimeout() { + if (cancelled || registeredIds.size > 0) return + + if (timeoutHandle) clearTimeout(timeoutHandle) + timeoutHandle = setTimeout( + callOnCrawlEndWhenIdle, + callCrawlEndIfIdleAfterMs, + ) + } + async function callOnCrawlEndWhenIdle() { + if (cancelled || registeredIds.size > 0) return + callOnCrawlEnd() + } + + return { + registerRequestProcessing, + waitForRequestsIdle, + cancel, + } +} diff --git a/packages/vite/src/node/server/environmentTransport.ts b/packages/vite/src/node/server/environmentTransport.ts new file mode 100644 index 00000000000000..4340c144adc615 --- /dev/null +++ b/packages/vite/src/node/server/environmentTransport.ts @@ -0,0 +1,38 @@ +import type { DevEnvironment } from './environment' + +export class RemoteEnvironmentTransport { + constructor( + private readonly options: { + send: (data: any) => void + onMessage: (handler: (data: any) => void) => void + }, + ) {} + + register(environment: DevEnvironment): void { + this.options.onMessage(async (data) => { + if (typeof data !== 'object' || !data || !data.__v) return + + const method = data.m as 'fetchModule' + const parameters = data.a as [string, string] + + try { + const result = await environment[method](...parameters) + this.options.send({ + __v: true, + r: result, + i: data.i, + }) + } catch (error) { + this.options.send({ + __v: true, + e: { + name: error.name, + message: error.message, + stack: error.stack, + }, + i: data.i, + }) + } + }) + } +} diff --git a/packages/vite/src/node/server/environments/nodeEnvironment.ts b/packages/vite/src/node/server/environments/nodeEnvironment.ts new file mode 100644 index 00000000000000..e4e76a1ba31eb2 --- /dev/null +++ b/packages/vite/src/node/server/environments/nodeEnvironment.ts @@ -0,0 +1,24 @@ +import type { ResolvedConfig } from '../../config' +import type { DevEnvironmentSetup } from '../environment' +import { DevEnvironment } from '../environment' +import { asyncFunctionDeclarationPaddingLineCount } from '../../../shared/utils' + +export function createNodeDevEnvironment( + name: string, + config: ResolvedConfig, + options: DevEnvironmentSetup, +): DevEnvironment { + return new DevEnvironment(name, config, { + ...options, + runner: { + processSourceMap(map) { + // this assumes that "new AsyncFunction" is used to create the module + return Object.assign({}, map, { + mappings: + ';'.repeat(asyncFunctionDeclarationPaddingLineCount) + map.mappings, + }) + }, + ...options?.runner, + }, + }) +} diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 040495673e8836..2d5b8484052d86 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -3,17 +3,26 @@ import path from 'node:path' import type { Server } from 'node:http' import { EventEmitter } from 'node:events' import colors from 'picocolors' -import type { CustomPayload, HMRPayload, Update } from 'types/hmrPayload' +import type { CustomPayload, HotPayload, Update } from 'types/hotPayload' import type { RollupError } from 'rollup' import { CLIENT_DIR } from '../constants' import { createDebugger, normalizePath } from '../utils' import type { InferCustomEventPayload, ViteDevServer } from '..' +import { getHookHandler } from '../plugins' import { isCSSRequest } from '../plugins/css' -import { getAffectedGlobModules } from '../plugins/importMetaGlob' import { isExplicitImportRequired } from '../plugins/importAnalysis' import { getEnvFilesForMode } from '../env' +import type { Environment } from '../environment' import { withTrailingSlash, wrapId } from '../../shared/utils' -import type { ModuleNode } from './moduleGraph' +import type { Plugin } from '../plugin' +import { + ignoreDeprecationWarnings, + warnFutureDeprecation, +} from '../deprecations' +import type { EnvironmentModuleNode } from './moduleGraph' +import type { ModuleNode } from './mixedModuleGraph' +import type { DevEnvironment } from './environment' +import { prepareError } from './middlewares/error' import { restartServerWithUrls } from '.' export const debugHmr = createDebugger('vite:hmr') @@ -31,10 +40,22 @@ export interface HmrOptions { timeout?: number overlay?: boolean server?: Server - /** @internal */ - channels?: HMRChannel[] } +export interface HotUpdateContext { + type: 'create' | 'update' | 'delete' + file: string + timestamp: number + modules: Array + read: () => string | Promise + server: ViteDevServer + environment: DevEnvironment +} + +/** + * @deprecated + * Used by handleHotUpdate for backward compatibility with mixed client and ssr moduleGraph + **/ export interface HmrContext { file: string timestamp: number @@ -44,31 +65,27 @@ export interface HmrContext { } interface PropagationBoundary { - boundary: ModuleNode - acceptedVia: ModuleNode + boundary: EnvironmentModuleNode + acceptedVia: EnvironmentModuleNode isWithinCircularImport: boolean } -export interface HMRBroadcasterClient { +export interface HotChannelClient { /** * Send event to the client */ - send(payload: HMRPayload): void + send(payload: HotPayload): void /** * Send custom event */ send(event: string, payload?: CustomPayload['data']): void } -export interface HMRChannel { - /** - * Unique channel name - */ - name: string +export interface HotChannel { /** * Broadcast events to all clients */ - send(payload: HMRPayload): void + send(payload: HotPayload): void /** * Send custom event */ @@ -80,7 +97,7 @@ export interface HMRChannel { event: T, listener: ( data: InferCustomEventPayload, - client: HMRBroadcasterClient, + client: HotChannelClient, ...args: any[] ) => void, ): void @@ -99,30 +116,60 @@ export interface HMRChannel { close(): void } -export interface HMRBroadcaster extends Omit { - /** - * All registered channels. Always has websocket channel. - */ - readonly channels: HMRChannel[] - /** - * Add a new third-party channel. - */ - addChannel(connection: HMRChannel): HMRBroadcaster - close(): Promise -} - export function getShortName(file: string, root: string): string { return file.startsWith(withTrailingSlash(root)) ? path.posix.relative(root, file) : file } +export function getSortedPluginsByHotUpdateHook( + plugins: readonly Plugin[], +): Plugin[] { + const sortedPlugins: Plugin[] = [] + // Use indexes to track and insert the ordered plugins directly in the + // resulting array to avoid creating 3 extra temporary arrays per hook + let pre = 0, + normal = 0, + post = 0 + for (const plugin of plugins) { + const hook = plugin['hotUpdate'] ?? plugin['handleHotUpdate'] + if (hook) { + if (typeof hook === 'object') { + if (hook.order === 'pre') { + sortedPlugins.splice(pre++, 0, plugin) + continue + } + if (hook.order === 'post') { + sortedPlugins.splice(pre + normal + post++, 0, plugin) + continue + } + } + sortedPlugins.splice(pre + normal++, 0, plugin) + } + } + + return sortedPlugins +} + +const sortedHotUpdatePluginsCache = new WeakMap() +function getSortedHotUpdatePlugins(environment: Environment): Plugin[] { + let sortedPlugins = sortedHotUpdatePluginsCache.get(environment) as Plugin[] + if (!sortedPlugins) { + sortedPlugins = getSortedPluginsByHotUpdateHook(environment.plugins) + sortedHotUpdatePluginsCache.set(environment, sortedPlugins) + } + return sortedPlugins +} + export async function handleHMRUpdate( type: 'create' | 'delete' | 'update', file: string, server: ViteDevServer, ): Promise { - const { hot, config, moduleGraph } = server + const { config } = server + const mixedModuleGraph = ignoreDeprecationWarnings(() => server.moduleGraph) + + const environments = Object.values(server.environments) const shortFile = getShortName(file, config.root) const isConfig = file === config.configFile @@ -156,80 +203,222 @@ export async function handleHMRUpdate( // (dev only) the client itself cannot be hot updated. if (file.startsWith(withTrailingSlash(normalizedClientDir))) { - hot.send({ - type: 'full-reload', - path: '*', - triggeredBy: path.resolve(config.root, file), - }) + environments.forEach(({ hot }) => + hot.send({ + type: 'full-reload', + path: '*', + triggeredBy: path.resolve(config.root, file), + }), + ) return } - const mods = new Set(moduleGraph.getModulesByFile(file)) - if (type === 'create') { - for (const mod of moduleGraph._hasResolveFailedErrorModules) { - mods.add(mod) - } - } - if (type === 'create' || type === 'delete') { - for (const mod of getAffectedGlobModules(file, server)) { - mods.add(mod) - } - } - - // check if any plugin wants to perform custom HMR handling const timestamp = Date.now() - const hmrContext: HmrContext = { + const contextMeta = { + type, file, timestamp, - modules: [...mods], read: () => readModifiedFile(file), server, } + const hotMap = new Map< + Environment, + { context: HotUpdateContext; error?: Error } + >() + + for (const environment of Object.values(server.environments)) { + const mods = new Set(environment.moduleGraph.getModulesByFile(file)) + if (type === 'create') { + for (const mod of environment.moduleGraph._hasResolveFailedErrorModules) { + mods.add(mod) + } + } + const context = { + ...contextMeta, + modules: [...mods], + // later on hotUpdate will be called for each runtime with a new hotContext + environment, + } + hotMap.set(environment, { context }) + } - if (type === 'update') { - for (const hook of config.getSortedPluginHooks('handleHotUpdate')) { - const filteredModules = await hook(hmrContext) - if (filteredModules) { - hmrContext.modules = filteredModules + const mixedMods = new Set(mixedModuleGraph.getModulesByFile(file)) + + const mixedHmrContext: HmrContext = { + ...contextMeta, + modules: [...mixedMods], + } + + const clientHotContext = hotMap.get(server.environments.client)!.context + const ssrHotContext = hotMap.get(server.environments.ssr)?.context + try { + for (const plugin of getSortedHotUpdatePlugins( + server.environments.client, + )) { + if (plugin.hotUpdate) { + const filteredModules = await getHookHandler(plugin.hotUpdate)( + clientHotContext, + ) + if (filteredModules) { + clientHotContext.modules = filteredModules + // Invalidate the hmrContext to force compat modules to be updated + mixedHmrContext.modules = mixedHmrContext.modules.filter( + (mixedMod) => + filteredModules.find((mod) => mixedMod.id === mod.id) || + ssrHotContext?.modules.find( + (ssrMod) => ssrMod.id === mixedMod.id, + ), + ) + mixedHmrContext.modules.push( + ...filteredModules + .filter( + (mod) => + !mixedHmrContext.modules.find( + (mixedMod) => mixedMod.id === mod.id, + ), + ) + .map((mod) => + mixedModuleGraph.getBackwardCompatibleModuleNode(mod), + ), + ) + } + } else if (type === 'update') { + warnFutureDeprecation( + config, + 'pluginHookHandleHotUpdate', + `Used in plugin "${plugin.name}".`, + false, + ) + // later on, we'll need: if (runtime === 'client') + // Backward compatibility with mixed client and ssr moduleGraph + const filteredModules = await getHookHandler(plugin.handleHotUpdate!)( + mixedHmrContext, + ) + if (filteredModules) { + mixedHmrContext.modules = filteredModules + clientHotContext.modules = clientHotContext.modules.filter((mod) => + filteredModules.find((mixedMod) => mod.id === mixedMod.id), + ) + clientHotContext.modules.push( + ...(filteredModules + .filter( + (mixedMod) => + !clientHotContext.modules.find( + (mod) => mod.id === mixedMod.id, + ), + ) + .map((mixedMod) => mixedMod._clientModule) + .filter(Boolean) as EnvironmentModuleNode[]), + ) + if (ssrHotContext) { + ssrHotContext.modules = ssrHotContext.modules.filter((mod) => + filteredModules.find((mixedMod) => mod.id === mixedMod.id), + ) + ssrHotContext.modules.push( + ...(filteredModules + .filter( + (mixedMod) => + !ssrHotContext.modules.find( + (mod) => mod.id === mixedMod.id, + ), + ) + .map((mixedMod) => mixedMod._ssrModule) + .filter(Boolean) as EnvironmentModuleNode[]), + ) + } + } } } + } catch (error) { + hotMap.get(server.environments.client)!.error = error } - if (!hmrContext.modules.length) { - // html file cannot be hot updated - if (file.endsWith('.html')) { - config.logger.info(colors.green(`page reload `) + colors.dim(shortFile), { - clear: true, - timestamp: true, - }) - hot.send({ - type: 'full-reload', - path: config.server.middlewareMode - ? '*' - : '/' + normalizePath(path.relative(config.root, file)), + for (const environment of Object.values(server.environments)) { + if (environment.name === 'client') continue + const hot = hotMap.get(environment)! + try { + for (const plugin of getSortedHotUpdatePlugins(environment)) { + if (plugin.hotUpdate) { + const filteredModules = await getHookHandler(plugin.hotUpdate)( + hot.context, + ) + if (filteredModules) { + hot.context.modules = filteredModules + } + } + } + } catch (error) { + hot.error = error + } + } + + async function hmr(environment: DevEnvironment) { + try { + const { context, error } = hotMap.get(environment)! + if (error) { + throw error + } + if (!context.modules.length) { + // html file cannot be hot updated + if (file.endsWith('.html')) { + environment.logger.info( + colors.green(`page reload `) + colors.dim(shortFile), + { + clear: true, + timestamp: true, + }, + ) + environment.hot.send({ + type: 'full-reload', + path: config.server.middlewareMode + ? '*' + : '/' + normalizePath(path.relative(config.root, file)), + }) + } else { + // loaded but not in the module graph, probably not js + debugHmr?.( + `(${environment.name}) [no modules matched] ${colors.dim(shortFile)}`, + ) + } + return + } + + updateModules(environment, shortFile, context.modules, timestamp) + } catch (err) { + environment.hot.send({ + type: 'error', + err: prepareError(err), }) - } else { - // loaded but not in the module graph, probably not js - debugHmr?.(`[no modules matched] ${colors.dim(shortFile)}`) } - return } - updateModules(shortFile, hmrContext.modules, timestamp, server) + const hotUpdateEnvironments = + server.config.server.hotUpdateEnvironments ?? + ((server, hmr) => { + // Run HMR in parallel for all environments by default + return Promise.all( + Object.values(server.environments).map((environment) => + hmr(environment), + ), + ) + }) + + await hotUpdateEnvironments(server, hmr) } type HasDeadEnd = boolean export function updateModules( + environment: DevEnvironment, file: string, - modules: ModuleNode[], + modules: EnvironmentModuleNode[], timestamp: number, - { config, hot, moduleGraph }: ViteDevServer, afterInvalidation?: boolean, ): void { + const { hot, config } = environment const updates: Update[] = [] - const invalidatedModules = new Set() - const traversedModules = new Set() + const invalidatedModules = new Set() + const traversedModules = new Set() // Modules could be empty if a root module is invalidated via import.meta.hot.invalidate() let needFullReload: HasDeadEnd = modules.length === 0 @@ -237,7 +426,12 @@ export function updateModules( const boundaries: PropagationBoundary[] = [] const hasDeadEnd = propagateUpdate(mod, traversedModules, boundaries) - moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true) + environment.moduleGraph.invalidateModule( + mod, + invalidatedModules, + timestamp, + true, + ) if (needFullReload) { continue @@ -260,9 +454,6 @@ export function updateModules( ? isExplicitImportRequired(acceptedVia.url) : false, isWithinCircularImport, - // browser modules are invalidated by changing ?t= query, - // but in ssr we control the module system, so we can directly remove them form cache - ssrInvalidates: getSSRInvalidatedImporters(acceptedVia), }), ), ) @@ -273,7 +464,7 @@ export function updateModules( typeof needFullReload === 'string' ? colors.dim(` (${needFullReload})`) : '' - config.logger.info( + environment.logger.info( colors.green(`page reload `) + colors.dim(file) + reason, { clear: !afterInvalidation, timestamp: true }, ) @@ -289,7 +480,7 @@ export function updateModules( return } - config.logger.info( + environment.logger.info( colors.green(`hmr update `) + colors.dim([...new Set(updates.map((u) => u.path))].join(', ')), { clear: !afterInvalidation, timestamp: true }, @@ -300,32 +491,6 @@ export function updateModules( }) } -function populateSSRImporters( - module: ModuleNode, - timestamp: number, - seen: Set = new Set(), -) { - module.ssrImportedModules.forEach((importer) => { - if (seen.has(importer)) { - return - } - if ( - importer.lastHMRTimestamp === timestamp || - importer.lastInvalidationTimestamp === timestamp - ) { - seen.add(importer) - populateSSRImporters(importer, timestamp, seen) - } - }) - return seen -} - -function getSSRInvalidatedImporters(module: ModuleNode) { - return [...populateSSRImporters(module, module.lastHMRTimestamp)].map( - (m) => m.file!, - ) -} - function areAllImportsAccepted( importedBindings: Set, acceptedExports: Set, @@ -339,10 +504,10 @@ function areAllImportsAccepted( } function propagateUpdate( - node: ModuleNode, - traversedModules: Set, + node: EnvironmentModuleNode, + traversedModules: Set, boundaries: PropagationBoundary[], - currentChain: ModuleNode[] = [node], + currentChain: EnvironmentModuleNode[] = [node], ): HasDeadEnd { if (traversedModules.has(node)) { return false @@ -454,10 +619,10 @@ function propagateUpdate( * @param traversedModules The set of modules that have traversed */ function isNodeWithinCircularImports( - node: ModuleNode, - nodeChain: ModuleNode[], - currentChain: ModuleNode[] = [node], - traversedModules = new Set(), + node: EnvironmentModuleNode, + nodeChain: EnvironmentModuleNode[], + currentChain: EnvironmentModuleNode[] = [node], + traversedModules = new Set(), ): boolean { // To help visualize how each parameters work, imagine this import graph: // @@ -527,8 +692,8 @@ function isNodeWithinCircularImports( } export function handlePrunedModules( - mods: Set, - { hot }: ViteDevServer, + mods: Set, + { hot }: DevEnvironment, ): void { // update the disposed modules' hmr timestamp // since if it's re-imported, it should re-apply side effects @@ -715,70 +880,20 @@ async function readModifiedFile(file: string): Promise { } } -export function createHMRBroadcaster(): HMRBroadcaster { - const channels: HMRChannel[] = [] - const readyChannels = new WeakSet() - const broadcaster: HMRBroadcaster = { - get channels() { - return [...channels] - }, - addChannel(channel) { - if (channels.some((c) => c.name === channel.name)) { - throw new Error(`HMR channel "${channel.name}" is already defined.`) - } - channels.push(channel) - return broadcaster - }, - on(event: string, listener: (...args: any[]) => any) { - // emit connection event only when all channels are ready - if (event === 'connection') { - // make a copy so we don't wait for channels that might be added after this is triggered - const channels = this.channels - channels.forEach((channel) => - channel.on('connection', () => { - readyChannels.add(channel) - if (channels.every((c) => readyChannels.has(c))) { - listener() - } - }), - ) - return - } - channels.forEach((channel) => channel.on(event, listener)) - return - }, - off(event, listener) { - channels.forEach((channel) => channel.off(event, listener)) - return - }, - send(...args: any[]) { - channels.forEach((channel) => channel.send(...(args as [any]))) - }, - listen() { - channels.forEach((channel) => channel.listen()) - }, - close() { - return Promise.all(channels.map((channel) => channel.close())) - }, - } - return broadcaster -} - -export interface ServerHMRChannel extends HMRChannel { +export interface ServerHotChannel extends HotChannel { api: { innerEmitter: EventEmitter outsideEmitter: EventEmitter } } -export function createServerHMRChannel(): ServerHMRChannel { +export function createServerHotChannel(): ServerHotChannel { const innerEmitter = new EventEmitter() const outsideEmitter = new EventEmitter() return { - name: 'ssr', send(...args: any[]) { - let payload: HMRPayload + let payload: HotPayload if (typeof args[0] === 'string') { payload = { type: 'custom', @@ -795,7 +910,7 @@ export function createServerHMRChannel(): ServerHMRChannel { }, on: ((event: string, listener: () => unknown) => { innerEmitter.on(event, listener) - }) as ServerHMRChannel['on'], + }) as ServerHotChannel['on'], close() { innerEmitter.removeAllListeners() outsideEmitter.removeAllListeners() @@ -809,3 +924,17 @@ export function createServerHMRChannel(): ServerHMRChannel { }, } } + +export function createNoopHotChannel(): HotChannel { + function noop() { + // noop + } + + return { + send: noop, + on: noop, + off: noop, + listen: noop, + close: noop, + } +} diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 7590bafcc3a88e..5fea53f9655d02 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -14,8 +14,7 @@ import type { FSWatcher, WatchOptions } from 'dep-types/chokidar' import type { Connect } from 'dep-types/connect' import launchEditorMiddleware from 'launch-editor-middleware' import type { SourceMap } from 'rollup' -import picomatch from 'picomatch' -import type { Matcher } from 'picomatch' +import type { ModuleRunner } from 'vite/module-runner' import type { CommonServerOptions } from '../http' import { httpServerStart, @@ -24,7 +23,7 @@ import { setClientErrorHandler, } from '../http' import type { InlineConfig, ResolvedConfig } from '../config' -import { isDepsOptimizerEnabled, resolveConfig } from '../config' +import { resolveConfig } from '../config' import { diffDnsOrderChange, isInNodeModules, @@ -32,7 +31,6 @@ import { isParentDirectory, mergeConfig, normalizePath, - promiseWithResolvers, resolveHostname, resolveServerUrls, } from '../utils' @@ -41,12 +39,13 @@ import { ssrLoadModule } from '../ssr/ssrModuleLoader' import { ssrFixStacktrace, ssrRewriteStacktrace } from '../ssr/ssrStacktrace' import { ssrTransform } from '../ssr/ssrTransform' import { ERR_OUTDATED_OPTIMIZED_DEP } from '../plugins/optimizedDeps' -import { getDepsOptimizer, initDepsOptimizer } from '../optimizer' +import { reloadOnTsconfigChange } from '../plugins/esbuild' import { bindCLIShortcuts } from '../shortcuts' import type { BindCLIShortcutsOptions } from '../shortcuts' import { CLIENT_DIR, DEFAULT_DEV_PORT } from '../constants' import type { Logger } from '../logger' import { printServerUrls } from '../logger' +import { warnFutureDeprecation } from '../deprecations' import { createNoopWatcher, getResolvedOutDirs, @@ -55,8 +54,6 @@ import { } from '../watch' import { initPublicFiles } from '../publicDir' import { getEnvFilesForMode } from '../env' -import type { FetchResult } from '../../runtime/types' -import { ssrFetchModule } from '../ssr/ssrFetchModule' import type { PluginContainer } from './pluginContainer' import { ERR_CLOSED_SERVER, createPluginContainer } from './pluginContainer' import type { WebSocketServer } from './ws' @@ -78,23 +75,20 @@ import { serveStaticMiddleware, } from './middlewares/static' import { timeMiddleware } from './middlewares/time' -import type { ModuleNode } from './moduleGraph' -import { ModuleGraph } from './moduleGraph' +import type { EnvironmentModuleNode } from './moduleGraph' +import { ModuleGraph } from './mixedModuleGraph' +import type { ModuleNode } from './mixedModuleGraph' import { notFoundMiddleware } from './middlewares/notFound' -import { errorMiddleware, prepareError } from './middlewares/error' -import type { HMRBroadcaster, HmrOptions } from './hmr' -import { - createHMRBroadcaster, - createServerHMRChannel, - getShortName, - handleHMRUpdate, - updateModules, -} from './hmr' +import { errorMiddleware } from './middlewares/error' +import type { HmrOptions } from './hmr' +import { createServerHotChannel, handleHMRUpdate, updateModules } from './hmr' import { openBrowser as _openBrowser } from './openBrowser' import type { TransformOptions, TransformResult } from './transformRequest' import { transformRequest } from './transformRequest' import { searchForWorkspaceRoot } from './searchRoot' import { warmupFiles } from './warmup' +import { DevEnvironment } from './environment' +import { createNodeDevEnvironment } from './environments/nodeEnvironment' export interface ServerOptions extends CommonServerOptions { /** @@ -109,6 +103,7 @@ export interface ServerOptions extends CommonServerOptions { /** * Warm-up files to transform and cache the results in advance. This improves the * initial page load during server starts and prevents transform waterfalls. + * @deprecated use dev.warmup / environment.ssr.dev.warmup */ warmup?: { /** @@ -152,6 +147,7 @@ export interface ServerOptions extends CommonServerOptions { /** * Pre-transform known direct imports * @default true + * @deprecated use dev.preTransformRequests */ preTransformRequests?: boolean /** @@ -161,10 +157,19 @@ export interface ServerOptions extends CommonServerOptions { * By default, it excludes all paths containing `node_modules`. You can pass `false` to * disable this behavior, or, for full control, a function that takes the source path and * sourcemap path and returns whether to ignore the source path. + * @deprecated use dev.sourcemapIgnoreList */ sourcemapIgnoreList?: | false | ((sourcePath: string, sourcemapPath: string) => boolean) + /** + * Run HMR tasks, by default the HMR propagation is done in parallel for all environments + * @experimental + */ + hotUpdateEnvironments?: ( + server: ViteDevServer, + hmr: (environment: DevEnvironment) => Promise, + ) => Promise } export interface ResolvedServerOptions @@ -247,23 +252,22 @@ export interface ViteDevServer { watcher: FSWatcher /** * web socket server with `send(payload)` method + * @deprecated use `environment.hot` instead */ ws: WebSocketServer - /** - * HMR broadcaster that can be used to send custom HMR messages to the client - * - * Always sends a message to at least a WebSocket client. Any third party can - * add a channel to the broadcaster to process messages - * @deprecated will be replaced with the environment api in v6. - */ - hot: HMRBroadcaster /** * Rollup plugin container that can run plugin hooks on a given file + * @deprecated use `environment.pluginContainer` instead */ pluginContainer: PluginContainer + /** + * Module execution environments attached to the Vite server. + */ + environments: Record<'client' | 'ssr' | (string & {}), DevEnvironment> /** * Module graph that tracks the import relationships, url to file mapping * and hmr state. + * @deprecated use `environment.moduleGraph` instead */ moduleGraph: ModuleGraph /** @@ -274,6 +278,7 @@ export interface ViteDevServer { /** * Programmatically resolve, load and transform a URL and get the result * without going through the http request pipeline. + * @deprecated use environment.transformRequest */ transformRequest( url: string, @@ -283,6 +288,7 @@ export interface ViteDevServer { * Same as `transformRequest` but only warm up the URLs so the next request * will already be cached. The function will never throw as it handles and * reports errors internally. + * @deprecated use environment.warmupRequest */ warmupRequest(url: string, options?: TransformOptions): Promise /** @@ -295,6 +301,7 @@ export interface ViteDevServer { ): Promise /** * Transform module code into SSR format. + * TODO: expose this to any environment? */ ssrTransform( code: string, @@ -309,11 +316,6 @@ export interface ViteDevServer { url: string, opts?: { fixStacktrace?: boolean }, ): Promise> - /** - * Fetch information about the module for Vite SSR runtime. - * @experimental - */ - ssrFetchModule(id: string, importer?: string): Promise /** * Returns a fixed version of the given stack */ @@ -327,6 +329,11 @@ export interface ViteDevServer { * API to retrieve the module to be reloaded. If `hmr` is false, this is a no-op. */ reloadModule(module: ModuleNode): Promise + /** + * Triggers HMR for an environment module in the module graph. + * If `hmr` is false, this is a no-op. + */ + reloadEnvironmentModule(module: EnvironmentModuleNode): Promise /** * Start the server. */ @@ -349,7 +356,6 @@ export interface ViteDevServer { * @param forceOptimize - force the optimizer to re-bundle, same as --force cli flag */ restart(forceOptimize?: boolean): Promise - /** * Open browser */ @@ -360,24 +366,13 @@ export interface ViteDevServer { * passed as a parameter to avoid deadlocks. Calling this function after the first * static imports section of the module graph has been processed will resolve immediately. * @experimental + * @deprecated use environment.waitForRequestsIdle() */ waitForRequestsIdle: (ignoredId?: string) => Promise - /** - * @internal - */ - _registerRequestProcessing: (id: string, done: () => Promise) => void - /** - * @internal - */ - _onCrawlEnd(cb: () => void): void /** * @internal */ _setInternalServer(server: ViteDevServer): void - /** - * @internal - */ - _importGlobMap: Map /** * @internal */ @@ -386,21 +381,6 @@ export interface ViteDevServer { * @internal */ _forceOptimizeOnRestart: boolean - /** - * @internal - */ - _pendingRequests: Map< - string, - { - request: Promise - timestamp: number - abort: () => void - } - > - /** - * @internal - */ - _fsDenyGlob: Matcher /** * @internal */ @@ -413,6 +393,10 @@ export interface ViteDevServer { * @internal */ _configServerPort?: number | undefined + /** + * @internal + */ + _ssrCompatModuleRunner?: ModuleRunner } export interface ResolvedServerUrls { @@ -464,12 +448,7 @@ export async function _createServer( : await resolveHttpServer(serverConfig, middlewares, httpsOptions) const ws = createWebSocketServer(httpServer, config, httpsOptions) - const hot = createHMRBroadcaster() - .addChannel(ws) - .addChannel(createServerHMRChannel()) - if (typeof config.server.hmr === 'object' && config.server.hmr.channels) { - config.server.hmr.channels.forEach((channel) => hot.addChannel(channel)) - } + const ssrHotChannel = createServerHotChannel() const publicFiles = await initPublicFilesPromise const { publicDir } = config @@ -495,40 +474,72 @@ export async function _createServer( ) as FSWatcher) : createNoopWatcher(resolvedWatchOptions) - const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) => - container.resolveId(url, undefined, { ssr }), - ) + const environments: Record = {} + + const client_createEnvironment = + config.environments.client?.dev?.createEnvironment ?? + ((name: string, config: ResolvedConfig) => + new DevEnvironment(name, config, { hot: ws, watcher })) + + environments.client = await client_createEnvironment('client', config) + + const ssr_createEnvironment = + config.environments.ssr?.dev?.createEnvironment ?? + ((name: string, config: ResolvedConfig) => + createNodeDevEnvironment(name, config, { hot: ssrHotChannel, watcher })) + + environments.ssr = await ssr_createEnvironment('ssr', config) + + for (const [name, EnvironmentOptions] of Object.entries( + config.environments, + )) { + // TODO: move client and ssr inside the loop? + if (name !== 'client' && name !== 'ssr') { + const createEnvironment = + EnvironmentOptions.dev?.createEnvironment ?? + ((name: string, config: ResolvedConfig) => + new DevEnvironment(name, config, { + hot: ws, // TODO: what should we use here? + })) + environments[name] = await createEnvironment(name, config) + } + } + + for (const environment of Object.values(environments)) { + await environment.init() + } + + // Backward compatibility + + let moduleGraph = new ModuleGraph({ + client: () => environments.client.moduleGraph, + ssr: () => environments.ssr.moduleGraph, + }) + const pluginContainer = createPluginContainer(environments) - const container = await createPluginContainer(config, moduleGraph, watcher) const closeHttpServer = createServerCloseFn(httpServer) let exitProcess: () => void const devHtmlTransformFn = createDevHtmlTransformFn(config) - const onCrawlEndCallbacks: (() => void)[] = [] - const crawlEndFinder = setupOnCrawlEnd(() => { - onCrawlEndCallbacks.forEach((cb) => cb()) - }) - function waitForRequestsIdle(ignoredId?: string): Promise { - return crawlEndFinder.waitForRequestsIdle(ignoredId) - } - function _registerRequestProcessing(id: string, done: () => Promise) { - crawlEndFinder.registerRequestProcessing(id, done) - } - function _onCrawlEnd(cb: () => void) { - onCrawlEndCallbacks.push(cb) - } - let server: ViteDevServer = { config, middlewares, httpServer, watcher, - pluginContainer: container, ws, - hot, - moduleGraph, + + environments, + pluginContainer, + get moduleGraph() { + warnFutureDeprecation(config, 'serverModuleGraph') + return moduleGraph + }, + set moduleGraph(graph) { + moduleGraph = graph + }, + resolvedUrls: null, // will be set on listen ssrTransform( code: string, @@ -538,12 +549,24 @@ export async function _createServer( ) { return ssrTransform(code, inMap, url, originalCode, server.config) }, + // environment.transformRequest and .warmupRequest don't take an options param for now, + // so the logic and error handling needs to be duplicated here. + // The only param in options that could be important is `html`, but we may remove it as + // that is part of the internal control flow for the vite dev server to be able to bail + // out and do the html fallback transformRequest(url, options) { - return transformRequest(url, server, options) + warnFutureDeprecation( + config, + 'serverTransformRequest', + 'server.transformRequest() is deprecated. Use environment.transformRequest() instead.', + ) + const environment = server.environments[options?.ssr ? 'ssr' : 'client'] + return transformRequest(environment, url, options) }, async warmupRequest(url, options) { try { - await transformRequest(url, server, options) + const environment = server.environments[options?.ssr ? 'ssr' : 'client'] + await transformRequest(environment, url, options) } catch (e) { if ( e?.code === ERR_OUTDATED_OPTIMIZED_DEP || @@ -563,6 +586,8 @@ export async function _createServer( return devHtmlTransformFn(server, url, html, originalUrl) }, async ssrLoadModule(url, opts?: { fixStacktrace?: boolean }) { + warnFutureDeprecation(config, 'ssrLoadModule') + return ssrLoadModule( url, server, @@ -571,18 +596,33 @@ export async function _createServer( opts?.fixStacktrace, ) }, - async ssrFetchModule(url: string, importer?: string) { - return ssrFetchModule(server, url, importer) - }, ssrFixStacktrace(e) { - ssrFixStacktrace(e, moduleGraph) + ssrFixStacktrace(e, server.environments.ssr.moduleGraph) }, ssrRewriteStacktrace(stack: string) { - return ssrRewriteStacktrace(stack, moduleGraph) + return ssrRewriteStacktrace(stack, server.environments.ssr.moduleGraph) }, async reloadModule(module) { if (serverConfig.hmr !== false && module.file) { - updateModules(module.file, [module], Date.now(), server) + // TODO: Should we also update the node moduleGraph for backward compatibility? + const environmentModule = (module._clientModule ?? module._ssrModule)! + updateModules( + environments[environmentModule.environment]!, + module.file, + [environmentModule], + Date.now(), + ) + } + }, + async reloadEnvironmentModule(module) { + // TODO: Should this be reloadEnvironmentModule(environment, module) ? + if (serverConfig.hmr !== false && module.file) { + updateModules( + environments[module.environment]!, + module.file, + [module], + Date.now(), + ) } }, async listen(port?: number, isRestart?: boolean) { @@ -649,28 +689,17 @@ export async function _createServer( process.stdin.off('end', exitProcess) } } + await Promise.allSettled([ watcher.close(), - hot.close(), - container.close(), - crawlEndFinder?.cancel(), - getDepsOptimizer(server.config)?.close(), - getDepsOptimizer(server.config, true)?.close(), + ws.close(), + Promise.allSettled( + Object.values(server.environments).map((environment) => + environment.close(), + ), + ), closeHttpServer(), ]) - // Await pending requests. We throw early in transformRequest - // and in hooks if the server is closing for non-ssr requests, - // so the import analysis plugin stops pre-transforming static - // imports and this block is resolved sooner. - // During SSR, we let pending requests finish to avoid exposing - // the server closed error to the users. - while (server._pendingRequests.size > 0) { - await Promise.allSettled( - [...server._pendingRequests.values()].map( - (pending) => pending.request, - ), - ) - } server.resolvedUrls = null }, printUrls() { @@ -702,9 +731,9 @@ export async function _createServer( return server._restartPromise }, - waitForRequestsIdle, - _registerRequestProcessing, - _onCrawlEnd, + waitForRequestsIdle(ignoredId?: string): Promise { + return environments.client.waitForRequestsIdle(ignoredId) + }, _setInternalServer(_server: ViteDevServer) { // Rebind internal the server variable so functions reference the user @@ -712,22 +741,7 @@ export async function _createServer( server = _server }, _restartPromise: null, - _importGlobMap: new Map(), _forceOptimizeOnRestart: false, - _pendingRequests: new Map(), - _fsDenyGlob: picomatch( - // matchBase: true does not work as it's documented - // https://github.com/micromatch/picomatch/issues/89 - // convert patterns without `/` on our side for now - config.server.fs.deny.map((pattern) => - pattern.includes('/') ? pattern : `**/${pattern}`, - ), - { - matchBase: false, - nocase: true, - dot: true, - }, - ), _shortcutsOptions: undefined, } @@ -761,45 +775,53 @@ export async function _createServer( file: string, ) => { if (serverConfig.hmr !== false) { - try { - await handleHMRUpdate(type, file, server) - } catch (err) { - hot.send({ - type: 'error', - err: prepareError(err), - }) - } + await handleHMRUpdate(type, file, server) } } const onFileAddUnlink = async (file: string, isUnlink: boolean) => { file = normalizePath(file) - await container.watchChange(file, { event: isUnlink ? 'delete' : 'create' }) + reloadOnTsconfigChange(server, file) + + await pluginContainer.watchChange(file, { + event: isUnlink ? 'delete' : 'create', + }) if (publicDir && publicFiles) { if (file.startsWith(publicDir)) { const path = file.slice(publicDir.length) publicFiles[isUnlink ? 'delete' : 'add'](path) if (!isUnlink) { - const moduleWithSamePath = await moduleGraph.getModuleByUrl(path) + const clientModuleGraph = server.environments.client.moduleGraph + const moduleWithSamePath = + await clientModuleGraph.getModuleByUrl(path) const etag = moduleWithSamePath?.transformResult?.etag if (etag) { // The public file should win on the next request over a module with the // same path. Prevent the transform etag fast path from serving the module - moduleGraph.etagToModuleMap.delete(etag) + clientModuleGraph.etagToModuleMap.delete(etag) } } } } - if (isUnlink) moduleGraph.onFileDelete(file) + if (isUnlink) { + // invalidate module graph cache on file change + for (const environment of Object.values(server.environments)) { + environment.moduleGraph.onFileDelete(file) + } + } await onHMRUpdate(isUnlink ? 'delete' : 'create', file) } watcher.on('change', async (file) => { file = normalizePath(file) - await container.watchChange(file, { event: 'update' }) + reloadOnTsconfigChange(server, file) + + await pluginContainer.watchChange(file, { event: 'update' }) // invalidate module graph cache on file change - moduleGraph.onFileChange(file) + for (const environment of Object.values(server.environments)) { + environment.moduleGraph.onFileChange(file) + } await onHMRUpdate('update', file) }) @@ -812,32 +834,6 @@ export async function _createServer( onFileAddUnlink(file, true) }) - hot.on('vite:invalidate', async ({ path, message }) => { - const mod = moduleGraph.urlToModuleMap.get(path) - if ( - mod && - mod.isSelfAccepting && - mod.lastHMRTimestamp > 0 && - !mod.lastHMRInvalidationReceived - ) { - mod.lastHMRInvalidationReceived = true - config.logger.info( - colors.yellow(`hmr invalidate `) + - colors.dim(path) + - (message ? ` ${message}` : ''), - { timestamp: true }, - ) - const file = getShortName(mod.file!, config.root) - updateModules( - file, - [...mod.importers], - mod.lastHMRTimestamp, - server, - true, - ) - } - }) - if (!middlewareMode && httpServer) { httpServer.once('listening', () => { // update actual port since this may be different from initial value @@ -943,11 +939,18 @@ export async function _createServer( if (initingServer) return initingServer initingServer = (async function () { - await container.buildStart({}) - // start deps optimizer after all container plugins are ready - if (isDepsOptimizerEnabled(config, false)) { - await initDepsOptimizer(config, server) - } + // TODO: Build start should be called for all environments + // The ecosystem and our tests expects a single call. We need to + // check how to do this change to be backward compatible + await server.environments.client.pluginContainer.buildStart({}) + + await Promise.all( + Object.values(server.environments).map((environment) => + environment.depsOptimizer?.init(), + ), + ) + + // TODO: move warmup call inside environment init() warmupFiles(server) initingServer = undefined serverInited = true @@ -961,7 +964,7 @@ export async function _createServer( httpServer.listen = (async (port: number, ...args: any[]) => { try { // ensure ws server started - hot.listen() + Object.values(environments).forEach((e) => e.hot.listen()) await initServer() } catch (e) { httpServer.emit('error', e) @@ -971,7 +974,7 @@ export async function _createServer( }) as any } else { if (options.hotListen) { - hot.listen() + Object.values(environments).forEach((e) => e.hot.listen()) } await initServer() } @@ -1056,6 +1059,7 @@ export function resolveServerOptions( raw: ServerOptions | undefined, logger: Logger, ): ResolvedServerOptions { + // TODO: deprecated server options moved to the dev config const server: ResolvedServerOptions = { preTransformRequests: true, ...(raw as Omit), @@ -1140,7 +1144,7 @@ async function restartServer(server: ViteDevServer) { // server instance and set the user instance to be used in the new server. // This allows us to keep the same server instance for the user. { - let newServer = null + let newServer: ViteDevServer | null = null try { // delay ws server listen newServer = await _createServer(inlineConfig, { hotListen: false }) @@ -1176,7 +1180,7 @@ async function restartServer(server: ViteDevServer) { if (!middlewareMode) { await server.listen(port, true) } else { - server.hot.listen() + server.ws.listen() } logger.info('server restarted.', { timestamp: true }) @@ -1215,81 +1219,3 @@ export async function restartServerWithUrls( server.printUrls() } } - -const callCrawlEndIfIdleAfterMs = 50 - -interface CrawlEndFinder { - registerRequestProcessing: (id: string, done: () => Promise) => void - waitForRequestsIdle: (ignoredId?: string) => Promise - cancel: () => void -} - -function setupOnCrawlEnd(onCrawlEnd: () => void): CrawlEndFinder { - const registeredIds = new Set() - const seenIds = new Set() - const onCrawlEndPromiseWithResolvers = promiseWithResolvers() - - let timeoutHandle: NodeJS.Timeout | undefined - - let cancelled = false - function cancel() { - cancelled = true - } - - let crawlEndCalled = false - function callOnCrawlEnd() { - if (!cancelled && !crawlEndCalled) { - crawlEndCalled = true - onCrawlEnd() - } - onCrawlEndPromiseWithResolvers.resolve() - } - - function registerRequestProcessing( - id: string, - done: () => Promise, - ): void { - if (!seenIds.has(id)) { - seenIds.add(id) - registeredIds.add(id) - done() - .catch(() => {}) - .finally(() => markIdAsDone(id)) - } - } - - function waitForRequestsIdle(ignoredId?: string): Promise { - if (ignoredId) { - seenIds.add(ignoredId) - markIdAsDone(ignoredId) - } - return onCrawlEndPromiseWithResolvers.promise - } - - function markIdAsDone(id: string): void { - if (registeredIds.has(id)) { - registeredIds.delete(id) - checkIfCrawlEndAfterTimeout() - } - } - - function checkIfCrawlEndAfterTimeout() { - if (cancelled || registeredIds.size > 0) return - - if (timeoutHandle) clearTimeout(timeoutHandle) - timeoutHandle = setTimeout( - callOnCrawlEndWhenIdle, - callCrawlEndIfIdleAfterMs, - ) - } - async function callOnCrawlEndWhenIdle() { - if (cancelled || registeredIds.size > 0) return - callOnCrawlEnd() - } - - return { - registerRequestProcessing, - waitForRequestsIdle, - cancel, - } -} diff --git a/packages/vite/src/node/server/middlewares/error.ts b/packages/vite/src/node/server/middlewares/error.ts index 1d67f1aa55e4ed..d5c326e0185a6d 100644 --- a/packages/vite/src/node/server/middlewares/error.ts +++ b/packages/vite/src/node/server/middlewares/error.ts @@ -2,7 +2,7 @@ import colors from 'picocolors' import type { RollupError } from 'rollup' import type { Connect } from 'dep-types/connect' import strip from 'strip-ansi' -import type { ErrorPayload } from 'types/hmrPayload' +import type { ErrorPayload } from 'types/hotPayload' import { pad } from '../../utils' import type { ViteDevServer } from '../..' @@ -51,7 +51,7 @@ export function logError(server: ViteDevServer, err: RollupError): void { error: err, }) - server.hot.send({ + server.environments.client.hot.send({ type: 'error', err: prepareError(err), }) diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index b5893dd072b972..b88a9c4185ff61 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -130,8 +130,8 @@ const processNodeUrl = ( ): string => { // prefix with base (dev only, base is never relative) const replacer = (url: string) => { - if (server?.moduleGraph) { - const mod = server.moduleGraph.urlToModuleMap.get(url) + if (server) { + const mod = server.environments.client.moduleGraph.urlToModuleMap.get(url) if (mod && mod.lastHMRTimestamp > 0) { url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) } @@ -182,7 +182,7 @@ const devHtmlHook: IndexHtmlTransformHook = async ( html, { path: htmlPath, filename, server, originalUrl }, ) => { - const { config, moduleGraph, watcher } = server! + const { config, watcher } = server! const base = config.base || '/' let proxyModulePath: string @@ -243,9 +243,10 @@ const devHtmlHook: IndexHtmlTransformHook = async ( const modulePath = `${proxyModuleUrl}?html-proxy&index=${inlineModuleIndex}.${ext}` // invalidate the module so the newly cached contents will be served - const module = server?.moduleGraph.getModuleById(modulePath) + const clientModuleGraph = server?.environments.client.moduleGraph + const module = clientModuleGraph?.getModuleById(modulePath) if (module) { - server?.moduleGraph.invalidateModule(module) + clientModuleGraph!.invalidateModule(module) } s.update( node.sourceCodeLocation!.startOffset, @@ -351,10 +352,16 @@ const devHtmlHook: IndexHtmlTransformHook = async ( const url = `${proxyModulePath}?html-proxy&direct&index=${index}.css` // ensure module in graph after successful load - const mod = await moduleGraph.ensureEntryFromUrl(url, false) + const mod = + await server!.environments.client.moduleGraph.ensureEntryFromUrl( + url, + false, + ) ensureWatchedFile(watcher, mod.file, config.root) - const result = await server!.pluginContainer.transform(code, mod.id!) + const result = await server!.pluginContainer.transform(code, mod.id!, { + environment: server!.environments.client, + }) let content = '' if (result) { if (result.map && 'version' in result.map) { @@ -376,10 +383,16 @@ const devHtmlHook: IndexHtmlTransformHook = async ( // will transform with css plugin and cache result with css-post plugin const url = `${proxyModulePath}?html-proxy&inline-css&style-attr&index=${index}.css` - const mod = await moduleGraph.ensureEntryFromUrl(url, false) + const mod = + await server!.environments.client.moduleGraph.ensureEntryFromUrl( + url, + false, + ) ensureWatchedFile(watcher, mod.file, config.root) - await server?.pluginContainer.transform(code, mod.id!) + await server?.pluginContainer.transform(code, mod.id!, { + environment: server!.environments.client, + }) const hash = getHash(cleanUrl(mod.id!)) const result = htmlProxyResult.get(`${hash}_${index}`) diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index d706b3fa926fee..6924989ab538ab 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -2,9 +2,12 @@ import path from 'node:path' import type { OutgoingHttpHeaders, ServerResponse } from 'node:http' import type { Options } from 'sirv' import sirv from 'sirv' +import picomatch from 'picomatch' +import type { Matcher } from 'picomatch' import type { Connect } from 'dep-types/connect' import escapeHtml from 'escape-html' -import type { ViteDevServer } from '../..' +import type { ViteDevServer } from '../../server' +import type { ResolvedConfig } from '../../config' import { FS_PREFIX } from '../../constants' import { fsPathFromId, @@ -204,27 +207,80 @@ export function serveRawFsMiddleware( } } +const safeModulePathsCache = new WeakMap>() +function isSafeModulePath(config: ResolvedConfig, filePath: string) { + let safeModulePaths = safeModulePathsCache.get(config) + if (!safeModulePaths) { + safeModulePaths = new Set() + safeModulePathsCache.set(config, safeModulePaths) + } + return safeModulePaths.has(filePath) +} +export function addSafeModulePath( + config: ResolvedConfig, + filePath: string, +): void { + let safeModulePaths = safeModulePathsCache.get(config) + if (!safeModulePaths) { + safeModulePaths = new Set() + safeModulePathsCache.set(config, safeModulePaths) + } + safeModulePaths.add(filePath) +} + +const fsDenyGlobCache = new WeakMap() +function fsDenyGlob(config: ResolvedConfig, filePath: string): boolean { + let matcher = fsDenyGlobCache.get(config) + if (!matcher) { + ;(matcher = picomatch( + // matchBase: true does not work as it's documented + // https://github.com/micromatch/picomatch/issues/89 + // convert patterns without `/` on our side for now + config.server.fs.deny.map((pattern) => + pattern.includes('/') ? pattern : `**/${pattern}`, + ), + { + matchBase: false, + nocase: true, + dot: true, + }, + )), + fsDenyGlobCache.set(config, matcher) + } + return matcher(filePath) +} + /** * Check if the url is allowed to be served, via the `server.fs` config. + * @deprecated use isFileLoadingAllowed */ export function isFileServingAllowed( url: string, server: ViteDevServer, ): boolean { - if (!server.config.server.fs.strict) return true + const { config } = server + if (!config.server.fs.strict) return true + const filePath = fsPathFromUrl(url) + return isFileLoadingAllowed(config, filePath) +} - const file = fsPathFromUrl(url) +function isUriInFilePath(uri: string, filePath: string) { + return isSameFileUri(uri, filePath) || isParentDirectory(uri, filePath) +} - if (server._fsDenyGlob(file)) return false +export function isFileLoadingAllowed( + config: ResolvedConfig, + filePath: string, +): boolean { + const { fs } = config.server - if (server.moduleGraph.safeModulesPath.has(file)) return true + if (!fs.strict) return true - if ( - server.config.server.fs.allow.some( - (uri) => isSameFileUri(uri, file) || isParentDirectory(uri, file), - ) - ) - return true + if (fsDenyGlob(config, filePath)) return false + + if (isSafeModulePath(config, filePath)) return true + + if (fs.allow.some((uri) => isUriInFilePath(uri, filePath))) return true return false } diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 12a440d4c10774..df20cd4a3c429d 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -32,7 +32,6 @@ import { ERR_OUTDATED_OPTIMIZED_DEP, } from '../../plugins/optimizedDeps' import { ERR_CLOSED_SERVER } from '../pluginContainer' -import { getDepsOptimizer } from '../../optimizer' import { cleanUrl, unwrapId, withTrailingSlash } from '../../../shared/utils' import { NULL_BYTE_PLACEHOLDER } from '../../../shared/constants' @@ -48,10 +47,12 @@ export function cachedTransformMiddleware( ): Connect.NextHandleFunction { // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return function viteCachedTransformMiddleware(req, res, next) { + const environment = server.environments.client + // check if we can return 304 early const ifNoneMatch = req.headers['if-none-match'] if (ifNoneMatch) { - const moduleByEtag = server.moduleGraph.getModuleByEtag(ifNoneMatch) + const moduleByEtag = environment.moduleGraph.getModuleByEtag(ifNoneMatch) if (moduleByEtag?.transformResult?.etag === ifNoneMatch) { // For CSS requests, if the same CSS file is imported in a module, // the browser sends the request for the direct CSS request with the etag @@ -80,6 +81,9 @@ export function transformMiddleware( const publicPath = `${publicDir.slice(root.length)}/` return async function viteTransformMiddleware(req, res, next) { + // TODO: We could do this for all browser like environments, and avoid the harcoded environments.client here + const environment = server.environments.client + if (req.method !== 'GET' || knownIgnoreList.has(req.url!)) { return next() } @@ -100,7 +104,7 @@ export function transformMiddleware( const isSourceMap = withoutQuery.endsWith('.map') // since we generate source map references, handle those requests here if (isSourceMap) { - const depsOptimizer = getDepsOptimizer(server.config, false) // non-ssr + const depsOptimizer = environment.depsOptimizer if (depsOptimizer?.isOptimizedDepUrl(url)) { // If the browser is requesting a source map for an optimized dep, it // means that the dependency has already been pre-bundled and loaded @@ -142,7 +146,7 @@ export function transformMiddleware( } else { const originalUrl = url.replace(/\.map($|\?)/, '$1') const map = ( - await server.moduleGraph.getModuleByUrl(originalUrl, false) + await environment.moduleGraph.getModuleByUrl(originalUrl) )?.transformResult?.map if (map) { return send(req, res, JSON.stringify(map), 'json', { @@ -185,8 +189,8 @@ export function transformMiddleware( const ifNoneMatch = req.headers['if-none-match'] if ( ifNoneMatch && - (await server.moduleGraph.getModuleByUrl(url, false)) - ?.transformResult?.etag === ifNoneMatch + (await environment.moduleGraph.getModuleByUrl(url))?.transformResult + ?.etag === ifNoneMatch ) { debugCache?.(`[304] ${prettifyUrl(url, server.config.root)}`) res.statusCode = 304 @@ -195,11 +199,11 @@ export function transformMiddleware( } // resolve, load and transform using the plugin container - const result = await transformRequest(url, server, { + const result = await transformRequest(environment, url, { html: req.headers.accept?.includes('text/html'), }) if (result) { - const depsOptimizer = getDepsOptimizer(server.config, false) // non-ssr + const depsOptimizer = environment.depsOptimizer const type = isDirectCSSRequest(url) ? 'css' : 'js' const isDep = DEP_VERSION_RE.test(url) || depsOptimizer?.isOptimizedDepUrl(url) @@ -264,6 +268,7 @@ export function transformMiddleware( return } if (e?.code === ERR_LOAD_URL) { + // TODO: Why not also do this on ERR_LOAD_PUBLIC_URL? // Let other middleware handle if we can't load the url via transformRequest return next() } diff --git a/packages/vite/src/node/server/mixedModuleGraph.ts b/packages/vite/src/node/server/mixedModuleGraph.ts new file mode 100644 index 00000000000000..9c06920aa794d7 --- /dev/null +++ b/packages/vite/src/node/server/mixedModuleGraph.ts @@ -0,0 +1,654 @@ +import type { ModuleInfo } from 'rollup' +import type { TransformResult } from './transformRequest' +import type { + EnvironmentModuleGraph, + EnvironmentModuleNode, + ResolvedUrl, +} from './moduleGraph' + +/** + * Backward compatible ModuleNode and ModuleGraph with mixed nodes from both the client and ssr environments + * It would be good to take the types names for the new EnvironmentModuleNode and EnvironmentModuleGraph but we can't + * do that at this point without breaking to much code in the ecosystem. + * We are going to deprecate these types and we can try to use them back in the future. + */ + +export class ModuleNode { + _moduleGraph: ModuleGraph + _clientModule: EnvironmentModuleNode | undefined + _ssrModule: EnvironmentModuleNode | undefined + constructor( + moduleGraph: ModuleGraph, + clientModule?: EnvironmentModuleNode, + ssrModule?: EnvironmentModuleNode, + ) { + this._moduleGraph = moduleGraph + this._clientModule = clientModule + this._ssrModule = ssrModule + } + _get( + prop: T, + ): EnvironmentModuleNode[T] { + return (this._clientModule?.[prop] ?? this._ssrModule?.[prop])! + } + _wrapModuleSet( + prop: ModuleSetNames, + module: EnvironmentModuleNode | undefined, + ): Set { + if (!module) { + return new Set() + } + return createBackwardCompatibleModuleSet(this._moduleGraph, prop, module) + } + _getModuleSetUnion(prop: 'importedModules' | 'importers'): Set { + // A good approximation to the previous logic that returned the union of + // the importedModules and importers from both the browser and server + const importedModules = new Set() + const ids = new Set() + if (this._clientModule) { + for (const mod of this._clientModule[prop]) { + if (mod.id) ids.add(mod.id) + importedModules.add( + this._moduleGraph.getBackwardCompatibleModuleNode(mod), + ) + } + } + if (this._ssrModule) { + for (const mod of this._ssrModule[prop]) { + if (mod.id && !ids.has(mod.id)) { + importedModules.add( + this._moduleGraph.getBackwardCompatibleModuleNode(mod), + ) + } + } + } + return importedModules + } + get url(): string { + return this._get('url') + } + get id(): string | null { + return this._get('id') + } + get file(): string | null { + return this._get('file') + } + get type(): 'js' | 'css' { + return this._get('type') + } + get info(): ModuleInfo | undefined { + return this._get('info') + } + get meta(): Record | undefined { + return this._get('meta') + } + get importers(): Set { + return this._getModuleSetUnion('importers') + } + get clientImportedModules(): Set { + return this._wrapModuleSet('importedModules', this._clientModule) + } + get ssrImportedModules(): Set { + return this._wrapModuleSet('importedModules', this._ssrModule) + } + get importedModules(): Set { + return this._getModuleSetUnion('importedModules') + } + get acceptedHmrDeps(): Set { + return this._wrapModuleSet('acceptedHmrDeps', this._clientModule) + } + get acceptedHmrExports(): Set | null { + return this._clientModule?.acceptedHmrExports ?? null + } + get importedBindings(): Map> | null { + return this._clientModule?.importedBindings ?? null + } + get isSelfAccepting(): boolean | undefined { + return this._clientModule?.isSelfAccepting + } + get transformResult(): TransformResult | null { + return this._clientModule?.transformResult ?? null + } + set transformResult(value: TransformResult | null) { + if (this._clientModule) { + this._clientModule.transformResult = value + } + } + get ssrTransformResult(): TransformResult | null { + return this._ssrModule?.transformResult ?? null + } + set ssrTransformResult(value: TransformResult | null) { + if (this._ssrModule) { + this._ssrModule.transformResult = value + } + } + get ssrModule(): Record | null { + return this._ssrModule?.ssrModule ?? null + } + get ssrError(): Error | null { + return this._ssrModule?.ssrError ?? null + } + get lastHMRTimestamp(): number { + return Math.max( + this._clientModule?.lastHMRTimestamp ?? 0, + this._ssrModule?.lastHMRTimestamp ?? 0, + ) + } + set lastHMRTimestamp(value: number) { + if (this._clientModule) { + this._clientModule.lastHMRTimestamp = value + } + if (this._ssrModule) { + this._ssrModule.lastHMRTimestamp = value + } + } + get lastInvalidationTimestamp(): number { + return Math.max( + this._clientModule?.lastInvalidationTimestamp ?? 0, + this._ssrModule?.lastInvalidationTimestamp ?? 0, + ) + } + get invalidationState(): TransformResult | 'HARD_INVALIDATED' | undefined { + return this._clientModule?.invalidationState + } + get ssrInvalidationState(): TransformResult | 'HARD_INVALIDATED' | undefined { + return this._ssrModule?.invalidationState + } +} + +function mapIterator( + iterable: IterableIterator, + transform: (value: T) => K, +): IterableIterator { + return { + [Symbol.iterator](): IterableIterator { + return this + }, + next(): IteratorResult { + const r = iterable.next() + return r.done + ? r + : { + value: transform(r.value), + done: false, + } + }, + } +} + +export class ModuleGraph { + /** @internal */ + _moduleGraphs: { + client: () => EnvironmentModuleGraph + ssr: () => EnvironmentModuleGraph + } + + /** @internal */ + get _client(): EnvironmentModuleGraph { + return this._moduleGraphs.client() + } + + /** @internal */ + get _ssr(): EnvironmentModuleGraph { + return this._moduleGraphs.ssr() + } + + urlToModuleMap: Map + idToModuleMap: Map + etagToModuleMap: Map + + fileToModulesMap: Map> + + constructor(moduleGraphs: { + client: () => EnvironmentModuleGraph + ssr: () => EnvironmentModuleGraph + }) { + this._moduleGraphs = moduleGraphs + + const getModuleMapUnion = + (prop: 'urlToModuleMap' | 'idToModuleMap') => () => { + // A good approximation to the previous logic that returned the union of + // the importedModules and importers from both the browser and server + if (this._ssr[prop].size === 0) { + return this._client[prop] + } + const map = new Map(this._client[prop]) + for (const [key, module] of this._ssr[prop]) { + if (!map.has(key)) { + map.set(key, module) + } + } + return map + } + + this.urlToModuleMap = createBackwardCompatibleModuleMap( + this, + 'urlToModuleMap', + getModuleMapUnion('urlToModuleMap'), + ) + this.idToModuleMap = createBackwardCompatibleModuleMap( + this, + 'idToModuleMap', + getModuleMapUnion('idToModuleMap'), + ) + this.etagToModuleMap = createBackwardCompatibleModuleMap( + this, + 'etagToModuleMap', + () => this._client.etagToModuleMap, + ) + this.fileToModulesMap = createBackwardCompatibleFileToModulesMap(this) + } + + /** @deprecated */ + getModuleById(id: string): ModuleNode | undefined { + const clientModule = this._client.getModuleById(id) + const ssrModule = this._ssr.getModuleById(id) + if (!clientModule && !ssrModule) { + return + } + return this.getBackwardCompatibleModuleNodeDual(clientModule, ssrModule) + } + + /** @deprecated */ + async getModuleByUrl( + url: string, + ssr?: boolean, + ): Promise { + // In the mixed graph, the ssr flag was used to resolve the id. + const [clientModule, ssrModule] = await Promise.all([ + this._client.getModuleByUrl(url), + this._ssr.getModuleByUrl(url), + ]) + if (!clientModule && !ssrModule) { + return + } + return this.getBackwardCompatibleModuleNodeDual(clientModule, ssrModule) + } + + /** @deprecated */ + getModulesByFile(file: string): Set | undefined { + // Until Vite 5.1.x, the moduleGraph contained modules from both the browser and server + // We maintain backwards compatibility by returning a Set of module proxies assuming + // that the modules for a certain file are the same in both the browser and server + const clientModules = this._client.getModulesByFile(file) + if (clientModules) { + return new Set( + [...clientModules].map( + (mod) => this.getBackwardCompatibleBrowserModuleNode(mod)!, + ), + ) + } + const ssrModules = this._ssr.getModulesByFile(file) + if (ssrModules) { + return new Set( + [...ssrModules].map( + (mod) => this.getBackwardCompatibleServerModuleNode(mod)!, + ), + ) + } + return undefined + } + + /** @deprecated */ + onFileChange(file: string): void { + this._client.onFileChange(file) + this._ssr.onFileChange(file) + } + + /** @deprecated */ + onFileDelete(file: string): void { + this._client.onFileDelete(file) + this._ssr.onFileDelete(file) + } + + /** @internal */ + _getModuleGraph(environment: string): EnvironmentModuleGraph { + switch (environment) { + case 'client': + return this._client + case 'ssr': + return this._ssr + default: + throw new Error(`Invalid module node environment ${environment}`) + } + } + + /** @deprecated */ + invalidateModule( + mod: ModuleNode, + seen: Set = new Set(), + timestamp: number = Date.now(), + isHmr: boolean = false, + /** @internal */ + softInvalidate = false, + ): void { + if (mod._clientModule) { + this._client.invalidateModule( + mod._clientModule, + new Set( + [...seen].map((mod) => mod._clientModule).filter(Boolean), + ) as Set, + timestamp, + isHmr, + softInvalidate, + ) + } + if (mod._ssrModule) { + // TODO: Maybe this isn't needed? + this._ssr.invalidateModule( + mod._ssrModule, + new Set( + [...seen].map((mod) => mod._ssrModule).filter(Boolean), + ) as Set, + timestamp, + isHmr, + softInvalidate, + ) + } + } + + /** @deprecated */ + invalidateAll(): void { + this._client.invalidateAll() + this._ssr.invalidateAll() + } + + /* TODO: I don't know if we need to implement this method (or how to do it yet) + async updateModuleInfo( + module: ModuleNode, + importedModules: Set, + importedBindings: Map> | null, + acceptedModules: Set, + acceptedExports: Set | null, + isSelfAccepting: boolean, + ssr?: boolean, + staticImportedUrls?: Set, // internal + ): Promise | undefined> { + const modules = await this._getModuleGraph( + module.environment, + ).updateModuleInfo( + module, + importedModules, // ? + importedBindings, + acceptedModules, // ? + acceptedExports, + isSelfAccepting, + staticImportedUrls, + ) + return modules + ? new Set( + [...modules].map((mod) => this.getBackwardCompatibleModuleNode(mod)!), + ) + : undefined + } + */ + + /** @deprecated */ + async ensureEntryFromUrl( + rawUrl: string, + ssr?: boolean, + setIsSelfAccepting = true, + ): Promise { + const module = await (ssr ? this._ssr : this._client).ensureEntryFromUrl( + rawUrl, + setIsSelfAccepting, + ) + return this.getBackwardCompatibleModuleNode(module)! + } + + /** @deprecated */ + createFileOnlyEntry(file: string): ModuleNode { + const clientModule = this._client.createFileOnlyEntry(file) + const ssrModule = this._ssr.createFileOnlyEntry(file) + return this.getBackwardCompatibleModuleNodeDual(clientModule, ssrModule)! + } + + /** @deprecated */ + async resolveUrl(url: string, ssr?: boolean): Promise { + return ssr ? this._ssr.resolveUrl(url) : this._client.resolveUrl(url) + } + + /** @deprecated */ + updateModuleTransformResult( + mod: ModuleNode, + result: TransformResult | null, + ssr?: boolean, + ): void { + const environment = ssr ? 'ssr' : 'client' + this._getModuleGraph(environment).updateModuleTransformResult( + (environment === 'client' ? mod._clientModule : mod._ssrModule)!, + result, + ) + } + + /** @deprecated */ + getModuleByEtag(etag: string): ModuleNode | undefined { + const mod = this._client.etagToModuleMap.get(etag) + return mod && this.getBackwardCompatibleBrowserModuleNode(mod) + } + + getBackwardCompatibleBrowserModuleNode( + clientModule: EnvironmentModuleNode, + ): ModuleNode { + return this.getBackwardCompatibleModuleNodeDual( + clientModule, + clientModule.id ? this._ssr.getModuleById(clientModule.id) : undefined, + ) + } + + getBackwardCompatibleServerModuleNode( + ssrModule: EnvironmentModuleNode, + ): ModuleNode { + return this.getBackwardCompatibleModuleNodeDual( + ssrModule.id ? this._client.getModuleById(ssrModule.id) : undefined, + ssrModule, + ) + } + + getBackwardCompatibleModuleNode(mod: EnvironmentModuleNode): ModuleNode { + return mod.environment === 'client' + ? this.getBackwardCompatibleBrowserModuleNode(mod) + : this.getBackwardCompatibleServerModuleNode(mod) + } + + getBackwardCompatibleModuleNodeDual( + clientModule?: EnvironmentModuleNode, + ssrModule?: EnvironmentModuleNode, + ): ModuleNode { + // ... + return new ModuleNode(this, clientModule, ssrModule) + } +} + +type ModuleSetNames = 'acceptedHmrDeps' | 'importedModules' + +function createBackwardCompatibleModuleSet( + moduleGraph: ModuleGraph, + prop: ModuleSetNames, + module: EnvironmentModuleNode, +): Set { + return { + [Symbol.iterator]() { + return this.keys() + }, + has(key) { + if (!key.id) { + return false + } + const keyModule = moduleGraph + ._getModuleGraph(module.environment) + .getModuleById(key.id) + return keyModule !== undefined && module[prop].has(keyModule) + }, + values() { + return this.keys() + }, + keys() { + return mapIterator(module[prop].keys(), (mod) => + moduleGraph.getBackwardCompatibleModuleNode(mod), + ) + }, + get size() { + return module[prop].size + }, + forEach(callback, thisArg) { + return module[prop].forEach((mod) => { + const backwardCompatibleMod = + moduleGraph.getBackwardCompatibleModuleNode(mod) + callback.call( + thisArg, + backwardCompatibleMod, + backwardCompatibleMod, + this, + ) + }) + }, + // TODO: should we implement all the set methods? + // missing: add, clear, delete, difference, intersection, isDisjointFrom, + // isSubsetOf, isSupersetOf, symmetricDifference, union + } as Set +} + +function createBackwardCompatibleModuleMap( + moduleGraph: ModuleGraph, + prop: 'urlToModuleMap' | 'idToModuleMap' | 'etagToModuleMap', + getModuleMap: () => Map, +): Map { + return { + [Symbol.iterator]() { + return this.entries() + }, + get(key) { + const clientModule = moduleGraph._client[prop].get(key) + const ssrModule = moduleGraph._ssr[prop].get(key) + if (!clientModule && !ssrModule) { + return + } + return moduleGraph.getBackwardCompatibleModuleNodeDual( + clientModule, + ssrModule, + ) + }, + keys() { + return getModuleMap().keys() + }, + values() { + return mapIterator(getModuleMap().values(), (mod) => + moduleGraph.getBackwardCompatibleModuleNode(mod), + ) + }, + entries() { + return mapIterator(getModuleMap().entries(), ([key, mod]) => [ + key, + moduleGraph.getBackwardCompatibleModuleNode(mod), + ]) + }, + get size() { + // TODO: Should we use Math.max(moduleGraph._client[prop].size, moduleGraph._ssr[prop].size) + // for performance? I don't think there are many use cases of this method + return getModuleMap().size + }, + forEach(callback, thisArg) { + return getModuleMap().forEach((mod, key) => { + const backwardCompatibleMod = + moduleGraph.getBackwardCompatibleModuleNode(mod) + callback.call(thisArg, backwardCompatibleMod, key, this) + }) + }, + } as Map +} + +function createBackwardCompatibleFileToModulesMap( + moduleGraph: ModuleGraph, +): Map> { + const getFileToModulesMap = (): Map> => { + // A good approximation to the previous logic that returned the union of + // the importedModules and importers from both the browser and server + if (!moduleGraph._ssr.fileToModulesMap.size) { + return moduleGraph._client.fileToModulesMap + } + const map = new Map(moduleGraph._client.fileToModulesMap) + for (const [key, modules] of moduleGraph._ssr.fileToModulesMap) { + const modulesSet = map.get(key) + if (!modulesSet) { + map.set(key, modules) + } else { + for (const ssrModule of modules) { + let hasModule = false + for (const clientModule of modulesSet) { + hasModule ||= clientModule.id === ssrModule.id + if (hasModule) { + break + } + } + if (!hasModule) { + modulesSet.add(ssrModule) + } + } + } + } + return map + } + const getBackwardCompatibleModules = ( + modules: Set, + ): Set => + new Set( + [...modules].map((mod) => + moduleGraph.getBackwardCompatibleModuleNode(mod), + ), + ) + + return { + [Symbol.iterator]() { + return this.entries() + }, + get(key) { + const clientModules = moduleGraph._client.fileToModulesMap.get(key) + const ssrModules = moduleGraph._ssr.fileToModulesMap.get(key) + if (!clientModules && !ssrModules) { + return + } + const modules = clientModules ?? new Set() + if (ssrModules) { + for (const ssrModule of ssrModules) { + if (ssrModule.id) { + let found = false + for (const mod of modules) { + found ||= mod.id === ssrModule.id + if (found) { + break + } + } + if (!found) { + modules?.add(ssrModule) + } + } + } + } + return getBackwardCompatibleModules(modules) + }, + keys() { + return getFileToModulesMap().keys() + }, + values() { + return mapIterator( + getFileToModulesMap().values(), + getBackwardCompatibleModules, + ) + }, + entries() { + return mapIterator(getFileToModulesMap().entries(), ([key, modules]) => [ + key, + getBackwardCompatibleModules(modules), + ]) + }, + get size() { + return getFileToModulesMap().size + }, + forEach(callback, thisArg) { + return getFileToModulesMap().forEach((modules, key) => { + callback.call(thisArg, getBackwardCompatibleModules(modules), key, this) + }) + }, + } as Map> +} diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index 442ece308dbaff..eeec3cd6188f07 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -10,7 +10,8 @@ import { FS_PREFIX } from '../constants' import { cleanUrl } from '../../shared/utils' import type { TransformResult } from './transformRequest' -export class ModuleNode { +export class EnvironmentModuleNode { + environment: string /** * Public served url path, starts with / */ @@ -18,22 +19,26 @@ export class ModuleNode { /** * Resolved file system path + query */ - id: string | null = null + id: string | null = null // TODO: remove null file: string | null = null type: 'js' | 'css' info?: ModuleInfo meta?: Record - importers = new Set() - clientImportedModules = new Set() - ssrImportedModules = new Set() - acceptedHmrDeps = new Set() + importers = new Set() + + importedModules = new Set() + + acceptedHmrDeps = new Set() acceptedHmrExports: Set | null = null importedBindings: Map> | null = null isSelfAccepting?: boolean transformResult: TransformResult | null = null - ssrTransformResult: TransformResult | null = null + + // ssrModule and ssrError are no longer needed. They are on the module runner module cache. + // Once `ssrLoadModule` is re-implemented on top of the new APIs, we can delete these. ssrModule: Record | null = null ssrError: Error | null = null + lastHMRTimestamp = 0 /** * `import.meta.hot.invalidate` is called by the client. @@ -54,10 +59,6 @@ export class ModuleNode { * @internal */ invalidationState: TransformResult | 'HARD_INVALIDATED' | undefined - /** - * @internal - */ - ssrInvalidationState: TransformResult | 'HARD_INVALIDATED' | undefined /** * The module urls that are statically imported in the code. This information is separated * out from `importedModules` as only importers that statically import the module can be @@ -69,21 +70,14 @@ export class ModuleNode { /** * @param setIsSelfAccepting - set `false` to set `isSelfAccepting` later. e.g. #7870 */ - constructor(url: string, setIsSelfAccepting = true) { + constructor(url: string, environment: string, setIsSelfAccepting = true) { + this.environment = environment this.url = url this.type = isDirectCSSRequest(url) ? 'css' : 'js' if (setIsSelfAccepting) { this.isSelfAccepting = false } } - - get importedModules(): Set { - const importedModules = new Set(this.clientImportedModules) - for (const module of this.ssrImportedModules) { - importedModules.add(module) - } - return importedModules - } } export type ResolvedUrl = [ @@ -92,66 +86,65 @@ export type ResolvedUrl = [ meta: object | null | undefined, ] -export class ModuleGraph { - urlToModuleMap = new Map() - idToModuleMap = new Map() - etagToModuleMap = new Map() +export class EnvironmentModuleGraph { + environment: string + + urlToModuleMap = new Map() + idToModuleMap = new Map() + etagToModuleMap = new Map() // a single file may corresponds to multiple modules with different queries - fileToModulesMap = new Map>() - safeModulesPath = new Set() + fileToModulesMap = new Map>() /** * @internal */ _unresolvedUrlToModuleMap = new Map< string, - Promise | ModuleNode + Promise | EnvironmentModuleNode >() + /** * @internal */ - _ssrUnresolvedUrlToModuleMap = new Map< - string, - Promise | ModuleNode - >() + _resolveId: (url: string) => Promise /** @internal */ - _hasResolveFailedErrorModules = new Set() + _hasResolveFailedErrorModules = new Set() constructor( - private resolveId: ( - url: string, - ssr: boolean, - ) => Promise, - ) {} + environment: string, + resolveId: (url: string) => Promise, + ) { + this.environment = environment + this._resolveId = resolveId + } async getModuleByUrl( rawUrl: string, - ssr?: boolean, - ): Promise { + ): Promise { // Quick path, if we already have a module for this rawUrl (even without extension) rawUrl = removeImportQuery(removeTimestampQuery(rawUrl)) - const mod = this._getUnresolvedUrlToModule(rawUrl, ssr) + const mod = this._getUnresolvedUrlToModule(rawUrl) if (mod) { return mod } - const [url] = await this._resolveUrl(rawUrl, ssr) + const [url] = await this._resolveUrl(rawUrl) return this.urlToModuleMap.get(url) } - getModuleById(id: string): ModuleNode | undefined { + getModuleById(id: string): EnvironmentModuleNode | undefined { return this.idToModuleMap.get(removeTimestampQuery(id)) } - getModulesByFile(file: string): Set | undefined { + getModulesByFile(file: string): Set | undefined { return this.fileToModulesMap.get(file) } onFileChange(file: string): void { const mods = this.getModulesByFile(file) if (mods) { - const seen = new Set() + const seen = new Set() mods.forEach((mod) => { this.invalidateModule(mod, seen) }) @@ -170,15 +163,15 @@ export class ModuleGraph { } invalidateModule( - mod: ModuleNode, - seen: Set = new Set(), + mod: EnvironmentModuleNode, + seen: Set = new Set(), timestamp: number = Date.now(), isHmr: boolean = false, /** @internal */ softInvalidate = false, ): void { const prevInvalidationState = mod.invalidationState - const prevSsrInvalidationState = mod.ssrInvalidationState + // const prevSsrInvalidationState = mod.ssrInvalidationState // Handle soft invalidation before the `seen` check, as consecutive soft/hard invalidations can // cause the final soft invalidation state to be different. @@ -186,19 +179,17 @@ export class ModuleGraph { // import timestamps only in `transformRequest`. If there's no previous `transformResult`, hard invalidate it. if (softInvalidate) { mod.invalidationState ??= mod.transformResult ?? 'HARD_INVALIDATED' - mod.ssrInvalidationState ??= mod.ssrTransformResult ?? 'HARD_INVALIDATED' } // If hard invalidated, further soft invalidations have no effect until it's reset to `undefined` else { mod.invalidationState = 'HARD_INVALIDATED' - mod.ssrInvalidationState = 'HARD_INVALIDATED' } // Skip updating the module if it was already invalidated before and the invalidation state has not changed if ( seen.has(mod) && - prevInvalidationState === mod.invalidationState && - prevSsrInvalidationState === mod.ssrInvalidationState + prevInvalidationState === mod.invalidationState + // && prevSsrInvalidationState === mod.ssrInvalidationState ) { return } @@ -219,7 +210,7 @@ export class ModuleGraph { if (etag) this.etagToModuleMap.delete(etag) mod.transformResult = null - mod.ssrTransformResult = null + mod.ssrModule = null mod.ssrError = null @@ -246,7 +237,7 @@ export class ModuleGraph { invalidateAll(): void { const timestamp = Date.now() - const seen = new Set() + const seen = new Set() this.idToModuleMap.forEach((mod) => { this.invalidateModule(mod, seen, timestamp) }) @@ -261,19 +252,18 @@ export class ModuleGraph { * This is only used for soft invalidations so `undefined` is fine but may cause more runtime processing. */ async updateModuleInfo( - mod: ModuleNode, - importedModules: Set, + mod: EnvironmentModuleNode, + importedModules: Set, importedBindings: Map> | null, - acceptedModules: Set, + acceptedModules: Set, acceptedExports: Set | null, isSelfAccepting: boolean, - ssr?: boolean, /** @internal */ staticImportedUrls?: Set, - ): Promise | undefined> { + ): Promise | undefined> { mod.isSelfAccepting = isSelfAccepting - const prevImports = ssr ? mod.ssrImportedModules : mod.clientImportedModules - let noLongerImported: Set | undefined + const prevImports = mod.importedModules + let noLongerImported: Set | undefined let resolvePromises = [] let resolveResults = new Array(importedModules.size) @@ -283,7 +273,7 @@ export class ModuleGraph { const nextIndex = index++ if (typeof imported === 'string') { resolvePromises.push( - this.ensureEntryFromUrl(imported, ssr).then((dep) => { + this.ensureEntryFromUrl(imported).then((dep) => { dep.importers.add(mod) resolveResults[nextIndex] = dep }), @@ -299,18 +289,11 @@ export class ModuleGraph { } const nextImports = new Set(resolveResults) - if (ssr) { - mod.ssrImportedModules = nextImports - } else { - mod.clientImportedModules = nextImports - } + mod.importedModules = nextImports // remove the importer from deps that were imported but no longer are. prevImports.forEach((dep) => { - if ( - !mod.clientImportedModules.has(dep) && - !mod.ssrImportedModules.has(dep) - ) { + if (!mod.importedModules.has(dep)) { dep.importers.delete(mod) if (!dep.importers.size) { // dependency no longer imported @@ -327,7 +310,7 @@ export class ModuleGraph { const nextIndex = index++ if (typeof accepted === 'string') { resolvePromises.push( - this.ensureEntryFromUrl(accepted, ssr).then((dep) => { + this.ensureEntryFromUrl(accepted).then((dep) => { resolveResults[nextIndex] = dep }), ) @@ -351,10 +334,9 @@ export class ModuleGraph { async ensureEntryFromUrl( rawUrl: string, - ssr?: boolean, setIsSelfAccepting = true, - ): Promise { - return this._ensureEntryFromUrl(rawUrl, ssr, setIsSelfAccepting) + ): Promise { + return this._ensureEntryFromUrl(rawUrl, setIsSelfAccepting) } /** @@ -362,26 +344,25 @@ export class ModuleGraph { */ async _ensureEntryFromUrl( rawUrl: string, - ssr?: boolean, setIsSelfAccepting = true, // Optimization, avoid resolving the same url twice if the caller already did it resolved?: PartialResolvedId, - ): Promise { + ): Promise { // Quick path, if we already have a module for this rawUrl (even without extension) rawUrl = removeImportQuery(removeTimestampQuery(rawUrl)) - let mod = this._getUnresolvedUrlToModule(rawUrl, ssr) + let mod = this._getUnresolvedUrlToModule(rawUrl) if (mod) { return mod } const modPromise = (async () => { - const [url, resolvedId, meta] = await this._resolveUrl( - rawUrl, - ssr, - resolved, - ) + const [url, resolvedId, meta] = await this._resolveUrl(rawUrl, resolved) mod = this.idToModuleMap.get(resolvedId) if (!mod) { - mod = new ModuleNode(url, setIsSelfAccepting) + mod = new EnvironmentModuleNode( + url, + this.environment, + setIsSelfAccepting, + ) if (meta) mod.meta = meta this.urlToModuleMap.set(url, mod) mod.id = resolvedId @@ -399,13 +380,13 @@ export class ModuleGraph { else if (!this.urlToModuleMap.has(url)) { this.urlToModuleMap.set(url, mod) } - this._setUnresolvedUrlToModule(rawUrl, mod, ssr) + this._setUnresolvedUrlToModule(rawUrl, mod) return mod })() // Also register the clean url to the module, so that we can short-circuit // resolving the same url twice - this._setUnresolvedUrlToModule(rawUrl, modPromise, ssr) + this._setUnresolvedUrlToModule(rawUrl, modPromise) return modPromise } @@ -413,7 +394,7 @@ export class ModuleGraph { // url because they are inlined into the main css import. But they still // need to be represented in the module graph so that they can trigger // hmr in the importing css file. - createFileOnlyEntry(file: string): ModuleNode { + createFileOnlyEntry(file: string): EnvironmentModuleNode { file = normalizePath(file) let fileMappedModules = this.fileToModulesMap.get(file) if (!fileMappedModules) { @@ -428,7 +409,7 @@ export class ModuleGraph { } } - const mod = new ModuleNode(url) + const mod = new EnvironmentModuleNode(url, this.environment) mod.file = file fileMappedModules.add(mod) return mod @@ -438,33 +419,29 @@ export class ModuleGraph { // 1. remove the HMR timestamp query (?t=xxxx) and the ?import query // 2. resolve its extension so that urls with or without extension all map to // the same module - async resolveUrl(url: string, ssr?: boolean): Promise { + async resolveUrl(url: string): Promise { url = removeImportQuery(removeTimestampQuery(url)) - const mod = await this._getUnresolvedUrlToModule(url, ssr) + const mod = await this._getUnresolvedUrlToModule(url) if (mod?.id) { return [mod.url, mod.id, mod.meta] } - return this._resolveUrl(url, ssr) + return this._resolveUrl(url) } updateModuleTransformResult( - mod: ModuleNode, + mod: EnvironmentModuleNode, result: TransformResult | null, - ssr: boolean, ): void { - if (ssr) { - mod.ssrTransformResult = result - } else { + if (this.environment === 'client') { const prevEtag = mod.transformResult?.etag if (prevEtag) this.etagToModuleMap.delete(prevEtag) - - mod.transformResult = result - if (result?.etag) this.etagToModuleMap.set(result.etag, mod) } + + mod.transformResult = result } - getModuleByEtag(etag: string): ModuleNode | undefined { + getModuleByEtag(etag: string): EnvironmentModuleNode | undefined { return this.etagToModuleMap.get(etag) } @@ -473,24 +450,17 @@ export class ModuleGraph { */ _getUnresolvedUrlToModule( url: string, - ssr?: boolean, - ): Promise | ModuleNode | undefined { - return ( - ssr ? this._ssrUnresolvedUrlToModuleMap : this._unresolvedUrlToModuleMap - ).get(url) + ): Promise | EnvironmentModuleNode | undefined { + return this._unresolvedUrlToModuleMap.get(url) } /** * @internal */ _setUnresolvedUrlToModule( url: string, - mod: Promise | ModuleNode, - ssr?: boolean, + mod: Promise | EnvironmentModuleNode, ): void { - ;(ssr - ? this._ssrUnresolvedUrlToModuleMap - : this._unresolvedUrlToModuleMap - ).set(url, mod) + this._unresolvedUrlToModuleMap.set(url, mod) } /** @@ -498,10 +468,9 @@ export class ModuleGraph { */ async _resolveUrl( url: string, - ssr?: boolean, alreadyResolved?: PartialResolvedId, ): Promise { - const resolved = alreadyResolved ?? (await this.resolveId(url, !!ssr)) + const resolved = alreadyResolved ?? (await this._resolveId(url)) const resolvedId = resolved?.id || url if ( url !== resolvedId && diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index 3251790d169864..9b0b35b427ce20 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -77,11 +77,16 @@ import { timeFrom, } from '../utils' import { FS_PREFIX } from '../constants' -import type { PluginHookUtils, ResolvedConfig } from '../config' import { createPluginHookUtils, getHookHandler } from '../plugins' import { cleanUrl, unwrapId } from '../../shared/utils' +import type { PluginHookUtils } from '../config' +import type { Environment } from '../environment' +import type { DevEnvironment } from './environment' import { buildErrorMessage } from './middlewares/error' -import type { ModuleGraph, ModuleNode } from './moduleGraph' +import type { + EnvironmentModuleGraph, + EnvironmentModuleNode, +} from './moduleGraph' const noop = () => {} @@ -120,43 +125,53 @@ export interface PluginContainerOptions { writeFile?: (name: string, source: string | Uint8Array) => void } -export async function createPluginContainer( - config: ResolvedConfig, - moduleGraph?: ModuleGraph, +/** + * Create a plugin container with a set of plugins. We pass them as a parameter + * instead of using environment.plugins to allow the creation of different + * pipelines working with the same environment (used for createIdResolver). + */ +export async function createEnvironmentPluginContainer( + environment: Environment, + plugins: Plugin[], watcher?: FSWatcher, -): Promise { - const container = new PluginContainer(config, moduleGraph, watcher) +): Promise { + const container = new EnvironmentPluginContainer( + environment, + plugins, + watcher, + ) await container.resolveRollupOptions() return container } -class PluginContainer { +class EnvironmentPluginContainer { private _pluginContextMap = new Map() - private _pluginContextMapSsr = new Map() private _resolvedRollupOptions?: InputOptions private _processesing = new Set>() private _seenResolves: Record = {} - private _closed = false + // _addedFiles from the `load()` hook gets saved here so it can be reused in the `transform()` hook private _moduleNodeToLoadAddedImports = new WeakMap< - ModuleNode, + EnvironmentModuleNode, Set | null >() getSortedPluginHooks: PluginHookUtils['getSortedPluginHooks'] getSortedPlugins: PluginHookUtils['getSortedPlugins'] + moduleGraph: EnvironmentModuleGraph | undefined watchFiles = new Set() minimalContext: MinimalPluginContext + private _closed = false + /** - * @internal use `createPluginContainer` instead + * @internal use `createEnvironmentPluginContainer` instead */ constructor( - public config: ResolvedConfig, - public moduleGraph?: ModuleGraph, + public environment: Environment, + public plugins: Plugin[], public watcher?: FSWatcher, - public plugins = config.plugins, ) { this.minimalContext = { meta: { @@ -172,6 +187,8 @@ class PluginContainer { const utils = createPluginHookUtils(plugins) this.getSortedPlugins = utils.getSortedPlugins this.getSortedPluginHooks = utils.getSortedPluginHooks + this.moduleGraph = + environment.mode === 'dev' ? environment.moduleGraph : undefined } private _updateModuleLoadAddedImports( @@ -191,35 +208,6 @@ class PluginContainer { : null } - getModuleInfo(id: string): ModuleInfo | null { - const module = this.moduleGraph?.getModuleById(id) - if (!module) { - return null - } - if (!module.info) { - module.info = new Proxy( - { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo, - // throw when an unsupported ModuleInfo property is accessed, - // so that incompatible plugins fail in a non-cryptic way. - { - get(info: any, key: string) { - if (key in info) { - return info[key] - } - // Don't throw an error when returning from an async function - if (key === 'then') { - return undefined - } - throw Error( - `[vite] The "${key}" property of ModuleInfo is not supported.`, - ) - }, - }, - ) - } - return module.info ?? null - } - // keeps track of hook promises so that we can wait for them all to finish upon closing the server private handleHookPromise(maybePromise: undefined | T | Promise) { if (!(maybePromise as any)?.then) { @@ -236,7 +224,7 @@ class PluginContainer { async resolveRollupOptions(): Promise { if (!this._resolvedRollupOptions) { - let options = this.config.build.rollupOptions + let options = this.environment.options.build.rollupOptions for (const optionsHook of this.getSortedPluginHooks('options')) { if (this._closed) { throwClosedServerError() @@ -251,13 +239,11 @@ class PluginContainer { return this._resolvedRollupOptions } - private _getPluginContext(plugin: Plugin, ssr: boolean) { - const map = ssr ? this._pluginContextMapSsr : this._pluginContextMap - if (!map.has(plugin)) { - const ctx = new PluginContext(plugin, this, ssr) - map.set(plugin, ctx) + private _getPluginContext(plugin: Plugin) { + if (!this._pluginContextMap.has(plugin)) { + this._pluginContextMap.set(plugin, new PluginContext(plugin, this)) } - return map.get(plugin)! + return this._pluginContextMap.get(plugin)! } // parallel, ignores returns @@ -288,7 +274,7 @@ class PluginContainer { await this.handleHookPromise( this.hookParallel( 'buildStart', - (plugin) => this._getPluginContext(plugin, false), + (plugin) => this._getPluginContext(plugin), () => [this.options as NormalizedInputOptions], ), ) @@ -296,12 +282,14 @@ class PluginContainer { async resolveId( rawId: string, - importer: string | undefined = join(this.config.root, 'index.html'), + importer: string | undefined = join( + this.environment.config.root, + 'index.html', + ), options?: { attributes?: Record custom?: CustomPluginOptions skip?: Set - ssr?: boolean /** * @internal */ @@ -310,16 +298,16 @@ class PluginContainer { }, ): Promise { const skip = options?.skip - const ssr = options?.ssr const scan = !!options?.scan - const ctx = new ResolveIdContext(this, !!ssr, skip, scan) + const ssr = this.environment.name !== 'client' + const ctx = new ResolveIdContext(this, skip, scan) const resolveStart = debugResolve ? performance.now() : 0 let id: string | null = null const partial: Partial = {} - for (const plugin of this.getSortedPlugins('resolveId')) { - if (this._closed && !ssr) throwClosedServerError() + if (this._closed && this.environment?.options.dev.recoverable) + throwClosedServerError() if (!plugin.resolveId) continue if (skip?.has(plugin)) continue @@ -348,7 +336,7 @@ class PluginContainer { debugPluginResolve?.( timeFrom(pluginResolveStart), plugin.name, - prettifyUrl(id, this.config.root), + prettifyUrl(id, this.environment.config.root), ) // resolveId() is hookFirst - first non-null result is returned. @@ -376,22 +364,18 @@ class PluginContainer { } } - async load( - id: string, - options?: { - ssr?: boolean - }, - ): Promise { - const ssr = options?.ssr - const ctx = new LoadPluginContext(this, !!ssr) - + async load(id: string, options?: {}): Promise { + const ssr = this.environment.name !== 'client' + options = options ? { ...options, ssr } : { ssr } + const ctx = new LoadPluginContext(this) for (const plugin of this.getSortedPlugins('load')) { - if (this._closed && !ssr) throwClosedServerError() + if (this._closed && this.environment?.options.dev.recoverable) + throwClosedServerError() if (!plugin.load) continue ctx._plugin = plugin const handler = getHookHandler(plugin.load) const result = await this.handleHookPromise( - handler.call(ctx as any, id, { ssr }), + handler.call(ctx as any, id, options), ) if (result != null) { if (isObject(result)) { @@ -409,34 +393,28 @@ class PluginContainer { code: string, id: string, options?: { - ssr?: boolean inMap?: SourceDescription['map'] }, ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> { + const ssr = this.environment.name !== 'client' + const optionsWithSSR = options ? { ...options, ssr } : { ssr } const inMap = options?.inMap - const ssr = options?.ssr - const ctx = new TransformPluginContext( - this, - id, - code, - inMap as SourceMap, - !!ssr, - ) + const ctx = new TransformPluginContext(this, id, code, inMap as SourceMap) ctx._addedImports = this._getAddedImports(id) for (const plugin of this.getSortedPlugins('transform')) { - if (this._closed && !ssr) throwClosedServerError() + if (this._closed && this.environment?.options.dev.recoverable) + throwClosedServerError() if (!plugin.transform) continue ctx._updateActiveInfo(plugin, id, code) - const start = debugPluginTransform ? performance.now() : 0 let result: TransformResult | string | undefined const handler = getHookHandler(plugin.transform) try { result = await this.handleHookPromise( - handler.call(ctx as any, code, id, { ssr }), + handler.call(ctx as any, code, id, optionsWithSSR), ) } catch (e) { ctx.error(e) @@ -445,7 +423,7 @@ class PluginContainer { debugPluginTransform?.( timeFrom(start), plugin.name, - prettifyUrl(id, this.config.root), + prettifyUrl(id, this.environment.config.root), ) if (isObject(result)) { if (result.code !== undefined) { @@ -475,7 +453,7 @@ class PluginContainer { ): Promise { await this.hookParallel( 'watchChange', - (plugin) => this._getPluginContext(plugin, false), + (plugin) => this._getPluginContext(plugin), () => [id, change], ) } @@ -486,41 +464,38 @@ class PluginContainer { await Promise.allSettled(Array.from(this._processesing)) await this.hookParallel( 'buildEnd', - (plugin) => this._getPluginContext(plugin, false), + (plugin) => this._getPluginContext(plugin), () => [], ) await this.hookParallel( 'closeBundle', - (plugin) => this._getPluginContext(plugin, false), + (plugin) => this._getPluginContext(plugin), () => [], ) } } class PluginContext implements Omit { - protected _scan = false - protected _resolveSkips?: Set - protected _activeId: string | null = null - protected _activeCode: string | null = null - + ssr = false + _scan = false + _activeId: string | null = null + _activeCode: string | null = null + _resolveSkips?: Set meta: RollupPluginContext['meta'] + environment: Environment constructor( public _plugin: Plugin, - public _container: PluginContainer, - public ssr: boolean, + public _container: EnvironmentPluginContainer, ) { + this.environment = this._container.environment this.meta = this._container.minimalContext.meta } - parse(code: string, opts: any): ReturnType { + parse(code: string, opts: any) { return rollupParseAst(code, opts) } - getModuleInfo(id: string): ModuleInfo | null { - return this._container.getModuleInfo(id) - } - async resolve( id: string, importer?: string, @@ -530,7 +505,7 @@ class PluginContext implements Omit { isEntry?: boolean skipSelf?: boolean }, - ): ReturnType { + ) { let skip: Set | undefined if (options?.skipSelf !== false && this._plugin) { skip = new Set(this._resolveSkips) @@ -541,7 +516,6 @@ class PluginContext implements Omit { custom: options?.custom, isEntry: !!options?.isEntry, skip, - ssr: this.ssr, scan: this._scan, }) if (typeof out === 'string') out = { id: out } @@ -555,20 +529,15 @@ class PluginContext implements Omit { } & Partial>, ): Promise { // We may not have added this to our module graph yet, so ensure it exists - await this._container.moduleGraph?.ensureEntryFromUrl( - unwrapId(options.id), - this.ssr, - ) + await this._container.moduleGraph?.ensureEntryFromUrl(unwrapId(options.id)) // Not all options passed to this function make sense in the context of loading individual files, // but we can at least update the module info properties we support this._updateModuleInfo(options.id, options) - const loadResult = await this._container.load(options.id, { - ssr: this.ssr, - }) + const loadResult = await this._container.load(options.id) const code = typeof loadResult === 'object' ? loadResult?.code : loadResult if (code != null) { - await this._container.transform(code, options.id, { ssr: this.ssr }) + await this._container.transform(code, options.id) } const moduleInfo = this.getModuleInfo(options.id) @@ -579,7 +548,37 @@ class PluginContext implements Omit { return moduleInfo } - _updateModuleInfo(id: string, { meta }: { meta?: object | null }): void { + getModuleInfo(id: string): ModuleInfo | null { + const module = this._container.moduleGraph?.getModuleById(id) + if (!module) { + return null + } + if (!module.info) { + module.info = new Proxy( + { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo, + + // throw when an unsupported ModuleInfo property is accessed, + // so that incompatible plugins fail in a non-cryptic way. + { + get(info: any, key: string) { + if (key in info) { + return info[key] + } + // Don't throw an error when returning from an async function + if (key === 'then') { + return undefined + } + throw Error( + `[vite] The "${key}" property of ModuleInfo is not supported.`, + ) + }, + }, + ) + } + return module.info ?? null + } + + _updateModuleInfo(id: string, { meta }: { meta?: object | null }) { if (meta) { const moduleInfo = this.getModuleInfo(id) if (moduleInfo) { @@ -600,7 +599,7 @@ class PluginContext implements Omit { ensureWatchedFile( this._container.watcher, id, - this._container.config.root, + this.environment.config.root, ) } @@ -632,7 +631,7 @@ class PluginContext implements Omit { [colors.yellow(`warning: ${err.message}`)], false, ) - this._container.config.logger.warn(msg, { + this.environment.logger.warn(msg, { clear: true, timestamp: true, }) @@ -671,7 +670,7 @@ class PluginContext implements Omit { try { errLocation = numberToPos(this._activeCode, pos) } catch (err2) { - this._container.config.logger.error( + this.environment.logger.error( colors.red( `Error in error handler:\n${err2.stack || err2.message}\n`, ), @@ -753,7 +752,7 @@ class PluginContext implements Omit { } _warnIncompatibleMethod(method: string): void { - this._container.config.logger.warn( + this.environment.logger.warn( colors.cyan(`[plugin:${this._plugin.name}] `) + colors.yellow( `context method ${colors.bold( @@ -766,12 +765,11 @@ class PluginContext implements Omit { class ResolveIdContext extends PluginContext { constructor( - container: PluginContainer, - ssr: boolean, + container: EnvironmentPluginContainer, skip: Set | undefined, scan: boolean, ) { - super(null!, container, ssr) + super(null!, container) this._resolveSkips = skip this._scan = scan } @@ -780,8 +778,8 @@ class ResolveIdContext extends PluginContext { class LoadPluginContext extends PluginContext { _addedImports: Set | null = null - constructor(container: PluginContainer, ssr: boolean) { - super(null!, container, ssr) + constructor(container: EnvironmentPluginContainer) { + super(null!, container) } override addWatchFile(id: string): void { @@ -804,13 +802,12 @@ class TransformPluginContext combinedMap: SourceMap | { mappings: '' } | null = null constructor( - container: PluginContainer, + container: EnvironmentPluginContainer, id: string, code: string, - inMap: SourceMap | string | undefined, - ssr: boolean, + inMap?: SourceMap | string, ) { - super(container, ssr) + super(container) this.filename = id this.originalCode = code @@ -906,10 +903,109 @@ class TransformPluginContext } } -// We only expose the types but not the implementations export type { - PluginContainer, - PluginContext, + EnvironmentPluginContainer, TransformPluginContext, TransformResult, } + +// Backward compatibility +class PluginContainer { + constructor(private environments: Record) {} + + // Backward compatibility + // Users should call pluginContainer.resolveId (and load/transform) passing the environment they want to work with + // But there is code that is going to call it without passing an environment, or with the ssr flag to get the ssr environment + private _getEnvironment(options?: { + ssr?: boolean + environment?: Environment + }) { + return options?.environment + ? options.environment + : this.environments?.[options?.ssr ? 'ssr' : 'client'] + } + + private _getPluginContainer(options?: { + ssr?: boolean + environment?: Environment + }) { + return (this._getEnvironment(options) as DevEnvironment).pluginContainer! + } + + get options(): InputOptions { + return (this.environments.client as DevEnvironment).pluginContainer!.options + } + + async buildStart(_options?: InputOptions): Promise { + ;(this.environments.client as DevEnvironment).pluginContainer!.buildStart( + _options, + ) + } + + async resolveId( + rawId: string, + importer?: string, + options?: { + attributes?: Record + custom?: CustomPluginOptions + skip?: Set + ssr?: boolean + /** + * @internal + */ + scan?: boolean + isEntry?: boolean + }, + ): Promise { + return this._getPluginContainer(options).resolveId(rawId, importer, options) + } + + async load( + id: string, + options?: { + ssr?: boolean + }, + ): Promise { + return this._getPluginContainer(options).load(id, options) + } + + async transform( + code: string, + id: string, + options?: { + ssr?: boolean + environment?: Environment + inMap?: SourceDescription['map'] + }, + ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> { + return this._getPluginContainer(options).transform(code, id, options) + } + + async watchChange( + _id: string, + _change: { event: 'create' | 'update' | 'delete' }, + ): Promise { + // noop, watchChange is already called for each environment + } + + async close(): Promise { + // noop, close will be called for each environment + } +} + +/** + * server.pluginContainer compatibility + * + * The default environment is in buildStart, buildEnd, watchChange, and closeBundle hooks, + * which are called once for all environments, or when no environment is passed in other hooks. + * The ssrEnvironment is needed for backward compatibility when the ssr flag is passed without + * an environment. The defaultEnvironment in the main pluginContainer in the server should be + * the client environment for backward compatibility. + **/ +export function createPluginContainer( + environments: Record, +): PluginContainer { + return new PluginContainer(environments) +} + +export type { PluginContainer } diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 94e8c124041077..7a6223e1fc4141 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -6,7 +6,7 @@ import MagicString from 'magic-string' import { init, parse as parseImports } from 'es-module-lexer' import type { PartialResolvedId, SourceDescription, SourceMap } from 'rollup' import colors from 'picocolors' -import type { ModuleNode, ViteDevServer } from '..' +import type { EnvironmentModuleNode } from '../server/moduleGraph' import { createDebugger, ensureWatchedFile, @@ -18,17 +18,17 @@ import { stripBase, timeFrom, } from '../utils' +import { ssrParseImports, ssrTransform } from '../ssr/ssrTransform' import { checkPublicFile } from '../publicDir' -import { isDepsOptimizerEnabled } from '../config' -import { getDepsOptimizer, initDevSsrDepsOptimizer } from '../optimizer' import { cleanUrl, unwrapId } from '../../shared/utils' import { applySourcemapIgnoreList, extractSourcemapFromFile, injectSourcesContent, } from './sourcemap' -import { isFileServingAllowed } from './middlewares/static' +import { isFileLoadingAllowed } from './middlewares/static' import { throwClosedServerError } from './pluginContainer' +import type { DevEnvironment } from './environment' export const ERR_LOAD_URL = 'ERR_LOAD_URL' export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL' @@ -40,24 +40,46 @@ const debugCache = createDebugger('vite:cache') export interface TransformResult { code: string map: SourceMap | { mappings: '' } | null + ssr?: boolean etag?: string deps?: string[] dynamicDeps?: string[] } +// TODO: Rename to LoadOptions and move to /plugin.ts ? export interface TransformOptions { + /** + * @deprecated infered from environment + */ ssr?: boolean + /** + * TODO: should this be internal? + */ html?: boolean } +// TODO: This function could be moved to the DevEnvironment class. +// It was already using private fields from the server before, and it now does +// the same with environment._closing, environment._pendingRequests and +// environment._registerRequestProcessing. Maybe it makes sense to keep it in +// separate file to preserve the history or keep the DevEnvironment class cleaner, +// but conceptually this is: `environment.transformRequest(url, options)` + export function transformRequest( + environment: DevEnvironment, url: string, - server: ViteDevServer, options: TransformOptions = {}, ): Promise { - if (server._restartPromise && !options.ssr) throwClosedServerError() + // Backward compatibility when only `ssr` is passed + if (!options?.ssr) { + // Backward compatibility + options = { ...options, ssr: environment.name !== 'client' } + } - const cacheKey = (options.ssr ? 'ssr:' : options.html ? 'html:' : '') + url + if (environment._closing && environment?.options.dev.recoverable) + throwClosedServerError() + + const cacheKey = `${options.html ? 'html:' : ''}${url}` // This module may get invalidated while we are processing it. For example // when a full page reload is needed after the re-processing of pre-bundled @@ -81,10 +103,10 @@ export function transformRequest( // last time this module is invalidated const timestamp = Date.now() - const pending = server._pendingRequests.get(cacheKey) + const pending = environment._pendingRequests.get(cacheKey) if (pending) { - return server.moduleGraph - .getModuleByUrl(removeTimestampQuery(url), options.ssr) + return environment.moduleGraph + .getModuleByUrl(removeTimestampQuery(url)) .then((module) => { if (!module || pending.timestamp > module.lastInvalidationTimestamp) { // The pending request is still valid, we can safely reuse its result @@ -97,24 +119,24 @@ export function transformRequest( // First request has been invalidated, abort it to clear the cache, // then perform a new doTransform. pending.abort() - return transformRequest(url, server, options) + return transformRequest(environment, url, options) } }) } - const request = doTransform(url, server, options, timestamp) + const request = doTransform(environment, url, options, timestamp) // Avoid clearing the cache of future requests if aborted let cleared = false const clearCache = () => { if (!cleared) { - server._pendingRequests.delete(cacheKey) + environment._pendingRequests.delete(cacheKey) cleared = true } } // Cache the request and clear it once processing is done - server._pendingRequests.set(cacheKey, { + environment._pendingRequests.set(cacheKey, { request, timestamp, abort: clearCache, @@ -124,100 +146,89 @@ export function transformRequest( } async function doTransform( + environment: DevEnvironment, url: string, - server: ViteDevServer, options: TransformOptions, timestamp: number, ) { url = removeTimestampQuery(url) - const { config, pluginContainer } = server - const ssr = !!options.ssr + await environment.init() - if (ssr && isDepsOptimizerEnabled(config, true)) { - await initDevSsrDepsOptimizer(config, server) - } + const { pluginContainer } = environment - let module = await server.moduleGraph.getModuleByUrl(url, ssr) + let module = await environment.moduleGraph.getModuleByUrl(url) if (module) { // try use cache from url const cached = await getCachedTransformResult( + environment, url, module, - server, - ssr, timestamp, ) if (cached) return cached } + // TODO: Simplify const resolved = module ? undefined - : (await pluginContainer.resolveId(url, undefined, { ssr })) ?? undefined + : (await pluginContainer.resolveId(url, undefined)) ?? undefined // resolve const id = module?.id ?? resolved?.id ?? url - module ??= server.moduleGraph.getModuleById(id) + module ??= environment.moduleGraph.getModuleById(id) if (module) { // if a different url maps to an existing loaded id, make sure we relate this url to the id - await server.moduleGraph._ensureEntryFromUrl(url, ssr, undefined, resolved) + await environment.moduleGraph._ensureEntryFromUrl(url, undefined, resolved) // try use cache from id const cached = await getCachedTransformResult( + environment, url, module, - server, - ssr, timestamp, ) if (cached) return cached } const result = loadAndTransform( + environment, id, url, - server, options, timestamp, module, resolved, ) - if (!ssr) { - // Only register client requests, server.waitForRequestsIdle should - // have been called server.waitForClientRequestsIdle. We can rename - // it as part of the environment API work - const depsOptimizer = getDepsOptimizer(config, ssr) - if (!depsOptimizer?.isOptimizedDepFile(id)) { - server._registerRequestProcessing(id, () => result) - } + const { depsOptimizer } = environment + if (!depsOptimizer?.isOptimizedDepFile(id)) { + environment._registerRequestProcessing(id, () => result) } return result } async function getCachedTransformResult( + environment: DevEnvironment, url: string, - module: ModuleNode, - server: ViteDevServer, - ssr: boolean, + module: EnvironmentModuleNode, timestamp: number, ) { - const prettyUrl = debugCache ? prettifyUrl(url, server.config.root) : '' + const prettyUrl = debugCache ? prettifyUrl(url, environment.config.root) : '' // tries to handle soft invalidation of the module if available, // returns a boolean true is successful, or false if no handling is needed const softInvalidatedTransformResult = module && - (await handleModuleSoftInvalidation(module, ssr, timestamp, server)) + (await handleModuleSoftInvalidation(environment, module, timestamp)) if (softInvalidatedTransformResult) { debugCache?.(`[memory-hmr] ${prettyUrl}`) return softInvalidatedTransformResult } // check if we have a fresh cache - const cached = - module && (ssr ? module.ssrTransformResult : module.transformResult) + const cached = module?.transformResult if (cached) { debugCache?.(`[memory] ${prettyUrl}`) return cached @@ -225,29 +236,32 @@ async function getCachedTransformResult( } async function loadAndTransform( + environment: DevEnvironment, id: string, url: string, - server: ViteDevServer, options: TransformOptions, timestamp: number, - mod?: ModuleNode, + mod?: EnvironmentModuleNode, resolved?: PartialResolvedId, ) { - const { config, pluginContainer, moduleGraph } = server + const { config, pluginContainer } = environment const { logger } = config const prettyUrl = debugLoad || debugTransform ? prettifyUrl(url, config.root) : '' - const ssr = !!options.ssr - const file = cleanUrl(id) + const moduleGraph = environment.moduleGraph let code: string | null = null let map: SourceDescription['map'] = null // load const loadStart = debugLoad ? performance.now() : 0 - const loadResult = await pluginContainer.load(id, { ssr }) + const loadResult = await pluginContainer.load(id, options) + + // TODO: Replace this with pluginLoadFallback if (loadResult == null) { + const file = cleanUrl(id) + // if this is an html request and there is no load result, skip ahead to // SPA fallback. if (options.html && !id.endsWith('.html')) { @@ -258,7 +272,10 @@ async function loadAndTransform( // as string // only try the fallback if access is allowed, skip for out of root url // like /service-worker.js or /api/users - if (options.ssr || isFileServingAllowed(file, server)) { + if ( + environment.options.nodeCompatible || + isFileLoadingAllowed(config, file) + ) { try { code = await fsp.readFile(file, 'utf-8') debugLoad?.(`${timeFrom(loadStart)} [fs] ${prettyUrl}`) @@ -270,8 +287,8 @@ async function loadAndTransform( throw e } } - if (code != null) { - ensureWatchedFile(server.watcher, file, config.root) + if (code != null && environment.watcher) { + ensureWatchedFile(environment.watcher, file, config.root) } } if (code) { @@ -306,10 +323,8 @@ async function loadAndTransform( `should not be imported from source code. It can only be referenced ` + `via HTML tags.` : `Does the file exist?` - const importerMod: ModuleNode | undefined = server.moduleGraph.idToModuleMap - .get(id) - ?.importers.values() - .next().value + const importerMod: EnvironmentModuleNode | undefined = + moduleGraph.idToModuleMap.get(id)?.importers.values().next().value const importer = importerMod?.file || importerMod?.url const err: any = new Error( `Failed to load url ${url} (resolved id: ${id})${ @@ -320,16 +335,16 @@ async function loadAndTransform( throw err } - if (server._restartPromise && !ssr) throwClosedServerError() + if (environment._closing && environment.options.dev.recoverable) + throwClosedServerError() // ensure module in graph after successful load - mod ??= await moduleGraph._ensureEntryFromUrl(url, ssr, undefined, resolved) + mod ??= await moduleGraph._ensureEntryFromUrl(url, undefined, resolved) // transform const transformStart = debugTransform ? performance.now() : 0 const transformResult = await pluginContainer.transform(code, id, { inMap: map, - ssr, }) const originalCode = code if ( @@ -392,21 +407,27 @@ async function loadAndTransform( } } - if (server._restartPromise && !ssr) throwClosedServerError() + if (environment._closing && environment.options.dev.recoverable) + throwClosedServerError() - const result = - ssr && !server.config.experimental.skipSsrTransform - ? await server.ssrTransform(code, normalizedMap, url, originalCode) - : ({ - code, - map: normalizedMap, - etag: getEtag(code, { weak: true }), - } satisfies TransformResult) + const result = environment.options.dev.moduleRunnerTransform + ? await ssrTransform( + code, + normalizedMap, + url, + originalCode, + environment.config, + ) + : ({ + code, + map: normalizedMap, + etag: getEtag(code, { weak: true }), + } satisfies TransformResult) // Only cache the result if the module wasn't invalidated while it was // being processed, so it is re-processed next time if it is stale if (timestamp > mod.lastInvalidationTimestamp) - moduleGraph.updateModuleTransformResult(mod, result, ssr) + moduleGraph.updateModuleTransformResult(mod, result) return result } @@ -419,37 +440,40 @@ async function loadAndTransform( * - SSR: We don't need to change anything as `ssrLoadModule` controls it */ async function handleModuleSoftInvalidation( - mod: ModuleNode, - ssr: boolean, + environment: DevEnvironment, + mod: EnvironmentModuleNode, timestamp: number, - server: ViteDevServer, ) { - const transformResult = ssr ? mod.ssrInvalidationState : mod.invalidationState + const transformResult = mod.invalidationState // Reset invalidation state - if (ssr) mod.ssrInvalidationState = undefined - else mod.invalidationState = undefined + mod.invalidationState = undefined // Skip if not soft-invalidated if (!transformResult || transformResult === 'HARD_INVALIDATED') return - if (ssr ? mod.ssrTransformResult : mod.transformResult) { + if (mod.transformResult) { throw new Error( `Internal server error: Soft-invalidated module "${mod.url}" should not have existing transform result`, ) } let result: TransformResult - // For SSR soft-invalidation, no transformation is needed - if (ssr) { + // No transformation is needed if it's disabled manually + // This is primarily for backwards compatible SSR + if (!environment.options.injectInvalidationTimestamp) { result = transformResult } - // For client soft-invalidation, we need to transform each imports with new timestamps if available + // We need to transform each imports with new timestamps if available else { - await init const source = transformResult.code const s = new MagicString(source) - const [imports] = parseImports(source, mod.id || undefined) + const imports = transformResult.ssr + ? await ssrParseImports(mod.url, source) + : await (async () => { + await init + return parseImports(source, mod.id || undefined)[0] + })() for (const imp of imports) { let rawUrl = source.slice(imp.s, imp.e) @@ -463,9 +487,12 @@ async function handleModuleSoftInvalidation( const urlWithoutTimestamp = removeTimestampQuery(rawUrl) // hmrUrl must be derived the same way as importAnalysis const hmrUrl = unwrapId( - stripBase(removeImportQuery(urlWithoutTimestamp), server.config.base), + stripBase( + removeImportQuery(urlWithoutTimestamp), + environment.config.base, + ), ) - for (const importedMod of mod.clientImportedModules) { + for (const importedMod of mod.importedModules) { if (importedMod.url !== hmrUrl) continue if (importedMod.lastHMRTimestamp > 0) { const replacedUrl = injectQuery( @@ -477,9 +504,9 @@ async function handleModuleSoftInvalidation( s.overwrite(start, end, replacedUrl) } - if (imp.d === -1 && server.config.server.preTransformRequests) { + if (imp.d === -1 && environment.options.dev.preTransformRequests) { // pre-transform known direct imports - server.warmupRequest(hmrUrl, { ssr }) + environment.warmupRequest(hmrUrl) } break @@ -499,7 +526,7 @@ async function handleModuleSoftInvalidation( // Only cache the result if the module wasn't invalidated while it was // being processed, so it is re-processed next time if it is stale if (timestamp > mod.lastInvalidationTimestamp) - server.moduleGraph.updateModuleTransformResult(mod, result, ssr) + environment.moduleGraph.updateModuleTransformResult(mod, result) return result } diff --git a/packages/vite/src/node/server/warmup.ts b/packages/vite/src/node/server/warmup.ts index 33af7bb2a599a3..e39797b4fceac3 100644 --- a/packages/vite/src/node/server/warmup.ts +++ b/packages/vite/src/node/server/warmup.ts @@ -5,28 +5,24 @@ import colors from 'picocolors' import { FS_PREFIX } from '../constants' import { normalizePath } from '../utils' import type { ViteDevServer } from '../index' +import type { DevEnvironment } from './environment' export function warmupFiles(server: ViteDevServer): void { - const options = server.config.server.warmup - const root = server.config.root - - if (options?.clientFiles?.length) { - mapFiles(options.clientFiles, root).then((files) => { - for (const file of files) { - warmupFile(server, file, false) - } - }) - } - if (options?.ssrFiles?.length) { - mapFiles(options.ssrFiles, root).then((files) => { + const { root } = server.config + for (const environment of Object.values(server.environments)) { + mapFiles(environment.options.dev.warmup, root).then((files) => { for (const file of files) { - warmupFile(server, file, true) + warmupFile(server, server.environments.client, file) } }) } } -async function warmupFile(server: ViteDevServer, file: string, ssr: boolean) { +async function warmupFile( + server: ViteDevServer, + environment: DevEnvironment, + file: string, +) { // transform html with the `transformIndexHtml` hook as Vite internals would // pre-transform the imported JS modules linked. this may cause `transformIndexHtml` // plugins to be executed twice, but that's probably fine. @@ -38,7 +34,7 @@ async function warmupFile(server: ViteDevServer, file: string, ssr: boolean) { await server.transformIndexHtml(url, html) } catch (e) { // Unexpected error, log the issue but avoid an unhandled exception - server.config.logger.error( + environment.logger.error( `Pre-transform error (${colors.cyan(file)}): ${e.message}`, { error: e, @@ -51,7 +47,7 @@ async function warmupFile(server: ViteDevServer, file: string, ssr: boolean) { // for other files, pass it through `transformRequest` with warmup else { const url = fileToUrl(file, server.config.root) - await server.warmupRequest(url, { ssr }) + await environment.warmupRequest(url) } } diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index d0bffcdce4f8a0..2688709c2903ba 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -9,11 +9,11 @@ import colors from 'picocolors' import type { WebSocket as WebSocketRaw } from 'ws' import { WebSocketServer as WebSocketServerRaw_ } from 'ws' import type { WebSocket as WebSocketTypes } from 'dep-types/ws' -import type { CustomPayload, ErrorPayload, HMRPayload } from 'types/hmrPayload' +import type { ErrorPayload, HotPayload } from 'types/hotPayload' import type { InferCustomEventPayload } from 'types/customEvent' -import type { ResolvedConfig } from '..' +import type { HotChannelClient, ResolvedConfig } from '..' import { isObject } from '../utils' -import type { HMRChannel } from './hmr' +import type { HotChannel } from './hmr' import type { HttpServer } from '.' /* In Bun, the `ws` module is overridden to hook into the native code. Using the bundled `js` version @@ -31,7 +31,7 @@ export type WebSocketCustomListener = ( client: WebSocketClient, ) => void -export interface WebSocketServer extends HMRChannel { +export interface WebSocketServer extends HotChannel { /** * Listen on port and host */ @@ -61,15 +61,7 @@ export interface WebSocketServer extends HMRChannel { } } -export interface WebSocketClient { - /** - * Send event to the client - */ - send(payload: HMRPayload): void - /** - * Send custom event - */ - send(event: string, payload?: CustomPayload['data']): void +export interface WebSocketClient extends HotChannelClient { /** * The raw WebSocket instance * @advanced @@ -96,7 +88,6 @@ export function createWebSocketServer( ): WebSocketServer { if (config.server.ws === false) { return { - name: 'ws', get clients() { return new Set() }, @@ -218,7 +209,7 @@ export function createWebSocketServer( if (!clientsMap.has(socket)) { clientsMap.set(socket, { send: (...args) => { - let payload: HMRPayload + let payload: HotPayload if (typeof args[0] === 'string') { payload = { type: 'custom', @@ -243,7 +234,6 @@ export function createWebSocketServer( let bufferedError: ErrorPayload | null = null return { - name: 'ws', listen: () => { wsHttpServer?.listen(port, host) }, @@ -269,7 +259,7 @@ export function createWebSocketServer( }, send(...args: any[]) { - let payload: HMRPayload + let payload: HotPayload if (typeof args[0] === 'string') { payload = { type: 'custom', diff --git a/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts b/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts index b3f3c2364ef04b..f4e396ed9fe0e5 100644 --- a/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts +++ b/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts @@ -62,3 +62,45 @@ test('import.meta.filename/dirname returns same value with Node', async () => { expect(viteValue.dirname).toBe(path.dirname(filename)) expect(viteValue.filename).toBe(filename) }) + +test('virtual module invalidation', async () => { + const server = await createServer({ + configFile: false, + root, + logLevel: 'silent', + optimizeDeps: { + noDiscovery: true, + }, + plugins: [ + { + name: 'virtual-test', + resolveId(id) { + if (id === 'virtual:test') { + return '\0virtual:test' + } + }, + load(id) { + if (id === '\0virtual:test') { + return ` + globalThis.__virtual_test_state ??= 0; + globalThis.__virtual_test_state++; + export default globalThis.__virtual_test_state; + ` + } + }, + }, + ], + }) + await server.pluginContainer.buildStart({}) + + const mod1 = await server.ssrLoadModule('virtual:test') + expect(mod1.default).toEqual(1) + const mod2 = await server.ssrLoadModule('virtual:test') + expect(mod2.default).toEqual(1) + + const modNode = server.moduleGraph.getModuleById('\0virtual:test') + server.moduleGraph.invalidateModule(modNode!) + + const mod3 = await server.ssrLoadModule('virtual:test') + expect(mod3.default).toEqual(2) +}) diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index 60c0cb0a416b57..6af5e0d5f3af7b 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -1,27 +1,28 @@ import { pathToFileURL } from 'node:url' -import type { ModuleNode, TransformResult, ViteDevServer } from '..' -import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve' +import type { FetchResult } from 'vite/module-runner' +import type { EnvironmentModuleNode, TransformResult } from '..' import { tryNodeResolve } from '../plugins/resolve' import { isBuiltin, isExternalUrl, isFilePathESM } from '../utils' -import type { FetchResult } from '../../runtime/types' import { unwrapId } from '../../shared/utils' import { + MODULE_RUNNER_SOURCEMAPPING_SOURCE, SOURCEMAPPING_URL, - VITE_RUNTIME_SOURCEMAPPING_SOURCE, } from '../../shared/constants' import { genSourceMapUrl } from '../server/sourcemap' +import type { DevEnvironment } from '../server/environment' export interface FetchModuleOptions { + cached?: boolean inlineSourceMap?: boolean processSourceMap?>(map: T): T } /** - * Fetch module information for Vite runtime. + * Fetch module information for Vite runner. * @experimental */ export async function fetchModule( - server: ViteDevServer, + environment: DevEnvironment, url: string, importer?: string, options: FetchModuleOptions = {}, @@ -36,33 +37,35 @@ export async function fetchModule( } if (url[0] !== '.' && url[0] !== '/') { - const { - isProduction, - resolve: { dedupe, preserveSymlinks }, - root, - ssr, - } = server.config - const overrideConditions = ssr.resolve?.externalConditions || [] - - const resolveOptions: InternalResolveOptionsWithOverrideConditions = { - mainFields: ['main'], - conditions: [], - overrideConditions: [...overrideConditions, 'production', 'development'], - extensions: ['.js', '.cjs', '.json'], - dedupe, - preserveSymlinks, - isBuild: false, - isProduction, - root, - ssrConfig: ssr, - packageCache: server.config.packageCache, - } + const { isProduction, root } = environment.config + const { externalConditions, dedupe, preserveSymlinks } = + environment.options.resolve const resolved = tryNodeResolve( url, importer, - { ...resolveOptions, tryEsmOnly: true }, - false, + { + mainFields: ['main'], + conditions: [], + externalConditions, + external: [], + noExternal: [], + overrideConditions: [ + ...externalConditions, + 'production', + 'development', + ], + extensions: ['.js', '.cjs', '.json'], + dedupe, + preserveSymlinks, + isBuild: false, + isProduction, + root, + packageCache: environment.config.packageCache, + tryEsmOnly: true, + webCompatible: environment.options.webCompatible, + nodeCompatible: environment.options.nodeCompatible, + }, undefined, true, ) @@ -74,7 +77,7 @@ export async function fetchModule( throw err } const file = pathToFileURL(resolved.id).toString() - const type = isFilePathESM(resolved.id, server.config.packageCache) + const type = isFilePathESM(resolved.id, environment.config.packageCache) ? 'module' : 'commonjs' return { externalize: file, type } @@ -82,7 +85,15 @@ export async function fetchModule( url = unwrapId(url) - let result = await server.transformRequest(url, { ssr: true }) + let mod = await environment.moduleGraph.getModuleByUrl(url) + const cached = !!mod?.transformResult + + // if url is already cached, we can just confirm it's also cached on the server + if (options.cached && cached) { + return { cache: true } + } + + let result = await environment.transformRequest(url) if (!result) { throw new Error( @@ -93,7 +104,7 @@ export async function fetchModule( } // module entry should be created by transformRequest - const mod = await server.moduleGraph.getModuleByUrl(url, true) + mod ??= await environment.moduleGraph.getModuleByUrl(url) if (!mod) { throw new Error( @@ -111,7 +122,11 @@ export async function fetchModule( if (result.code[0] === '#') result.code = result.code.replace(/^#!.*/, (s) => ' '.repeat(s.length)) - return { code: result.code, file: mod.file } + return { + code: result.code, + file: mod.file, + invalidate: !cached, + } } const OTHER_SOURCE_MAP_REGEXP = new RegExp( @@ -120,7 +135,7 @@ const OTHER_SOURCE_MAP_REGEXP = new RegExp( ) function inlineSourceMap( - mod: ModuleNode, + mod: EnvironmentModuleNode, result: TransformResult, processSourceMap?: FetchModuleOptions['processSourceMap'], ) { @@ -130,7 +145,7 @@ function inlineSourceMap( if ( !map || !('version' in map) || - code.includes(VITE_RUNTIME_SOURCEMAPPING_SOURCE) + code.includes(MODULE_RUNNER_SOURCEMAPPING_SOURCE) ) return result @@ -142,7 +157,7 @@ function inlineSourceMap( const sourceMap = processSourceMap?.(map) || map result.code = `${code.trimEnd()}\n//# sourceURL=${ mod.id - }\n${VITE_RUNTIME_SOURCEMAPPING_SOURCE}\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(sourceMap)}\n` + }\n${MODULE_RUNNER_SOURCEMAPPING_SOURCE}\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(sourceMap)}\n` return result } diff --git a/packages/vite/src/node/ssr/index.ts b/packages/vite/src/node/ssr/index.ts index 3847e69544b2c0..aa65807eb13ed3 100644 --- a/packages/vite/src/node/ssr/index.ts +++ b/packages/vite/src/node/ssr/index.ts @@ -2,16 +2,33 @@ import type { DepOptimizationConfig } from '../optimizer' export type SSRTarget = 'node' | 'webworker' -export type SsrDepOptimizationOptions = DepOptimizationConfig +export type SsrDepOptimizationConfig = DepOptimizationConfig +/** + * @deprecated use environments.ssr + */ export interface SSROptions { + /** + * @deprecated use environment.resolve.noExternal + */ noExternal?: string | RegExp | (string | RegExp)[] | true + /** + * @deprecated use environment.resolve.external + */ external?: string[] | true /** * Define the target for the ssr build. The browser field in package.json * is ignored for node but used if webworker is the target + * + * if (ssr.target === 'webworker') { + * build.rollupOptions.entryFileNames = '[name].js' + * build.rollupOptions.inlineDynamicImports = (typeof input === 'string' || Object.keys(input).length === 1)) + * webCompatible = true + * } + * * @default 'node' + * @deprecated use environment.webCompatible */ target?: SSRTarget @@ -22,9 +39,13 @@ export interface SSROptions { * During dev: * explicit no external CJS dependencies are optimized by default * @experimental + * @deprecated */ - optimizeDeps?: SsrDepOptimizationOptions + optimizeDeps?: SsrDepOptimizationConfig + /** + * @deprecated + */ resolve?: { /** * Conditions that are used in the plugin pipeline. The default value is the root config's `resolve.conditions`. @@ -32,6 +53,7 @@ export interface SSROptions { * Use this to override the default ssr conditions for the ssr build. * * @default rootConfig.resolve.conditions + * @deprecated */ conditions?: string[] @@ -39,6 +61,7 @@ export interface SSROptions { * Conditions that are used during ssr import (including `ssrLoadModule`) of externalized dependencies. * * @default [] + * @deprecated */ externalConditions?: string[] } @@ -46,7 +69,7 @@ export interface SSROptions { export interface ResolvedSSROptions extends SSROptions { target: SSRTarget - optimizeDeps: SsrDepOptimizationOptions + optimizeDeps: SsrDepOptimizationConfig } export function resolveSSROptions( diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/default-string.ts b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/default-string.ts new file mode 100644 index 00000000000000..6b473bf83e5380 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/default-string.ts @@ -0,0 +1,3 @@ +const str: string = 'hello world' + +export default str diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.mjs b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.mjs new file mode 100644 index 00000000000000..bc617d0300e69d --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/worker.mjs @@ -0,0 +1,35 @@ +// @ts-check + +import { BroadcastChannel, parentPort } from 'node:worker_threads' +import { fileURLToPath } from 'node:url' +import { ESModulesEvaluator, ModuleRunner, RemoteRunnerTransport } from 'vite/module-runner' + +if (!parentPort) { + throw new Error('File "worker.js" must be run in a worker thread') +} + +const runner = new ModuleRunner( + { + root: fileURLToPath(new URL('./', import.meta.url)), + transport: new RemoteRunnerTransport({ + onMessage: listener => { + parentPort?.on('message', listener) + }, + send: message => { + parentPort?.postMessage(message) + } + }) + }, + new ESModulesEvaluator(), +) + +const channel = new BroadcastChannel('vite-worker') +channel.onmessage = async (message) => { + try { + const mod = await runner.import(message.data.id) + channel.postMessage({ result: mod.default }) + } catch (e) { + channel.postMessage({ error: e.stack }) + } +} +parentPort.postMessage('ready') \ No newline at end of file diff --git a/packages/vite/src/node/ssr/runtime/__tests__/package.json b/packages/vite/src/node/ssr/runtime/__tests__/package.json index 89fe86abc39d19..40a971f043f8a9 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/package.json +++ b/packages/vite/src/node/ssr/runtime/__tests__/package.json @@ -2,6 +2,9 @@ "name": "@vitejs/unit-runtime", "private": true, "version": "0.0.0", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, "dependencies": { "@vitejs/cjs-external": "link:./fixtures/cjs-external", "@vitejs/esm-external": "link:./fixtures/esm-external", diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-hmr.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-hmr.spec.ts index ccc822f543cefc..997df1f12095b7 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-hmr.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-hmr.spec.ts @@ -1,39 +1,39 @@ import { describe, expect } from 'vitest' -import { createViteRuntimeTester } from './utils' +import { createModuleRunnerTester } from './utils' describe( - 'vite-runtime hmr works as expected', + 'module runner hmr works as expected', async () => { - const it = await createViteRuntimeTester({ + const it = await createModuleRunnerTester({ server: { // override watch options because it's disabled by default watch: {}, }, }) - it('hmr options are defined', async ({ runtime }) => { - expect(runtime.hmrClient).toBeDefined() + it('hmr options are defined', async ({ runner }) => { + expect(runner.hmrClient).toBeDefined() - const mod = await runtime.executeUrl('/fixtures/hmr.js') + const mod = await runner.import('/fixtures/hmr.js') expect(mod).toHaveProperty('hmr') expect(mod.hmr).toHaveProperty('accept') }) - it('correctly populates hmr client', async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/d') + it('correctly populates hmr client', async ({ runner }) => { + const mod = await runner.import('/fixtures/d') expect(mod.d).toBe('a') const fixtureC = '/fixtures/c.ts' const fixtureD = '/fixtures/d.ts' - expect(runtime.hmrClient!.hotModulesMap.size).toBe(2) - expect(runtime.hmrClient!.dataMap.size).toBe(2) - expect(runtime.hmrClient!.ctxToListenersMap.size).toBe(2) + expect(runner.hmrClient!.hotModulesMap.size).toBe(2) + expect(runner.hmrClient!.dataMap.size).toBe(2) + expect(runner.hmrClient!.ctxToListenersMap.size).toBe(2) for (const fixture of [fixtureC, fixtureD]) { - expect(runtime.hmrClient!.hotModulesMap.has(fixture)).toBe(true) - expect(runtime.hmrClient!.dataMap.has(fixture)).toBe(true) - expect(runtime.hmrClient!.ctxToListenersMap.has(fixture)).toBe(true) + expect(runner.hmrClient!.hotModulesMap.has(fixture)).toBe(true) + expect(runner.hmrClient!.dataMap.has(fixture)).toBe(true) + expect(runner.hmrClient!.ctxToListenersMap.has(fixture)).toBe(true) } }) }, diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-no-hmr.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-no-hmr.spec.ts index ea2816756c927f..d4cf03c756c565 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-no-hmr.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-no-hmr.spec.ts @@ -1,8 +1,8 @@ import { describe, expect } from 'vitest' -import { createViteRuntimeTester } from './utils' +import { createModuleRunnerTester } from './utils' -describe('vite-runtime hmr works as expected', async () => { - const it = await createViteRuntimeTester({ +describe('module runner hmr works as expected', async () => { + const it = await createModuleRunnerTester({ server: { // override watch options because it's disabled by default watch: {}, @@ -10,10 +10,10 @@ describe('vite-runtime hmr works as expected', async () => { }, }) - it("hmr client is not defined if it's disabled", async ({ runtime }) => { - expect(runtime.hmrClient).toBeUndefined() + it("hmr client is not defined if it's disabled", async ({ runner }) => { + expect(runner.hmrClient).toBeUndefined() - const mod = await runtime.executeUrl('/fixtures/hmr.js') + const mod = await runner.import('/fixtures/hmr.js') expect(mod).toHaveProperty('hmr') expect(mod.hmr).toBeUndefined() }) diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts index bcf06bb91d4005..d6323eaf9daf5f 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-runtime.spec.ts @@ -3,42 +3,42 @@ import { posix, win32 } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect } from 'vitest' import { isWindows } from '../../../../shared/utils' -import { createViteRuntimeTester } from './utils' +import { createModuleRunnerTester } from './utils' const _URL = URL -describe('vite-runtime initialization', async () => { - const it = await createViteRuntimeTester() +describe('module runner initialization', async () => { + const it = await createModuleRunnerTester() - it('correctly runs ssr code', async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/simple.js') + it('correctly runs ssr code', async ({ runner }) => { + const mod = await runner.import('/fixtures/simple.js') expect(mod.test).toEqual('I am initialized') // loads the same module if id is a file url const fileUrl = new _URL('./fixtures/simple.js', import.meta.url) - const mod2 = await runtime.executeUrl(fileUrl.toString()) + const mod2 = await runner.import(fileUrl.toString()) expect(mod).toBe(mod2) // loads the same module if id is a file path const filePath = fileURLToPath(fileUrl) - const mod3 = await runtime.executeUrl(filePath) + const mod3 = await runner.import(filePath) expect(mod).toBe(mod3) }) - it('can load virtual modules as an entry point', async ({ runtime }) => { - const mod = await runtime.executeEntrypoint('virtual:test') + it('can load virtual modules as an entry point', async ({ runner }) => { + const mod = await runner.import('virtual:test') expect(mod.msg).toBe('virtual') }) - it('css is loaded correctly', async ({ runtime }) => { - const css = await runtime.executeUrl('/fixtures/test.css') + it('css is loaded correctly', async ({ runner }) => { + const css = await runner.import('/fixtures/test.css') expect(css.default).toMatchInlineSnapshot(` ".test { color: red; } " `) - const module = await runtime.executeUrl('/fixtures/test.module.css') + const module = await runner.import('/fixtures/test.module.css') expect(module).toMatchObject({ default: { test: expect.stringMatching(/^_test_/), @@ -47,8 +47,8 @@ describe('vite-runtime initialization', async () => { }) }) - it('assets are loaded correctly', async ({ runtime }) => { - const assets = await runtime.executeUrl('/fixtures/assets.js') + it('assets are loaded correctly', async ({ runner }) => { + const assets = await runner.import('/fixtures/assets.js') expect(assets).toMatchObject({ mov: '/fixtures/assets/placeholder.mov', txt: '/fixtures/assets/placeholder.txt', @@ -57,17 +57,17 @@ describe('vite-runtime initialization', async () => { }) }) - it('ids with Vite queries are loaded correctly', async ({ runtime }) => { - const raw = await runtime.executeUrl('/fixtures/simple.js?raw') + it('ids with Vite queries are loaded correctly', async ({ runner }) => { + const raw = await runner.import('/fixtures/simple.js?raw') expect(raw.default).toMatchInlineSnapshot(` "export const test = 'I am initialized' import.meta.hot?.accept() " `) - const url = await runtime.executeUrl('/fixtures/simple.js?url') + const url = await runner.import('/fixtures/simple.js?url') expect(url.default).toMatchInlineSnapshot(`"/fixtures/simple.js"`) - const inline = await runtime.executeUrl('/fixtures/test.css?inline') + const inline = await runner.import('/fixtures/test.css?inline') expect(inline.default).toMatchInlineSnapshot(` ".test { color: red; @@ -77,16 +77,16 @@ describe('vite-runtime initialization', async () => { }) it('modules with query strings are treated as different modules', async ({ - runtime, + runner, }) => { - const modSimple = await runtime.executeUrl('/fixtures/simple.js') - const modUrl = await runtime.executeUrl('/fixtures/simple.js?url') + const modSimple = await runner.import('/fixtures/simple.js') + const modUrl = await runner.import('/fixtures/simple.js?url') expect(modSimple).not.toBe(modUrl) expect(modUrl.default).toBe('/fixtures/simple.js') }) - it('exports is not modifiable', async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/simple.js') + it('exports is not modifiable', async ({ runner }) => { + const mod = await runner.import('/fixtures/simple.js') expect(Object.isSealed(mod)).toBe(true) expect(() => { mod.test = 'I am modified' @@ -110,11 +110,11 @@ describe('vite-runtime initialization', async () => { ) }) - it('throws the same error', async ({ runtime }) => { + it('throws the same error', async ({ runner }) => { expect.assertions(3) const s = Symbol() try { - await runtime.executeUrl('/fixtures/has-error.js') + await runner.import('/fixtures/has-error.js') } catch (e) { expect(e[s]).toBeUndefined() e[s] = true @@ -122,16 +122,15 @@ describe('vite-runtime initialization', async () => { } try { - await runtime.executeUrl('/fixtures/has-error.js') + await runner.import('/fixtures/has-error.js') } catch (e) { expect(e[s]).toBe(true) } }) - it('importing external cjs library checks exports', async ({ runtime }) => { - await expect(() => - runtime.executeUrl('/fixtures/cjs-external-non-existing.js'), - ).rejects.toThrowErrorMatchingInlineSnapshot(` + it('importing external cjs library checks exports', async ({ runner }) => { + await expect(() => runner.import('/fixtures/cjs-external-non-existing.js')) + .rejects.toThrowErrorMatchingInlineSnapshot(` [SyntaxError: [vite] Named export 'nonExisting' not found. The requested module '@vitejs/cjs-external' is a CommonJS module, which may not support all module.exports as named exports. CommonJS modules can always be imported via the default export, for example using: @@ -141,28 +140,28 @@ describe('vite-runtime initialization', async () => { `) // subsequent imports of the same external package should not throw if imports are correct await expect( - runtime.executeUrl('/fixtures/cjs-external-existing.js'), + runner.import('/fixtures/cjs-external-existing.js'), ).resolves.toMatchObject({ result: 'world', }) }) - it('importing external esm library checks exports', async ({ runtime }) => { + it('importing external esm library checks exports', async ({ runner }) => { await expect(() => - runtime.executeUrl('/fixtures/esm-external-non-existing.js'), + runner.import('/fixtures/esm-external-non-existing.js'), ).rejects.toThrowErrorMatchingInlineSnapshot( `[SyntaxError: [vite] The requested module '@vitejs/esm-external' does not provide an export named 'nonExisting']`, ) // subsequent imports of the same external package should not throw if imports are correct await expect( - runtime.executeUrl('/fixtures/esm-external-existing.js'), + runner.import('/fixtures/esm-external-existing.js'), ).resolves.toMatchObject({ result: 'world', }) }) - it("dynamic import doesn't produce duplicates", async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/dynamic-import.js') + it("dynamic import doesn't produce duplicates", async ({ runner }) => { + const mod = await runner.import('/fixtures/dynamic-import.js') const modules = await mod.initialize() // toBe checks that objects are actually the same, not just structually // using toEqual here would be a mistake because it chesk the structural difference @@ -172,14 +171,14 @@ describe('vite-runtime initialization', async () => { expect(modules.static).toBe(modules.dynamicAbsoluteExtension) }) - it('correctly imports a virtual module', async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/virtual.js') + it('correctly imports a virtual module', async ({ runner }) => { + const mod = await runner.import('/fixtures/virtual.js') expect(mod.msg0).toBe('virtual0') expect(mod.msg).toBe('virtual') }) - it('importing package from node_modules', async ({ runtime }) => { - const mod = (await runtime.executeUrl( + it('importing package from node_modules', async ({ runner }) => { + const mod = (await runner.import( '/fixtures/installed.js', )) as typeof import('tinyspy') const fn = mod.spy() @@ -187,17 +186,14 @@ describe('vite-runtime initialization', async () => { expect(fn.called).toBe(true) }) - it('importing native node package', async ({ runtime }) => { - const mod = await runtime.executeUrl('/fixtures/native.js') + it('importing native node package', async ({ runner }) => { + const mod = await runner.import('/fixtures/native.js') expect(mod.readdirSync).toBe(readdirSync) expect(mod.existsSync).toBe(existsSync) }) - it('correctly resolves module url', async ({ runtime, server }) => { - const { meta } = - await runtime.executeUrl( - '/fixtures/basic', - ) + it('correctly resolves module url', async ({ runner, server }) => { + const { meta } = await runner.import('/fixtures/basic') const basicUrl = new _URL('./fixtures/basic.js', import.meta.url).toString() expect(meta.url).toBe(basicUrl) diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts index fd8973235af0b6..cc97a44cc494ef 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts @@ -1,9 +1,9 @@ import { describe, expect } from 'vitest' -import type { ViteRuntime } from 'vite/runtime' -import { createViteRuntimeTester, editFile, resolvePath } from './utils' +import type { ModuleRunner } from 'vite/module-runner' +import { createModuleRunnerTester, editFile, resolvePath } from './utils' -describe('vite-runtime initialization', async () => { - const it = await createViteRuntimeTester( +describe('module runner initialization', async () => { + const it = await createModuleRunnerTester( {}, { sourcemapInterceptor: 'prepareStackTrace', @@ -18,32 +18,32 @@ describe('vite-runtime initialization', async () => { return err } } - const serializeStack = (runtime: ViteRuntime, err: Error) => { - return err.stack!.split('\n')[1].replace(runtime.options.root, '') + const serializeStack = (runner: ModuleRunner, err: Error) => { + return err.stack!.split('\n')[1].replace(runner.options.root, '') } - const serializeStackDeep = (runtime: ViteRuntime, err: Error) => { + const serializeStackDeep = (runtime: ModuleRunner, err: Error) => { return err .stack!.split('\n') .map((s) => s.replace(runtime.options.root, '')) } it('source maps are correctly applied to stack traces', async ({ - runtime, + runner, server, }) => { expect.assertions(3) const topLevelError = await getError(() => - runtime.executeUrl('/fixtures/has-error.js'), + runner.import('/fixtures/has-error.js'), ) - expect(serializeStack(runtime, topLevelError)).toBe( + expect(serializeStack(runner, topLevelError)).toBe( ' at /fixtures/has-error.js:2:7', ) const methodError = await getError(async () => { - const mod = await runtime.executeUrl('/fixtures/throws-error-method.ts') + const mod = await runner.import('/fixtures/throws-error-method.ts') mod.throwError() }) - expect(serializeStack(runtime, methodError)).toBe( + expect(serializeStack(runner, methodError)).toBe( ' at Module.throwError (/fixtures/throws-error-method.ts:6:9)', ) @@ -52,25 +52,25 @@ describe('vite-runtime initialization', async () => { resolvePath(import.meta.url, './fixtures/throws-error-method.ts'), (code) => '\n\n\n\n\n' + code + '\n', ) - runtime.moduleCache.clear() - server.moduleGraph.invalidateAll() + runner.moduleCache.clear() + server.environments.ssr.moduleGraph.invalidateAll() // TODO: environment? const methodErrorNew = await getError(async () => { - const mod = await runtime.executeUrl('/fixtures/throws-error-method.ts') + const mod = await runner.import('/fixtures/throws-error-method.ts') mod.throwError() }) - expect(serializeStack(runtime, methodErrorNew)).toBe( + expect(serializeStack(runner, methodErrorNew)).toBe( ' at Module.throwError (/fixtures/throws-error-method.ts:11:9)', ) }) - it('deep stacktrace', async ({ runtime }) => { + it('deep stacktrace', async ({ runner }) => { const methodError = await getError(async () => { - const mod = await runtime.executeUrl('/fixtures/has-error-deep.ts') + const mod = await runner.import('/fixtures/has-error-deep.ts') mod.main() }) - expect(serializeStackDeep(runtime, methodError).slice(0, 3)).toEqual([ + expect(serializeStackDeep(runner, methodError).slice(0, 3)).toEqual([ 'Error: crash', ' at crash (/fixtures/has-error-deep.ts:2:9)', ' at Module.main (/fixtures/has-error-deep.ts:6:3)', diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts new file mode 100644 index 00000000000000..70c519b1ad5925 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts @@ -0,0 +1,67 @@ +import { BroadcastChannel, Worker } from 'node:worker_threads' +import { describe, expect, it, onTestFinished } from 'vitest' +import { DevEnvironment } from '../../../server/environment' +import { createServer } from '../../../server' +import { RemoteEnvironmentTransport } from '../../..' + +describe('running module runner inside a worker', () => { + it('correctly runs ssr code', async () => { + expect.assertions(1) + const worker = new Worker( + new URL('./fixtures/worker.mjs', import.meta.url), + { + stdout: true, + }, + ) + await new Promise((resolve, reject) => { + worker.on('message', () => resolve()) + worker.on('error', reject) + }) + const server = await createServer({ + root: __dirname, + logLevel: 'error', + server: { + middlewareMode: true, + watch: null, + hmr: { + port: 9609, + }, + }, + environments: { + worker: { + dev: { + createEnvironment: (name, config) => { + return new DevEnvironment(name, config, { + runner: { + transport: new RemoteEnvironmentTransport({ + send: (data) => worker.postMessage(data), + onMessage: (handler) => worker.on('message', handler), + }), + }, + }) + }, + }, + }, + }, + }) + onTestFinished(() => { + server.close() + worker.terminate() + }) + const channel = new BroadcastChannel('vite-worker') + return new Promise((resolve, reject) => { + channel.onmessage = (event) => { + try { + expect((event as MessageEvent).data).toEqual({ + result: 'hello world', + }) + } catch (e) { + reject(e) + } finally { + resolve() + } + } + channel.postMessage({ id: './fixtures/default-string.ts' }) + }) + }) +}) diff --git a/packages/vite/src/node/ssr/runtime/__tests__/utils.ts b/packages/vite/src/node/ssr/runtime/__tests__/utils.ts index 7e14bb986e828a..9a91992aa32fa2 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/utils.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/utils.ts @@ -3,21 +3,23 @@ import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import type { TestAPI } from 'vitest' import { afterEach, beforeEach, test } from 'vitest' -import type { ViteRuntime } from 'vite/runtime' -import type { MainThreadRuntimeOptions } from '../mainThreadRuntime' +import type { ModuleRunner } from 'vite/module-runner' +import type { ServerModuleRunnerOptions } from '../serverModuleRunner' import type { ViteDevServer } from '../../../server' import type { InlineConfig } from '../../../config' import { createServer } from '../../../server' -import { createViteRuntime } from '../mainThreadRuntime' +import { createServerModuleRunner } from '../serverModuleRunner' +import type { DevEnvironment } from '../../../server/environment' interface TestClient { server: ViteDevServer - runtime: ViteRuntime + runner: ModuleRunner + environment: DevEnvironment } -export async function createViteRuntimeTester( +export async function createModuleRunnerTester( config: InlineConfig = {}, - runtimeConfig: MainThreadRuntimeOptions = {}, + runnerConfig: ServerModuleRunnerOptions = {}, ): Promise> { function waitForWatcher(server: ViteDevServer) { return new Promise((resolve) => { @@ -73,13 +75,14 @@ export async function createViteRuntimeTester( ], ...config, }) - t.runtime = await createViteRuntime(t.server, { + t.environment = t.server.environments.ssr + t.runner = createServerModuleRunner(t.environment, { hmr: { logger: false, }, // don't override by default so Vitest source maps are correct sourcemapInterceptor: false, - ...runtimeConfig, + ...runnerConfig, }) if (config.server?.watch) { await waitForWatcher(t.server) @@ -87,7 +90,7 @@ export async function createViteRuntimeTester( }) afterEach(async (t) => { - await t.runtime.destroy() + await t.runner.destroy() await t.server.close() }) diff --git a/packages/vite/src/node/ssr/runtime/mainThreadRuntime.ts b/packages/vite/src/node/ssr/runtime/mainThreadRuntime.ts deleted file mode 100644 index cbb8e3d8edfbdd..00000000000000 --- a/packages/vite/src/node/ssr/runtime/mainThreadRuntime.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { existsSync, readFileSync } from 'node:fs' -import { ESModulesRunner, ViteRuntime } from 'vite/runtime' -import type { ViteModuleRunner, ViteRuntimeOptions } from 'vite/runtime' -import type { ViteDevServer } from '../../server' -import type { HMRLogger } from '../../../shared/hmr' -import { ServerHMRConnector } from './serverHmrConnector' - -/** - * @experimental - */ -export interface MainThreadRuntimeOptions - extends Omit { - /** - * Disable HMR or configure HMR logger. - */ - hmr?: - | false - | { - logger?: false | HMRLogger - } - /** - * Provide a custom module runner. This controls how the code is executed. - */ - runner?: ViteModuleRunner -} - -function createHMROptions( - server: ViteDevServer, - options: MainThreadRuntimeOptions, -) { - if (server.config.server.hmr === false || options.hmr === false) { - return false - } - const connection = new ServerHMRConnector(server) - return { - connection, - logger: options.hmr?.logger, - } -} - -const prepareStackTrace = { - retrieveFile(id: string) { - if (existsSync(id)) { - return readFileSync(id, 'utf-8') - } - }, -} - -function resolveSourceMapOptions(options: MainThreadRuntimeOptions) { - if (options.sourcemapInterceptor != null) { - if (options.sourcemapInterceptor === 'prepareStackTrace') { - return prepareStackTrace - } - if (typeof options.sourcemapInterceptor === 'object') { - return { ...prepareStackTrace, ...options.sourcemapInterceptor } - } - return options.sourcemapInterceptor - } - if (typeof process !== 'undefined' && 'setSourceMapsEnabled' in process) { - return 'node' - } - return prepareStackTrace -} - -/** - * Create an instance of the Vite SSR runtime that support HMR. - * @experimental - */ -export async function createViteRuntime( - server: ViteDevServer, - options: MainThreadRuntimeOptions = {}, -): Promise { - const hmr = createHMROptions(server, options) - return new ViteRuntime( - { - ...options, - root: server.config.root, - fetchModule: server.ssrFetchModule, - hmr, - sourcemapInterceptor: resolveSourceMapOptions(options), - }, - options.runner || new ESModulesRunner(), - ) -} diff --git a/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts b/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts index b8bed32a8733c2..5a4000794ba15a 100644 --- a/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts +++ b/packages/vite/src/node/ssr/runtime/serverHmrConnector.ts @@ -1,13 +1,12 @@ -import type { CustomPayload, HMRPayload } from 'types/hmrPayload' -import type { HMRRuntimeConnection } from 'vite/runtime' -import type { ViteDevServer } from '../../server' -import type { HMRBroadcasterClient, ServerHMRChannel } from '../../server/hmr' +import type { CustomPayload, HotPayload } from 'types/hotPayload' +import type { ModuleRunnerHMRConnection } from 'vite/module-runner' +import type { HotChannelClient, ServerHotChannel } from '../../server/hmr' -class ServerHMRBroadcasterClient implements HMRBroadcasterClient { - constructor(private readonly hmrChannel: ServerHMRChannel) {} +class ServerHMRBroadcasterClient implements HotChannelClient { + constructor(private readonly hotChannel: ServerHotChannel) {} send(...args: any[]) { - let payload: HMRPayload + let payload: HotPayload if (typeof args[0] === 'string') { payload = { type: 'custom', @@ -22,7 +21,7 @@ class ServerHMRBroadcasterClient implements HMRBroadcasterClient { 'Cannot send non-custom events from the client to the server.', ) } - this.hmrChannel.send(payload) + this.hotChannel.send(payload) } } @@ -30,27 +29,18 @@ class ServerHMRBroadcasterClient implements HMRBroadcasterClient { * The connector class to establish HMR communication between the server and the Vite runtime. * @experimental */ -export class ServerHMRConnector implements HMRRuntimeConnection { - private handlers: ((payload: HMRPayload) => void)[] = [] - private hmrChannel: ServerHMRChannel +export class ServerHMRConnector implements ModuleRunnerHMRConnection { + private handlers: ((payload: HotPayload) => void)[] = [] private hmrClient: ServerHMRBroadcasterClient private connected = false - constructor(server: ViteDevServer) { - const hmrChannel = server.hot?.channels.find( - (c) => c.name === 'ssr', - ) as ServerHMRChannel - if (!hmrChannel) { - throw new Error( - "Your version of Vite doesn't support HMR during SSR. Please, use Vite 5.1 or higher.", - ) - } - this.hmrClient = new ServerHMRBroadcasterClient(hmrChannel) - hmrChannel.api.outsideEmitter.on('send', (payload: HMRPayload) => { + constructor(private hotChannel: ServerHotChannel) { + this.hmrClient = new ServerHMRBroadcasterClient(hotChannel) + hotChannel.api.outsideEmitter.on('send', (payload: HotPayload) => { this.handlers.forEach((listener) => listener(payload)) }) - this.hmrChannel = hmrChannel + this.hotChannel = hotChannel } isReady(): boolean { @@ -59,14 +49,14 @@ export class ServerHMRConnector implements HMRRuntimeConnection { send(message: string): void { const payload = JSON.parse(message) as CustomPayload - this.hmrChannel.api.innerEmitter.emit( + this.hotChannel.api.innerEmitter.emit( payload.event, payload.data, this.hmrClient, ) } - onUpdate(handler: (payload: HMRPayload) => void): void { + onUpdate(handler: (payload: HotPayload) => void): void { this.handlers.push(handler) handler({ type: 'connected' }) this.connected = true diff --git a/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts b/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts new file mode 100644 index 00000000000000..931bd07b80a5c7 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/serverModuleRunner.ts @@ -0,0 +1,103 @@ +import { existsSync, readFileSync } from 'node:fs' +import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' +import type { + ModuleEvaluator, + ModuleRunnerHMRConnection, + ModuleRunnerHmr, + ModuleRunnerOptions, +} from 'vite/module-runner' +import type { DevEnvironment } from '../../server/environment' +import type { ServerHotChannel } from '../../server/hmr' +import { ServerHMRConnector } from './serverHmrConnector' + +/** + * @experimental + */ +export interface ServerModuleRunnerOptions + extends Omit< + ModuleRunnerOptions, + 'root' | 'fetchModule' | 'hmr' | 'transport' + > { + /** + * Disable HMR or configure HMR logger. + */ + hmr?: + | false + | { + connection?: ModuleRunnerHMRConnection + logger?: ModuleRunnerHmr['logger'] + } + /** + * Provide a custom module evaluator. This controls how the code is executed. + */ + evaluator?: ModuleEvaluator +} + +function createHMROptions( + environment: DevEnvironment, + options: ServerModuleRunnerOptions, +) { + if (environment.config.server.hmr === false || options.hmr === false) { + return false + } + if (options.hmr?.connection) { + return { + connection: options.hmr.connection, + logger: options.hmr.logger, + } + } + if (!('api' in environment.hot)) return false + const connection = new ServerHMRConnector(environment.hot as ServerHotChannel) + return { + connection, + logger: options.hmr?.logger, + } +} + +const prepareStackTrace = { + retrieveFile(id: string) { + if (existsSync(id)) { + return readFileSync(id, 'utf-8') + } + }, +} + +function resolveSourceMapOptions(options: ServerModuleRunnerOptions) { + if (options.sourcemapInterceptor != null) { + if (options.sourcemapInterceptor === 'prepareStackTrace') { + return prepareStackTrace + } + if (typeof options.sourcemapInterceptor === 'object') { + return { ...prepareStackTrace, ...options.sourcemapInterceptor } + } + return options.sourcemapInterceptor + } + if (typeof process !== 'undefined' && 'setSourceMapsEnabled' in process) { + return 'node' + } + return prepareStackTrace +} + +/** + * Create an instance of the Vite SSR runtime that support HMR. + * @experimental + */ +export function createServerModuleRunner( + environment: DevEnvironment, + options: ServerModuleRunnerOptions = {}, +): ModuleRunner { + const hmr = createHMROptions(environment, options) + return new ModuleRunner( + { + ...options, + root: environment.config.root, + transport: { + fetchModule: (id, importer, options) => + environment.fetchModule(id, importer, options), + }, + hmr, + sourcemapInterceptor: resolveSourceMapOptions(options), + }, + options.evaluator || new ESModulesEvaluator(), + ) +} diff --git a/packages/vite/src/node/ssr/ssrFetchModule.ts b/packages/vite/src/node/ssr/ssrFetchModule.ts deleted file mode 100644 index d0e1c98cca2569..00000000000000 --- a/packages/vite/src/node/ssr/ssrFetchModule.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ViteDevServer } from '../server' -import type { FetchResult } from '../../runtime/types' -import { asyncFunctionDeclarationPaddingLineCount } from '../../shared/utils' -import { fetchModule } from './fetchModule' - -export function ssrFetchModule( - server: ViteDevServer, - id: string, - importer?: string, -): Promise { - return fetchModule(server, id, importer, { - processSourceMap(map) { - // this assumes that "new AsyncFunction" is used to create the module - return Object.assign({}, map, { - mappings: - ';'.repeat(asyncFunctionDeclarationPaddingLineCount) + map.mappings, - }) - }, - }) -} diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index 1a539d2ff58943..0d7223275fe7de 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -1,31 +1,9 @@ -import path from 'node:path' -import { pathToFileURL } from 'node:url' import colors from 'picocolors' +import type { ModuleRunner } from 'vite/module-runner' import type { ViteDevServer } from '../server' -import { isBuiltin, isExternalUrl, isFilePathESM } from '../utils' -import { transformRequest } from '../server/transformRequest' -import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve' -import { tryNodeResolve } from '../plugins/resolve' -import { genSourceMapUrl } from '../server/sourcemap' -import { - AsyncFunction, - asyncFunctionDeclarationPaddingLineCount, - isWindows, - unwrapId, -} from '../../shared/utils' -import { - type SSRImportBaseMetadata, - analyzeImportedModDifference, -} from '../../shared/ssrTransform' -import { SOURCEMAPPING_URL } from '../../shared/constants' -import { - ssrDynamicImportKey, - ssrExportAllKey, - ssrImportKey, - ssrImportMetaKey, - ssrModuleExportsKey, -} from './ssrTransform' +import { unwrapId } from '../../shared/utils' import { ssrFixStacktrace } from './ssrStacktrace' +import { createServerModuleRunner } from './runtime/serverModuleRunner' interface SSRContext { global: typeof globalThis @@ -33,232 +11,53 @@ interface SSRContext { type SSRModule = Record -interface NodeImportResolveOptions - extends InternalResolveOptionsWithOverrideConditions { - legacyProxySsrExternalModules?: boolean -} - -const pendingModules = new Map>() -const pendingImports = new Map() -const importErrors = new WeakMap() - export async function ssrLoadModule( url: string, server: ViteDevServer, - context: SSRContext = { global }, - urlStack: string[] = [], + _context: SSRContext = { global }, + _urlStack: string[] = [], fixStacktrace?: boolean, ): Promise { - url = unwrapId(url) + const runner = + server._ssrCompatModuleRunner || + (server._ssrCompatModuleRunner = createServerModuleRunner( + server.environments.ssr, + { + sourcemapInterceptor: false, + hmr: false, + }, + )) - // when we instantiate multiple dependency modules in parallel, they may - // point to shared modules. We need to avoid duplicate instantiation attempts - // by register every module as pending synchronously so that all subsequent - // request to that module are simply waiting on the same promise. - const pending = pendingModules.get(url) - if (pending) { - return pending - } + url = unwrapId(url) - const modulePromise = instantiateModule( - url, - server, - context, - urlStack, - fixStacktrace, - ) - pendingModules.set(url, modulePromise) - modulePromise - .catch(() => { - pendingImports.delete(url) - }) - .finally(() => { - pendingModules.delete(url) - }) - return modulePromise + return instantiateModule(url, runner, server, fixStacktrace) } async function instantiateModule( url: string, + runner: ModuleRunner, server: ViteDevServer, - context: SSRContext = { global }, - urlStack: string[] = [], fixStacktrace?: boolean, ): Promise { - const { moduleGraph } = server - const mod = await moduleGraph.ensureEntryFromUrl(url, true) + const environment = server.environments.ssr + const mod = await environment.moduleGraph.ensureEntryFromUrl(url) if (mod.ssrError) { throw mod.ssrError } - if (mod.ssrModule) { - return mod.ssrModule - } - const result = - mod.ssrTransformResult || - (await transformRequest(url, server, { ssr: true })) - if (!result) { - // TODO more info? is this even necessary? - throw new Error(`failed to load module for ssr: ${url}`) - } - - const ssrModule = { - [Symbol.toStringTag]: 'Module', - } - Object.defineProperty(ssrModule, '__esModule', { value: true }) - - // Tolerate circular imports by ensuring the module can be - // referenced before it's been instantiated. - mod.ssrModule = ssrModule - - // replace '/' with '\\' on Windows to match Node.js - const osNormalizedFilename = isWindows ? path.resolve(mod.file!) : mod.file! - - const ssrImportMeta = { - dirname: path.dirname(osNormalizedFilename), - filename: osNormalizedFilename, - // The filesystem URL, matching native Node.js modules - url: pathToFileURL(mod.file!).toString(), - } - - urlStack = urlStack.concat(url) - const isCircular = (url: string) => urlStack.includes(url) - - const { - isProduction, - resolve: { dedupe, preserveSymlinks }, - root, - ssr, - } = server.config - - const overrideConditions = ssr.resolve?.externalConditions || [] - - const resolveOptions: NodeImportResolveOptions = { - mainFields: ['main'], - conditions: [], - overrideConditions: [...overrideConditions, 'production', 'development'], - extensions: ['.js', '.cjs', '.json'], - dedupe, - preserveSymlinks, - isBuild: false, - isProduction, - root, - ssrConfig: ssr, - legacyProxySsrExternalModules: - server.config.legacy?.proxySsrExternalModules, - packageCache: server.config.packageCache, - } - - // Since dynamic imports can happen in parallel, we need to - // account for multiple pending deps and duplicate imports. - const pendingDeps: string[] = [] - - const ssrImport = async (dep: string, metadata?: SSRImportBaseMetadata) => { - try { - if (dep[0] !== '.' && dep[0] !== '/') { - return await nodeImport(dep, mod.file!, resolveOptions, metadata) - } - // convert to rollup URL because `pendingImports`, `moduleGraph.urlToModuleMap` requires that - dep = unwrapId(dep) - if (!isCircular(dep) && !pendingImports.get(dep)?.some(isCircular)) { - pendingDeps.push(dep) - if (pendingDeps.length === 1) { - pendingImports.set(url, pendingDeps) - } - const mod = await ssrLoadModule( - dep, - server, - context, - urlStack, - fixStacktrace, - ) - if (pendingDeps.length === 1) { - pendingImports.delete(url) - } else { - pendingDeps.splice(pendingDeps.indexOf(dep), 1) - } - // return local module to avoid race condition #5470 - return mod - } - return moduleGraph.urlToModuleMap.get(dep)?.ssrModule - } catch (err) { - // tell external error handler which mod was imported with error - importErrors.set(err, { importee: dep }) - - throw err - } - } - - const ssrDynamicImport = (dep: string) => { - // #3087 dynamic import vars is ignored at rewrite import path, - // so here need process relative path - if (dep[0] === '.') { - dep = path.posix.resolve(path.dirname(url), dep) - } - return ssrImport(dep, { isDynamicImport: true }) - } - - function ssrExportAll(sourceModule: any) { - for (const key in sourceModule) { - if (key !== 'default' && key !== '__esModule') { - Object.defineProperty(ssrModule, key, { - enumerable: true, - configurable: true, - get() { - return sourceModule[key] - }, - }) - } - } - } - - let sourceMapSuffix = '' - if (result.map && 'version' in result.map) { - const moduleSourceMap = Object.assign({}, result.map, { - mappings: - ';'.repeat(asyncFunctionDeclarationPaddingLineCount) + - result.map.mappings, - }) - sourceMapSuffix = `\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(moduleSourceMap)}` - } - try { - const initModule = new AsyncFunction( - `global`, - ssrModuleExportsKey, - ssrImportMetaKey, - ssrImportKey, - ssrDynamicImportKey, - ssrExportAllKey, - '"use strict";' + - result.code + - `\n//# sourceURL=${mod.id}${sourceMapSuffix}`, - ) - await initModule( - context.global, - ssrModule, - ssrImportMeta, - ssrImport, - ssrDynamicImport, - ssrExportAll, - ) - } catch (e) { + const exports = await runner.import(url) + mod.ssrModule = exports + return exports + } catch (e: any) { mod.ssrError = e - const errorData = importErrors.get(e) - if (e.stack && fixStacktrace) { - ssrFixStacktrace(e, moduleGraph) + ssrFixStacktrace(e, environment.moduleGraph) } - server.config.logger.error( - colors.red( - `Error when evaluating SSR module ${url}:` + - (errorData?.importee - ? ` failed to import "${errorData.importee}"` - : '') + - `\n|- ${e.stack}\n`, - ), + environment.logger.error( + colors.red(`Error when evaluating SSR module ${url}:\n|- ${e.stack}\n`), { timestamp: true, clear: server.config.clearScreen, @@ -268,82 +67,4 @@ async function instantiateModule( throw e } - - return Object.freeze(ssrModule) -} - -// In node@12+ we can use dynamic import to load CJS and ESM -async function nodeImport( - id: string, - importer: string, - resolveOptions: NodeImportResolveOptions, - metadata?: SSRImportBaseMetadata, -) { - let url: string - let filePath: string | undefined - if (id.startsWith('data:') || isExternalUrl(id) || isBuiltin(id)) { - url = id - } else { - const resolved = tryNodeResolve( - id, - importer, - { ...resolveOptions, tryEsmOnly: true }, - false, - undefined, - true, - ) - if (!resolved) { - const err: any = new Error( - `Cannot find module '${id}' imported from '${importer}'`, - ) - err.code = 'ERR_MODULE_NOT_FOUND' - throw err - } - filePath = resolved.id - url = pathToFileURL(resolved.id).toString() - } - - const mod = await import(url) - - if (resolveOptions.legacyProxySsrExternalModules) { - return proxyESM(mod) - } else if (filePath) { - analyzeImportedModDifference( - mod, - id, - isFilePathESM(filePath, resolveOptions.packageCache) - ? 'module' - : undefined, - metadata, - ) - return mod - } else { - return mod - } -} - -// rollup-style default import interop for cjs -function proxyESM(mod: any) { - // This is the only sensible option when the exports object is a primitive - if (isPrimitive(mod)) return { default: mod } - - let defaultExport = 'default' in mod ? mod.default : mod - - if (!isPrimitive(defaultExport) && '__esModule' in defaultExport) { - mod = defaultExport - if ('default' in defaultExport) { - defaultExport = defaultExport.default - } - } - - return new Proxy(mod, { - get(mod, prop) { - if (prop === 'default') return defaultExport - return mod[prop] ?? defaultExport?.[prop] - }, - }) -} - -function isPrimitive(value: any) { - return !value || (typeof value !== 'object' && typeof value !== 'function') } diff --git a/packages/vite/src/node/ssr/ssrStacktrace.ts b/packages/vite/src/node/ssr/ssrStacktrace.ts index a98af4dd94bb74..18489224ea4af6 100644 --- a/packages/vite/src/node/ssr/ssrStacktrace.ts +++ b/packages/vite/src/node/ssr/ssrStacktrace.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping' -import type { ModuleGraph } from '../server/moduleGraph' +import type { EnvironmentModuleGraph } from '..' let offset: number @@ -22,7 +22,7 @@ function calculateOffsetOnce() { export function ssrRewriteStacktrace( stack: string, - moduleGraph: ModuleGraph, + moduleGraph: EnvironmentModuleGraph, ): string { calculateOffsetOnce() return stack @@ -33,8 +33,8 @@ export function ssrRewriteStacktrace( (input, varName, id, line, column) => { if (!id) return input - const mod = moduleGraph.idToModuleMap.get(id) - const rawSourceMap = mod?.ssrTransformResult?.map + const mod = moduleGraph.getModuleById(id) + const rawSourceMap = mod?.transformResult?.map if (!rawSourceMap) { return input @@ -86,7 +86,10 @@ export function rebindErrorStacktrace(e: Error, stacktrace: string): void { const rewroteStacktraces = new WeakSet() -export function ssrFixStacktrace(e: Error, moduleGraph: ModuleGraph): void { +export function ssrFixStacktrace( + e: Error, + moduleGraph: EnvironmentModuleGraph, +): void { if (!e.stack) return // stacktrace shouldn't be rewritten more than once if (rewroteStacktraces.has(e)) return diff --git a/packages/vite/src/node/ssr/ssrTransform.ts b/packages/vite/src/node/ssr/ssrTransform.ts index 61849d0e3ca4f2..0ba9dd09ed00b7 100644 --- a/packages/vite/src/node/ssr/ssrTransform.ts +++ b/packages/vite/src/node/ssr/ssrTransform.ts @@ -2,8 +2,10 @@ import path from 'node:path' import MagicString from 'magic-string' import type { SourceMap } from 'rollup' import type { + CallExpression, Function as FunctionNode, Identifier, + Literal, Pattern, Property, VariableDeclaration, @@ -13,6 +15,7 @@ import { extract_names as extractNames } from 'periscopic' import { walk as eswalk } from 'estree-walker' import type { RawSourceMap } from '@ampproject/remapping' import { parseAstAsync as rollupParseAstAsync } from 'rollup/parseAst' +import type { ImportSpecifier } from 'es-module-lexer' import type { TransformResult } from '../server/transformRequest' import { combineSourcemaps, isDefined } from '../utils' import { isJSONRequest } from '../plugins/json' @@ -23,7 +26,7 @@ type Node = _Node & { end: number } -interface TransformOptions { +export interface ModuleRunnerTransformOptions { json?: { stringify?: boolean } @@ -37,12 +40,14 @@ export const ssrImportMetaKey = `__vite_ssr_import_meta__` const hashbangRE = /^#!.*\n/ +// TODO: Should we rename to moduleRunnerTransform? + export async function ssrTransform( code: string, inMap: SourceMap | { mappings: '' } | null, url: string, originalCode: string, - options?: TransformOptions, + options?: ModuleRunnerTransformOptions, // TODO: Should we export two functions instead of using options here? ): Promise { if (options?.json?.stringify && isJSONRequest(url)) { return ssrTransformJSON(code, inMap) @@ -59,6 +64,7 @@ async function ssrTransformJSON( map: inMap, deps: [], dynamicDeps: [], + ssr: true, } } @@ -322,11 +328,58 @@ async function ssrTransformScript( return { code: s.toString(), map, + ssr: true, deps: [...deps], dynamicDeps: [...dynamicDeps], } } +export async function ssrParseImports( + url: string, + code: string, +): Promise { + let ast: any + try { + ast = await rollupParseAstAsync(code) + } catch (err) { + if (!err.loc || !err.loc.line) throw err + const line = err.loc.line + throw new Error( + `Parse failure: ${ + err.message + }\nAt file: ${url}\nContents of line ${line}: ${ + code.split('\n')[line - 1] + }`, + ) + } + const imports: ImportSpecifier[] = [] + eswalk(ast, { + enter(_n, parent) { + if (_n.type !== 'Identifier') return + const node = _n as Node & Identifier + const isStaticImport = node.name === ssrImportKey + const isDynamicImport = node.name === ssrDynamicImportKey + if (isStaticImport || isDynamicImport) { + // this is a standardised output, so we can safely assume the parent and arguments + const importExpression = parent as Node & CallExpression + const importLiteral = importExpression.arguments[0] as Node & Literal + + imports.push({ + n: importLiteral.value as string | undefined, + s: importLiteral.start, + e: importLiteral.end, + se: importExpression.start, + ss: importExpression.end, + t: isStaticImport ? 2 : 1, + d: isDynamicImport ? importLiteral.start : -1, + a: -1, // not used + }) + } + }, + }) + return imports +} + interface Visitors { onIdentifier: ( node: Identifier & { diff --git a/packages/vite/src/node/tsconfig.json b/packages/vite/src/node/tsconfig.json index db8e56fa8449c5..dfbb49c465ee1d 100644 --- a/packages/vite/src/node/tsconfig.json +++ b/packages/vite/src/node/tsconfig.json @@ -1,12 +1,19 @@ { "extends": "../../tsconfig.base.json", - "include": ["./", "../runtime", "../dep-types", "../types", "constants.ts"], - "exclude": ["../**/__tests__"], + "include": [ + "./", + "../module-runner", + "../dep-types", + "./__tests_dts__", + "../types", + "constants.ts" + ], + "exclude": ["../**/__tests__/"], "compilerOptions": { "lib": ["ESNext", "DOM"], "stripInternal": true, "paths": { - "vite/runtime": ["../runtime"] + "vite/module-runner": ["../module-runner"] } } } diff --git a/packages/vite/src/node/typeUtils.ts b/packages/vite/src/node/typeUtils.ts new file mode 100644 index 00000000000000..83acb5f5089357 --- /dev/null +++ b/packages/vite/src/node/typeUtils.ts @@ -0,0 +1,22 @@ +import type { + ObjectHook, + Plugin as RollupPlugin, + PluginContext as RollupPluginContext, +} from 'rollup' + +export type NonNeverKeys = { + [K in keyof T]: T[K] extends never ? never : K +}[keyof T] + +export type GetHookContextMap = { + [K in keyof Plugin]-?: Plugin[K] extends ObjectHook + ? T extends (this: infer This, ...args: any[]) => any + ? This extends RollupPluginContext + ? This + : never + : never + : never +} + +type RollupPluginHooksContext = GetHookContextMap +export type RollupPluginHooks = NonNeverKeys diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 048508a1bad7b5..3b39734f76db1c 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -30,7 +30,7 @@ import { loopbackHosts, wildcardHosts, } from './constants' -import type { DepOptimizationConfig } from './optimizer' +import type { DepOptimizationOptions } from './optimizer' import type { ResolvedConfig } from './config' import type { ResolvedServerUrls, ViteDevServer } from './server' import type { PreviewServer } from './preview' @@ -122,7 +122,7 @@ export function moduleListContains( export function isOptimizable( id: string, - optimizeDeps: DepOptimizationConfig, + optimizeDeps: DepOptimizationOptions, ): boolean { const { extensions } = optimizeDeps return ( @@ -1095,7 +1095,7 @@ function mergeConfigRecursively( merged[key] = [].concat(existing, value) continue } else if ( - key === 'noExternal' && + key === 'noExternal' && // TODO: environments rootPath === 'ssr' && (existing === true || value === true) ) { diff --git a/packages/vite/src/shared/constants.ts b/packages/vite/src/shared/constants.ts index 7c0e685d5abf6b..a12c674cc98ed6 100644 --- a/packages/vite/src/shared/constants.ts +++ b/packages/vite/src/shared/constants.ts @@ -19,5 +19,5 @@ export const NULL_BYTE_PLACEHOLDER = `__x00__` export let SOURCEMAPPING_URL = 'sourceMa' SOURCEMAPPING_URL += 'ppingURL' -export const VITE_RUNTIME_SOURCEMAPPING_SOURCE = - '//# sourceMappingSource=vite-runtime' +export const MODULE_RUNNER_SOURCEMAPPING_SOURCE = + '//# sourceMappingSource=vite-generated' diff --git a/packages/vite/src/shared/hmr.ts b/packages/vite/src/shared/hmr.ts index 0f2cb23b4ad71f..919d1054b5d9b2 100644 --- a/packages/vite/src/shared/hmr.ts +++ b/packages/vite/src/shared/hmr.ts @@ -1,4 +1,4 @@ -import type { Update } from 'types/hmrPayload' +import type { Update } from 'types/hotPayload' import type { ModuleNamespace, ViteHotContext } from 'types/hot' import type { InferCustomEventPayload } from 'types/customEvent' @@ -111,9 +111,12 @@ export class HMRContext implements ViteHotContext { path: this.ownerPath, message, }) - this.send('vite:invalidate', { path: this.ownerPath, message }) + this.send('vite:invalidate', { + path: this.ownerPath, + message, + }) this.hmrClient.logger.debug( - `[vite] invalidate ${this.ownerPath}${message ? `: ${message}` : ''}`, + `invalidate ${this.ownerPath}${message ? `: ${message}` : ''}`, ) } @@ -252,7 +255,7 @@ export class HMRClient { this.logger.error(err) } this.logger.error( - `[hmr] Failed to reload ${path}. ` + + `Failed to reload ${path}. ` + `This could be due to syntax errors or importing non-existent ` + `modules. (see errors above)`, ) @@ -313,7 +316,7 @@ export class HMRClient { ) } const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}` - this.logger.debug(`[vite] hot updated: ${loggedPath}`) + this.logger.debug(`hot updated: ${loggedPath}`) } } } diff --git a/packages/vite/src/shared/tsconfig.json b/packages/vite/src/shared/tsconfig.json index a7f7890f1d0e7b..96451a95759b42 100644 --- a/packages/vite/src/shared/tsconfig.json +++ b/packages/vite/src/shared/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "include": ["./", "../dep-types", "../types"], - "exclude": ["**/__tests__"], + "exclude": ["**/__tests__", "**/__tests_dts__"], "compilerOptions": { "lib": ["ESNext", "DOM"], "stripInternal": true diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index 9202738f8227fb..eee1544dbd4d73 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -3,7 +3,7 @@ import type { FullReloadPayload, PrunePayload, UpdatePayload, -} from './hmrPayload' +} from './hotPayload' export interface CustomEventMap { 'vite:beforeUpdate': UpdatePayload diff --git a/packages/vite/types/hmrPayload.d.ts b/packages/vite/types/hotPayload.d.ts similarity index 89% rename from packages/vite/types/hmrPayload.d.ts rename to packages/vite/types/hotPayload.d.ts index 79dc349d3c880c..7c7f81f6cc1daa 100644 --- a/packages/vite/types/hmrPayload.d.ts +++ b/packages/vite/types/hotPayload.d.ts @@ -1,4 +1,6 @@ -export type HMRPayload = +/** @deprecated use HotPayload */ +export type HMRPayload = HotPayload +export type HotPayload = | ConnectedPayload | UpdatePayload | FullReloadPayload @@ -25,7 +27,7 @@ export interface Update { /** @internal */ isWithinCircularImport?: boolean /** @internal */ - ssrInvalidates?: string[] + invalidates?: string[] } export interface PrunePayload { diff --git a/playground/environment-react-ssr/__tests__/basic.spec.ts b/playground/environment-react-ssr/__tests__/basic.spec.ts new file mode 100644 index 00000000000000..4b98b37a2394f7 --- /dev/null +++ b/playground/environment-react-ssr/__tests__/basic.spec.ts @@ -0,0 +1,9 @@ +import { test } from 'vitest' +import { page } from '~utils' + +test('basic', async () => { + await page.getByText('hydrated: true').isVisible() + await page.getByText('Count: 0').isVisible() + await page.getByRole('button', { name: '+' }).click() + await page.getByText('Count: 1').isVisible() +}) diff --git a/playground/environment-react-ssr/index.html b/playground/environment-react-ssr/index.html new file mode 100644 index 00000000000000..9f4d44a675c1b1 --- /dev/null +++ b/playground/environment-react-ssr/index.html @@ -0,0 +1,14 @@ + + + + + environment-react-ssr + + + + + + diff --git a/playground/environment-react-ssr/package.json b/playground/environment-react-ssr/package.json new file mode 100644 index 00000000000000..77228c8054c5c8 --- /dev/null +++ b/playground/environment-react-ssr/package.json @@ -0,0 +1,17 @@ +{ + "name": "@vitejs/test-environment-react-ssr", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build --all", + "preview": "vite preview" + }, + "devDependencies": { + "@types/react": "^18.2.73", + "@types/react-dom": "^18.2.23", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/playground/environment-react-ssr/src/entry-client.tsx b/playground/environment-react-ssr/src/entry-client.tsx new file mode 100644 index 00000000000000..e33d677abfbab2 --- /dev/null +++ b/playground/environment-react-ssr/src/entry-client.tsx @@ -0,0 +1,12 @@ +import ReactDomClient from 'react-dom/client' +import React from 'react' +import Root from './root' + +async function main() { + const el = document.getElementById('root') + React.startTransition(() => { + ReactDomClient.hydrateRoot(el!, ) + }) +} + +main() diff --git a/playground/environment-react-ssr/src/entry-server.tsx b/playground/environment-react-ssr/src/entry-server.tsx new file mode 100644 index 00000000000000..9df5ef336b2c9f --- /dev/null +++ b/playground/environment-react-ssr/src/entry-server.tsx @@ -0,0 +1,24 @@ +import ReactDomServer from 'react-dom/server' +import type { Connect, ViteDevServer } from 'vite' +import Root from './root' + +const hanlder: Connect.NextHandleFunction = async (_req, res) => { + const ssrHtml = ReactDomServer.renderToString() + let html = await importHtml() + html = html.replace(//, `
${ssrHtml}
`) + res.setHeader('content-type', 'text/html').end(html) +} + +export default hanlder + +declare let __globalServer: ViteDevServer + +async function importHtml() { + if (import.meta.env.DEV) { + const mod = await import('/index.html?raw') + return __globalServer.transformIndexHtml('/', mod.default) + } else { + const mod = await import('/dist/client/index.html?raw') + return mod.default + } +} diff --git a/playground/environment-react-ssr/src/root.tsx b/playground/environment-react-ssr/src/root.tsx new file mode 100644 index 00000000000000..3d077cafb892ba --- /dev/null +++ b/playground/environment-react-ssr/src/root.tsx @@ -0,0 +1,19 @@ +import React from 'react' + +export default function Root() { + const [count, setCount] = React.useState(0) + + const [hydrated, setHydrated] = React.useState(false) + React.useEffect(() => { + setHydrated(true) + }, []) + + return ( +
+
hydrated: {String(hydrated)}
+
Count: {count}
+ + +
+ ) +} diff --git a/playground/environment-react-ssr/tsconfig.json b/playground/environment-react-ssr/tsconfig.json new file mode 100644 index 00000000000000..be3ffda527ca91 --- /dev/null +++ b/playground/environment-react-ssr/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "jsx": "react-jsx" + } +} diff --git a/playground/environment-react-ssr/vite.config.ts b/playground/environment-react-ssr/vite.config.ts new file mode 100644 index 00000000000000..96c193677316a3 --- /dev/null +++ b/playground/environment-react-ssr/vite.config.ts @@ -0,0 +1,90 @@ +import { + type Connect, + type Plugin, + type PluginOption, + createServerModuleRunner, + defineConfig, +} from 'vite' + +export default defineConfig((env) => ({ + clearScreen: false, + appType: 'custom', + plugins: [ + vitePluginSsrMiddleware({ + entry: '/src/entry-server', + preview: new URL('./dist/server/index.js', import.meta.url).toString(), + }), + { + name: 'global-server', + configureServer(server) { + Object.assign(globalThis, { __globalServer: server }) + }, + }, + ], + environments: { + client: { + build: { + minify: false, + sourcemap: true, + outDir: 'dist/client', + }, + }, + ssr: { + build: { + outDir: 'dist/server', + // [feedback] + // is this still meant to be used? + // for example, `ssr: true` seems to make `minify: false` automatically + // and also externalization. + ssr: true, + rollupOptions: { + input: { + index: '/src/entry-server', + }, + }, + }, + }, + }, + + builder: { + async buildApp(builder) { + await builder.build(builder.environments.client) + await builder.build(builder.environments.ssr) + }, + }, +})) + +// vavite-style ssr middleware plugin +export function vitePluginSsrMiddleware({ + entry, + preview, +}: { + entry: string + preview?: string +}): PluginOption { + const plugin: Plugin = { + name: vitePluginSsrMiddleware.name, + + configureServer(server) { + const runner = createServerModuleRunner(server.environments.ssr) + const handler: Connect.NextHandleFunction = async (req, res, next) => { + try { + const mod = await runner.import(entry) + await mod['default'](req, res, next) + } catch (e) { + next(e) + } + } + return () => server.middlewares.use(handler) + }, + + async configurePreviewServer(server) { + if (preview) { + const mod = await import(preview) + return () => server.middlewares.use(mod.default) + } + return + }, + } + return [plugin] +} diff --git a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts index 6a2b3763b3ffec..9e51525766e2ae 100644 --- a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts +++ b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts @@ -4,8 +4,8 @@ import { dirname, posix, resolve } from 'node:path' import EventEmitter from 'node:events' import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest' import type { InlineConfig, Logger, ViteDevServer } from 'vite' -import { createServer, createViteRuntime } from 'vite' -import type { ViteRuntime } from 'vite/runtime' +import { createServer, createServerModuleRunner } from 'vite' +import type { ModuleRunner } from 'vite/module-runner' import type { RollupError } from 'rollup' import { addFile, @@ -19,7 +19,7 @@ import { let server: ViteDevServer const clientLogs: string[] = [] const serverLogs: string[] = [] -let runtime: ViteRuntime +let runner: ModuleRunner const logsEmitter = new EventEmitter() @@ -54,7 +54,7 @@ const updated = (file: string, via?: string) => { describe('hmr works correctly', () => { beforeAll(async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') }) test('should connect', async () => { @@ -297,27 +297,28 @@ describe('hmr works correctly', () => { // expect((await page.$$('link')).length).toBe(1) // }) - // #2255 - test('importing reloaded', async () => { - const outputEle = () => hmr('.importing-reloaded') + // #2255 - not applicable to SSR becaue invlaidateModule expects the module + // to always be reloaded again + // test('importing reloaded', async () => { + // const outputEle = () => hmr('.importing-reloaded') - await untilUpdated(outputEle, ['a.js: a0', 'b.js: b0,a0'].join('
')) + // await untilUpdated(outputEle, ['a.js: a0', 'b.js: b0,a0'].join('
')) - editFile('importing-updated/a.js', (code) => code.replace("'a0'", "'a1'")) - await untilUpdated( - outputEle, - ['a.js: a0', 'b.js: b0,a0', 'a.js: a1'].join('
'), - ) + // editFile('importing-updated/a.js', (code) => code.replace("'a0'", "'a1'")) + // await untilUpdated( + // outputEle, + // ['a.js: a0', 'b.js: b0,a0', 'a.js: a1'].join('
'), + // ) - editFile('importing-updated/b.js', (code) => - code.replace('`b0,${a}`', '`b1,${a}`'), - ) - // note that "a.js: a1" should not happen twice after "b.js: b0,a0'" - await untilUpdated( - outputEle, - ['a.js: a0', 'b.js: b0,a0', 'a.js: a1', 'b.js: b1,a1'].join('
'), - ) - }) + // editFile('importing-updated/b.js', (code) => + // code.replace('`b0,${a}`', '`b1,${a}`'), + // ) + // // note that "a.js: a1" should not happen twice after "b.js: b0,a0'" + // await untilUpdated( + // outputEle, + // ['a.js: a0', 'b.js: b0,a0', 'a.js: a1', 'b.js: b1,a1'].join('
'), + // ) + // }) }) describe('acceptExports', () => { @@ -338,7 +339,7 @@ describe('acceptExports', () => { beforeAll(async () => { await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, />>>>>>/], (logs) => { expect(logs).toContain(`<<<<<< A0 B0 D0 ; ${dep}`) @@ -466,7 +467,7 @@ describe('acceptExports', () => { beforeAll(async () => { await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, />>>>>>/], (logs) => { expect(logs).toContain(`<<< named: ${a} ; ${dep}`) @@ -520,8 +521,9 @@ describe('acceptExports', () => { beforeAll(async () => { clientLogs.length = 0 // so it's in the module graph - await server.transformRequest(testFile, { ssr: true }) - await server.transformRequest('non-tested/dep.js', { ssr: true }) + const ssrEnvironment = server.environments.ssr + await ssrEnvironment.transformRequest(testFile) + await ssrEnvironment.transformRequest('non-tested/dep.js') }) test('does not full reload', async () => { @@ -569,7 +571,7 @@ describe('acceptExports', () => { const file = 'side-effects.ts' await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, />>>/], (logs) => { expect(logs).toContain('>>> side FX') @@ -598,7 +600,7 @@ describe('acceptExports', () => { const url = '/' + file await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, '-- unused --'], (logs) => { expect(logs).toContain('-- unused --') @@ -621,7 +623,7 @@ describe('acceptExports', () => { const file = `${testDir}/${fileName}` await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, '-- used --', 'used:foo0'], (logs) => { expect(logs).toContain('-- used --') @@ -654,7 +656,7 @@ describe('acceptExports', () => { const url = '/' + file await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, '>>> ready <<<'], (logs) => { expect(logs).toContain('loaded:all:a0b0c0default0') @@ -688,7 +690,7 @@ describe('acceptExports', () => { const file = `${testDir}/${fileName}` await untilConsoleLogAfter( - () => setupViteRuntime(`/${testDir}/index`), + () => setupModuleRunner(`/${testDir}/index`), [CONNECTED, '>>> ready <<<'], (logs) => { expect(logs).toContain('loaded:some:a0b0c0default0') @@ -716,7 +718,7 @@ describe('acceptExports', () => { }) test('handle virtual module updates', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') const el = () => hmr('.virtual') expect(el()).toBe('[success]0') editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]')) @@ -724,7 +726,7 @@ test('handle virtual module updates', async () => { }) test('invalidate virtual module', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') const el = () => hmr('.virtual') expect(el()).toBe('[wow]0') globalThis.__HMR__['virtual:increment']() @@ -732,7 +734,7 @@ test('invalidate virtual module', async () => { }) test.todo('should hmr when file is deleted and restored', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') const parentFile = 'file-delete-restore/parent.js' const childFile = 'file-delete-restore/child.js' @@ -820,7 +822,7 @@ test.todo('delete file should not break hmr', async () => { test.todo( 'deleted file should trigger dispose and prune callbacks', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') const parentFile = 'file-delete-restore/parent.js' const childFile = 'file-delete-restore/child.js' @@ -857,7 +859,7 @@ test.todo( ) test('import.meta.hot?.accept', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') await untilConsoleLogAfter( () => editFile('optional-chaining/child.js', (code) => @@ -869,7 +871,7 @@ test('import.meta.hot?.accept', async () => { }) test('hmr works for self-accepted module within circular imported files', async () => { - await setupViteRuntime('/self-accept-within-circular/index') + await setupModuleRunner('/self-accept-within-circular/index') const el = () => hmr('.self-accept-within-circular') expect(el()).toBe('c') editFile('self-accept-within-circular/c.js', (code) => @@ -885,7 +887,7 @@ test('hmr works for self-accepted module within circular imported files', async }) test('hmr should not reload if no accepted within circular imported files', async () => { - await setupViteRuntime('/circular/index') + await setupModuleRunner('/circular/index') const el = () => hmr('.circular') expect(el()).toBe( // tests in the browser check that there is an error, but vite runtime just returns undefined in those cases @@ -901,7 +903,7 @@ test('hmr should not reload if no accepted within circular imported files', asyn }) test('assets HMR', async () => { - await setupViteRuntime('/hmr.ts') + await setupModuleRunner('/hmr.ts') const el = () => hmr('#logo') await untilConsoleLogAfter( () => @@ -1096,7 +1098,7 @@ function createInMemoryLogger(logs: string[]) { return logger } -async function setupViteRuntime( +async function setupModuleRunner( entrypoint: string, serverOptions: InlineConfig = {}, ) { @@ -1104,7 +1106,7 @@ async function setupViteRuntime( await server.close() clientLogs.length = 0 serverLogs.length = 0 - runtime.clearCache() + runner.clearCache() } globalThis.__HMR__ = {} as any @@ -1137,9 +1139,9 @@ async function setupViteRuntime( const logger = new HMRMockLogger() // @ts-expect-error not typed for HMR - globalThis.log = (...msg) => logger.debug(...msg) + globalThis.log = (...msg) => logger.log(...msg) - runtime = await createViteRuntime(server, { + runner = createServerModuleRunner(server.environments.ssr, { hmr: { logger, }, @@ -1147,22 +1149,29 @@ async function setupViteRuntime( await waitForWatcher(server, entrypoint) - await runtime.executeEntrypoint(entrypoint) + await runner.import(entrypoint) return { - runtime, + runtime: runner, server, } } class HMRMockLogger { - debug(...msg: unknown[]) { + log(...msg: unknown[]) { const log = msg.join(' ') clientLogs.push(log) logsEmitter.emit('log', log) } + + debug(...msg: unknown[]) { + const log = ['[vite]', ...msg].join(' ') + clientLogs.push(log) + logsEmitter.emit('log', log) + } error(msg: string) { - clientLogs.push(msg) - logsEmitter.emit('log', msg) + const log = ['[vite]', msg].join(' ') + clientLogs.push(log) + logsEmitter.emit('log', log) } } diff --git a/playground/hmr-ssr/vite.config.ts b/playground/hmr-ssr/vite.config.ts index 5b4a7c17fe27cb..f03eac818eee1b 100644 --- a/playground/hmr-ssr/vite.config.ts +++ b/playground/hmr-ssr/vite.config.ts @@ -8,18 +8,21 @@ export default defineConfig({ plugins: [ { name: 'mock-custom', - async handleHotUpdate({ file, read, server }) { + async hotUpdate({ environment, file, read, server }) { if (file.endsWith('customFile.js')) { const content = await read() const msg = content.match(/export const msg = '(\w+)'/)[1] - server.hot.send('custom:foo', { msg }) - server.hot.send('custom:remove', { msg }) + environment.hot.send('custom:foo', { msg }) + environment.hot.send('custom:remove', { msg }) } }, configureServer(server) { - server.hot.on('custom:remote-add', ({ a, b }, client) => { - client.send('custom:remote-add-result', { result: a + b }) - }) + server.environments.ssr.hot.on( + 'custom:remote-add', + ({ a, b }, client) => { + client.send('custom:remote-add-result', { result: a + b }) + }, + ) }, }, virtualPlugin(), @@ -45,11 +48,14 @@ export const virtual = _virtual + '${num}';` } }, configureServer(server) { - server.hot.on('virtual:increment', async () => { - const mod = await server.moduleGraph.getModuleByUrl('\0virtual:file') + server.environments.ssr.hot.on('virtual:increment', async () => { + const mod = + await server.environments.ssr.moduleGraph.getModuleByUrl( + '\0virtual:file', + ) if (mod) { num++ - server.reloadModule(mod) + server.reloadEnvironmentModule(mod) } }) }, diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts index b290ff60a3140d..64d47797a841cd 100644 --- a/playground/hmr/vite.config.ts +++ b/playground/hmr/vite.config.ts @@ -10,18 +10,21 @@ export default defineConfig({ plugins: [ { name: 'mock-custom', - async handleHotUpdate({ file, read, server }) { + async hotUpdate({ environment, file, read }) { if (file.endsWith('customFile.js')) { const content = await read() const msg = content.match(/export const msg = '(\w+)'/)[1] - server.hot.send('custom:foo', { msg }) - server.hot.send('custom:remove', { msg }) + environment.hot.send('custom:foo', { msg }) + environment.hot.send('custom:remove', { msg }) } }, configureServer(server) { - server.hot.on('custom:remote-add', ({ a, b }, client) => { - client.send('custom:remote-add-result', { result: a + b }) - }) + server.environments.client.hot.on( + 'custom:remote-add', + ({ a, b }, client) => { + client.send('custom:remote-add-result', { result: a + b }) + }, + ) }, }, virtualPlugin(), @@ -47,11 +50,14 @@ export const virtual = _virtual + '${num}';` } }, configureServer(server) { - server.hot.on('virtual:increment', async () => { - const mod = await server.moduleGraph.getModuleByUrl('\0virtual:file') + server.environments.client.hot.on('virtual:increment', async () => { + const mod = + await server.environments.client.moduleGraph.getModuleByUrl( + '\0virtual:file', + ) if (mod) { num++ - server.reloadModule(mod) + server.reloadEnvironmentModule(mod) } }) }, diff --git a/playground/html/__tests__/html.spec.ts b/playground/html/__tests__/html.spec.ts index 85ceea961752ee..896267ea06bae5 100644 --- a/playground/html/__tests__/html.spec.ts +++ b/playground/html/__tests__/html.spec.ts @@ -282,7 +282,7 @@ describe.runIf(isServe)('invalid', () => { await page.keyboard.press('Escape') await hiddenPromise - viteServer.hot.send({ + viteServer.environments.client.hot.send({ type: 'error', err: { message: 'someError', @@ -390,7 +390,10 @@ describe.runIf(isServe)('warmup', () => { // warmup transform files async during server startup, so the module check // here might take a while to load await withRetry(async () => { - const mod = await viteServer.moduleGraph.getModuleByUrl('/warmup/warm.js') + const mod = + await viteServer.environments.client.moduleGraph.getModuleByUrl( + '/warmup/warm.js', + ) expect(mod).toBeTruthy() }) }) diff --git a/playground/module-graph/__tests__/module-graph.spec.ts b/playground/module-graph/__tests__/module-graph.spec.ts index bfabd53f289724..20492e968c674f 100644 --- a/playground/module-graph/__tests__/module-graph.spec.ts +++ b/playground/module-graph/__tests__/module-graph.spec.ts @@ -4,7 +4,7 @@ import { isServe, page, viteServer } from '~utils' test.runIf(isServe)('importedUrls order is preserved', async () => { const el = page.locator('.imported-urls-order') expect(await el.textContent()).toBe('[success]') - const mod = await viteServer.moduleGraph.getModuleByUrl( + const mod = await viteServer.environments.client.moduleGraph.getModuleByUrl( '/imported-urls-order.js', ) const importedModuleIds = [...mod.importedModules].map((m) => m.url) diff --git a/playground/ssr-deps/__tests__/ssr-deps.spec.ts b/playground/ssr-deps/__tests__/ssr-deps.spec.ts index c8794ce915dc21..64886f2f0c7e54 100644 --- a/playground/ssr-deps/__tests__/ssr-deps.spec.ts +++ b/playground/ssr-deps/__tests__/ssr-deps.spec.ts @@ -120,7 +120,11 @@ test('import css library', async () => { }) describe.runIf(isServe)('hmr', () => { - test('handle isomorphic module updates', async () => { + // TODO: the server file is not imported on the client at all + // so it's not present in the client moduleGraph anymore + // we need to decide if we want to support a usecase when ssr change + // affcts the client in any way + test.skip('handle isomorphic module updates', async () => { await page.goto(url) expect(await page.textContent('.isomorphic-module-server')).toMatch( diff --git a/playground/ssr-html/__tests__/ssr-html.spec.ts b/playground/ssr-html/__tests__/ssr-html.spec.ts index 92a2713420f2c3..249bfb5d722383 100644 --- a/playground/ssr-html/__tests__/ssr-html.spec.ts +++ b/playground/ssr-html/__tests__/ssr-html.spec.ts @@ -123,7 +123,7 @@ describe.runIf(isServe)('network-imports', () => { [ '--experimental-network-imports', 'test-network-imports.js', - '--runtime', + '--module-runner', ], { cwd: fileURLToPath(new URL('..', import.meta.url)), diff --git a/playground/ssr-html/test-network-imports.js b/playground/ssr-html/test-network-imports.js index 91f84f6a3b3ea3..6e6a87d93d4219 100644 --- a/playground/ssr-html/test-network-imports.js +++ b/playground/ssr-html/test-network-imports.js @@ -1,8 +1,8 @@ import assert from 'node:assert' import { fileURLToPath } from 'node:url' -import { createServer, createViteRuntime } from 'vite' +import { createServer, createServerModuleRunner } from 'vite' -async function runTest(useRuntime) { +async function runTest(userRunner) { const server = await createServer({ configFile: false, root: fileURLToPath(new URL('.', import.meta.url)), @@ -11,9 +11,11 @@ async function runTest(useRuntime) { }, }) let mod - if (useRuntime) { - const runtime = await createViteRuntime(server, { hmr: false }) - mod = await runtime.executeUrl('/src/network-imports.js') + if (userRunner) { + const runner = await createServerModuleRunner(server.environments.ssr, { + hmr: false, + }) + mod = await runner.import('/src/network-imports.js') } else { mod = await server.ssrLoadModule('/src/network-imports.js') } @@ -21,4 +23,4 @@ async function runTest(useRuntime) { await server.close() } -runTest(process.argv.includes('--runtime')) +runTest(process.argv.includes('--module-runner')) diff --git a/playground/ssr-html/test-stacktrace-runtime.js b/playground/ssr-html/test-stacktrace-runtime.js index c2b8f670b5a089..0f4914dcbfe599 100644 --- a/playground/ssr-html/test-stacktrace-runtime.js +++ b/playground/ssr-html/test-stacktrace-runtime.js @@ -1,6 +1,6 @@ import { fileURLToPath } from 'node:url' import assert from 'node:assert' -import { createServer, createViteRuntime } from 'vite' +import { createServer, createServerModuleRunner } from 'vite' // same test case as packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts // implemented for e2e to catch build specific behavior @@ -13,11 +13,11 @@ const server = await createServer({ }, }) -const runtime = await createViteRuntime(server, { +const runner = await createServerModuleRunner(server.environments.ssr, { sourcemapInterceptor: 'prepareStackTrace', }) -const mod = await runtime.executeEntrypoint('/src/has-error-deep.ts') +const mod = await runner.import('/src/has-error-deep.ts') let error try { mod.main() diff --git a/playground/ssr-noexternal/package.json b/playground/ssr-noexternal/package.json index 3273e76b04c599..5df63e8468ead6 100644 --- a/playground/ssr-noexternal/package.json +++ b/playground/ssr-noexternal/package.json @@ -7,7 +7,8 @@ "dev": "node server", "build": "vite build --ssr src/entry-server.js", "serve": "NODE_ENV=production node server", - "debug": "node --inspect-brk server" + "debug": "node --inspect-brk server", + "build-all": "vite build --all" }, "dependencies": { "@vitejs/test-external-cjs": "file:./external-cjs", diff --git a/playground/ssr-noexternal/vite.config.js b/playground/ssr-noexternal/vite.config.js index 1109bddd187001..8932076d09e24d 100644 --- a/playground/ssr-noexternal/vite.config.js +++ b/playground/ssr-noexternal/vite.config.js @@ -15,5 +15,6 @@ export default defineConfig({ rollupOptions: { external: ['@vitejs/test-external-cjs'], }, + ssr: 'src/entry-server.js', // for 'all' }, }) diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index ff2303dc498569..50b23b0c8a5207 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -13,6 +13,7 @@ import type { } from 'vite' import { build, + createBuilder, createServer, loadConfigFromFile, mergeConfig, @@ -257,15 +258,20 @@ export async function startDefaultServe(): Promise { plugins: [resolvedPlugin()], }, ) - const rollupOutput = await build(buildConfig) - const isWatch = !!resolvedConfig!.build.watch - // in build watch,call startStaticServer after the build is complete - if (isWatch) { - watcher = rollupOutput as RollupWatcher - await notifyRebuildComplete(watcher) - } - if (buildConfig.__test__) { - buildConfig.__test__() + if (buildConfig.builder) { + const builder = await createBuilder({ root: rootDir }) + await builder.buildApp() + } else { + const rollupOutput = await build(buildConfig) + const isWatch = !!resolvedConfig!.build.watch + // in build watch,call startStaticServer after the build is complete + if (isWatch) { + watcher = rollupOutput as RollupWatcher + await notifyRebuildComplete(watcher) + } + if (buildConfig.__test__) { + buildConfig.__test__() + } } const previewConfig = await loadConfig({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f89a919fc7b99c..f7512668b74c13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: '@rollup/plugin-typescript': specifier: ^11.1.6 version: 11.1.6(rollup@4.13.0)(tslib@2.6.2)(typescript@5.2.2) + '@type-challenges/utils': + specifier: ^0.1.1 + version: 0.1.1 '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 @@ -425,6 +428,14 @@ importers: specifier: ^8.17.0 version: 8.17.0 + packages/vite/src/node/__tests__: + dependencies: + '@vitejs/cjs-ssr-dep': + specifier: link:./fixtures/cjs-ssr-dep + version: link:fixtures/cjs-ssr-dep + + packages/vite/src/node/__tests__/fixtures/cjs-ssr-dep: {} + packages/vite/src/node/__tests__/packages/module: {} packages/vite/src/node/__tests__/packages/name: {} @@ -443,14 +454,6 @@ importers: packages/vite/src/node/server/__tests__/fixtures/yarn/nested: {} - packages/vite/src/node/ssr/__tests__: - dependencies: - '@vitejs/cjs-ssr-dep': - specifier: link:./fixtures/cjs-ssr-dep - version: link:fixtures/cjs-ssr-dep - - packages/vite/src/node/ssr/__tests__/fixtures/cjs-ssr-dep: {} - packages/vite/src/node/ssr/runtime/__tests__: dependencies: '@vitejs/cjs-external': @@ -674,6 +677,21 @@ importers: playground/env-nested: {} + playground/environment-react-ssr: + devDependencies: + '@types/react': + specifier: ^18.2.73 + version: 18.2.74 + '@types/react-dom': + specifier: ^18.2.23 + version: 18.2.23 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + playground/extensions: dependencies: vue: @@ -4394,6 +4412,10 @@ packages: /@tsconfig/node16@1.0.2: resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==} + /@type-challenges/utils@0.1.1: + resolution: {integrity: sha512-A7ljYfBM+FLw+NDyuYvGBJiCEV9c0lPWEAdzfOAkb3JFqfLl0Iv/WhWMMARHiRKlmmiD1g8gz/507yVvHdQUYA==} + dev: true + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -4603,6 +4625,10 @@ packages: kleur: 3.0.3 dev: true + /@types/prop-types@15.7.12: + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + dev: true + /@types/qs@6.9.12: resolution: {integrity: sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==} dev: true @@ -4611,6 +4637,19 @@ packages: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true + /@types/react-dom@18.2.23: + resolution: {integrity: sha512-ZQ71wgGOTmDYpnav2knkjr3qXdAFu0vsk8Ci5w3pGAIdj7/kKAyn+VsQDhXsmzzzepAiI9leWMmubXz690AI/A==} + dependencies: + '@types/react': 18.2.74 + dev: true + + /@types/react@18.2.74: + resolution: {integrity: sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==} + dependencies: + '@types/prop-types': 15.7.12 + csstype: 3.1.3 + dev: true + /@types/resolve@1.20.2: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} dev: true @@ -7797,7 +7836,6 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 - dev: false /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} @@ -9193,7 +9231,6 @@ packages: loose-envify: 1.4.0 react: 18.3.1 scheduler: 0.23.2 - dev: false /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} @@ -9204,7 +9241,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -9494,7 +9530,6 @@ packages: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} dependencies: loose-envify: 1.4.0 - dev: false /scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts index 7533ea991c5f95..db750c65ebec21 100644 --- a/vitest.config.e2e.ts +++ b/vitest.config.e2e.ts @@ -1,7 +1,7 @@ import { resolve } from 'node:path' import { defineConfig } from 'vitest/config' -const timeout = process.env.CI ? 50000 : 30000 +const timeout = process.env.PWDEBUG ? Infinity : process.env.CI ? 50000 : 30000 export default defineConfig({ resolve: { diff --git a/vitest.config.ts b/vitest.config.ts index 2802969bc155c2..edf35c9c4cf507 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -21,9 +21,9 @@ export default defineConfig({ publicDir: false, resolve: { alias: { - 'vite/runtime': path.resolve( + 'vite/module-runner': path.resolve( _dirname, - './packages/vite/src/runtime/index.ts', + './packages/vite/src/module-runner/index.ts', ), }, },