Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: experimental Vite Runtime API #12165

Merged
merged 34 commits into from Feb 1, 2024

Conversation

sheremet-va
Copy link
Member

@sheremet-va sheremet-va commented Feb 23, 2023

Description

This PR adds a modified implementation of ViteNode into the Vite core for further improvements.

Fixes #7887

Additional context

Expectations

Reasons why we want to implement this API in Vite core.

  • Make it easier for frameworks to integrate Vite SSR with additional features (provide customizable API)
  • Support HMR during SSR
  • Support different runtimes, not just Node.js
  • Keep it simple (and compatible with existing ssrLoadModule)

Public API

Using a helper:

import { createServer, createViteRuntime, ServerHMRConnector } from 'vite'

const server = await createServer()
const runtime = await createViteRuntime(server)

// To support HMR, use "executeEntrypoint"
// or you can also use "executeUrl(path)" - this will not correctly support full-reload HMR event
await runtime.executeEntrypoint('/index.js')

Or using primitives:

import { ViteRuntime, ESModulesRunner, createHMRHandler } from 'vite/runtime'

const hmrConnection = new ServerHMRConnector(server)
const runtime  = new ViteRuntime(
  {
    root: server.config.root,
    // you can access this function as "server.ssrFetchModule" if you support Vite SSR,
    // or import "fetchModule" from "vite" entry point and use that one (the only difference is in how source maps are processed)
    fetchModule: server.ssrFetchModule,
    hmr: {
	  connection: hmrConnection,
	},
  },
  // can provide your own! this is how the code _runs_ (new Function/eval/vm.runInContext)
  // this can also be configured in createViteRuntime using a `runner` option 
  new ESModulesRunner(),
)

await runtime.executeEntrypoint('/index.js')

The main difference from the old API is that you can have a client in a separate worker thread or even on another machine. This API also doesn't limit how many clients you can have with a single server. You can choose your own way of communicating between the server and the runtime. Example with worker threads (will be released as a separate package):

// main.ts
import { createServer } from 'vite'
import { createRpc } from './some-rpc.js'

const server = await createServer()

const rpc = createRpc({
  fetchModule: id => server.ssrFetchModule(id),
})
const worker = new Worker('./runtime.js')
worker.postMessage({ type: 'init', port: rpc.port, id: '/index.js', root: server.config.root }, [rpc.port])
// runtime.js
import { ViteRuntime, ESModulesRunner, HMRConnection, createHMRHandler } from 'vite/runtime'
import { createReceiverRpc } from './some-rpc.js'

export class WorkerPortHMRConnector implements HMRConnection {
  constructor(private readonly port: MessagePort) {}

  send(message: string): void {
    this.port.postMessage({ type: 'hmr:custom', payload: message })
  }

  onUpdate(handler: (payload: HMRPayload) => void): () => void {
    function onMessage({
      type,
      payload,
    }: {
      type: string
      payload: HMRPayload
    }) {
      if (type === 'vite:hmr') {
        if(payload.type === 'full-reload') {
          rpc('reload')
        } else {
          handler(payload)
        }
      }
    }

    this.port.on('message', onMessage)
    return () => {
      this.port.off('message', onMessage)
    }
  }
}

async function createWorkerViteRuntime(
  root: string,
  rpc: WorkerExampleRPC,
): Promise<ViteRuntime> {
  const hmrConnector = new WorkerPortHMRConnector(options.port)
  return new ViteRuntime(
    {
      root: options.root,
      fetchModule: (id) => rpc('fetchModule', { id }),
      hmr: hmrConnector,
    },
    new ESModulesRunner(),
  )
}

self.onmessage = (event) => {
  if(event.type !== 'init') return

  const rpc = createReceiverRpc(event.port)
  const runtime = createWorkerViteRuntime(rpc)

  await runtime.executeEntrypoint(event.id)
}

ViteRuntime doesn't depend on server code and should be lightweight (so, anything that imports it would be faster than just importing from vite entry which depends on a lot of modules)

All primitives used in createWorkerViteRuntime and createViteRuntime are exported from vite/runtime so they can be consumed in environments that don't support workers/node builtins.

Vite only exports createViteRuntime for better compatibility with the previous ssrLoadModule implementation. Vite doesn't expose createWorkerViteRuntime, it's shown only as an example - it will be released as a separate package though.


What is the purpose of this pull request?

  • Bug fix
  • New Feature
  • Documentation update
  • Other

Before submitting the PR, please make sure you do the following

  • Read the Contributing Guidelines.
  • Read the Pull Request Guidelines and follow the PR Title Convention.
  • Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
  • Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • Ideally, include relevant tests that fail without this PR but pass with it.

@sheremet-va sheremet-va changed the title feat: add vite-node into core feat: add vite-node into the core Feb 23, 2023
@benmccann
Copy link
Collaborator

Could you clarify in the PR description what the benefit would be? Is there a performance improvement, is it primarily to make it easier to support Vitest, etc.?

@benmccann
Copy link
Collaborator

Leaving a note here to capture discussion from Discord. This does not currently support Deno because of use of vm.runInThisContext. However, @bartlomieju said that it should be able to be polyfilled and he will take a look in early March

@bartlomieju
Copy link
Contributor

@benmccann sorry for slow turnaround, but we got swamped in other work. I'll prioritize vm.runInThisContext in the coming weeks.

@bartlomieju
Copy link
Contributor

@benmccann FYI we have improved vm.runInThisContext in denoland/deno#18767. This will be released in Deno 1.33 later this month.

@brillout
Copy link
Contributor

Alternative to vite-node: vavite by @cyco130.

@patak-dev
Copy link
Member

@cyco130 is aware of this work and there were some discussions out of this PR. I hope that we can work together and bring what makes sense of vavite into the table. At minimum, it would be great to have documented here why are we going with the vite-node approach instead at this point.

@cyco130
Copy link
Contributor

cyco130 commented Apr 27, 2023

Alternative to vite-node: vavite by @cyco130.

If we leave the @vavite/loader package aside, vavite is easier to set up but less flexible than vite-node. vavite is just a Vite plugin™ but can't do things like running the server code out of process or customizing HMR behavior. Basically it just loads your entry with ssrLoadModule and inserts its default export into the dev server's middleware stack.

@vavite/loader is a whole different story: It imports SSR code directly and uses a custom Node ESM loader to transpile the code with Vite. It is probably better for compatibility between dev and build except for one important flaw: It leaks memory. Node doesn't offer any way to dispose of a stale module so when you change a module's code, the old version stays in memory and it keeps accumulating indefinitely. I found it not to be a problem for me in practice but your mileage may vary. In any case, its main appeal was to enable sourcemaps which has been addressed by #11780 since.

@sheremet-va sheremet-va force-pushed the feat/vite-node-core branch 2 times, most recently from 7c19c08 to 6b796ce Compare July 3, 2023 15:21
@haikyuu
Copy link
Contributor

haikyuu commented Jul 6, 2023

One piece of feedback about vite-node is that it's not clear how to implement HMR when running the viteNodeServer and the runner in different contexts.

The current implementation of HMR in vite-node cli showcases this

const runner = new ViteNodeRunner({
    root: server.config.root,
    base: server.config.base,
    fetchModule(id) {
      return node.fetchModule(id)
    },
    resolveId(id, importer) {
      return node.resolveId(id, importer)
    },
    createHotContext(runner, url) {
      return createHotContext(runner, server.emitter, files, url)
    },
  })

server.emitter wouldn't be accessible to the runner.

@sheremet-va
Copy link
Member Author

sheremet-va commented Jan 18, 2024

This should be ready for a review. Most of the files are copy-pasted from playground/hmr, so don't feel discouraged - the important part is located in node/ssr/runtime. This PR also includes #15631 for now while I wait for the answer there.

@patak-dev patak-dev changed the title feat: add vite-node into the core feat: experimental Vite Runtime API Jan 31, 2024
playground/test-utils.ts Outdated Show resolved Hide resolved

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.
Copy link
Member

@patak-dev patak-dev Jan 31, 2024

Choose a reason for hiding this comment

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

I think it will be helpful to have some links to custom implementations like @sapphi-red's one. At least during the experimental phase as it is a great resource to see the API in action.

We can add these links in another PR though, once https://github.com/sapphi-red/vite-envs/tree/use-vite-runtime is merged into main in vite-envs

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, sounds like a good idea.

patak-dev
patak-dev previously approved these changes Jan 31, 2024
Copy link
Member

@patak-dev patak-dev left a comment

Choose a reason for hiding this comment

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

Amazing work @sheremet-va! As we discussed, let's merge the PR so we can start iterating from here on. We'll release the Vite Runtime API as experimental in Vite 5.1 to let the ecosystem explore how it works for their projects. Breaking changes are expected to be applied in Vite 5.2 as we gather more feedback, so please pin Vite to the 5.1 minor if you are going to use the API.

@patak-dev patak-dev added this to the 5.1 milestone Jan 31, 2024
Copy link
Member

@bluwy bluwy left a comment

Choose a reason for hiding this comment

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

I've not tested the API design, but mainly looking in its implementation and have some comments on it.

I'm not really sure about the duplicated code in many places. I think we should figure something out to prevent that. Sometimes there are changes to the SSR code, and the duplicated ones can often be missed.

I'm also confused about what the ViteRuntime means here. Will the runtime code be always processed by Vite (and its plugins)? It looks like you could entirely avoid Vite if you wanted to, some of the API exposed (like requestStub) seems to imply that. But I'm not sure then if it's actually a Vite runtime.

Overall, my comments are mostly concerning if we were to maintain this new code. It's great that the new runtime stuff are entirely within it's own folder.

playground/hmr-ssr/package.json Outdated Show resolved Hide resolved
packages/vite/src/node/ssr/runtime/hmrHandler.ts Outdated Show resolved Hide resolved
packages/vite/src/node/ssr/runtime/source-map/decoder.ts Outdated Show resolved Hide resolved
packages/vite/src/node/ssr/runtime/types.ts Outdated Show resolved Hide resolved
packages/vite/src/node/ssr/runtime/utils.ts Outdated Show resolved Hide resolved
packages/vite/src/node/ssr/runtime/index.ts Outdated Show resolved Hide resolved
packages/vite/src/node/ssr/runtime/types.ts Show resolved Hide resolved
packages/vite/src/node/ssr/runtime/types.ts Outdated Show resolved Hide resolved
packages/vite/src/node/ssr/runtime/utils.ts Outdated Show resolved Hide resolved
@sheremet-va
Copy link
Member Author

I'm also confused about what the ViteRuntime means here. Will the runtime code be always processed by Vite (and its plugins)? It looks like you could entirely avoid Vite if you wanted to, some of the API exposed (like requestStub) seems to imply that. But I'm not sure then if it's actually a Vite runtime.

I think there was a discussion about the name in Discord. requestStub is a leftover, we should probably remove it. Technically if you really want to, you can avoid relying on Vite SSR transform by providing a separate runner that doesn't use __vite_ssr_import__, but I'd say this is out of our scope.

Copy link
Member

@bluwy bluwy left a comment

Choose a reason for hiding this comment

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

Made some comments below about marking the exported APIs with the @experimental jsdoc. That way it's more explicit and we can remove/rename the types/api in the future. I think we can get away marking all the APIs in vite/runtime as experimental if we mention that vite/runtime is experimental entirely instead.

packages/vite/src/node/index.ts Show resolved Hide resolved
packages/vite/src/node/index.ts Outdated Show resolved Hide resolved
packages/vite/src/node/server/index.ts Outdated Show resolved Hide resolved
@sheremet-va
Copy link
Member Author

Made some comments below about marking the exported APIs with the @experimental jsdoc.

Totally agree 👍🏻

I think we can get away marking all the APIs in vite/runtime as experimental if we mention that vite/runtime is experimental entirely instead.

We mention in the documentation that it is an experimental API: https://github.com/vitejs/vite/pull/12165/files#diff-b9b4fba55b51e1129c00cb2309528aa9ea13f7a85f27c0fd4f185ad7326586c7R4

bluwy
bluwy previously approved these changes Jan 31, 2024
Copy link
Member

@bluwy bluwy left a comment

Choose a reason for hiding this comment

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

Thanks for addressing my comments quickly! I'm ok with merging this for now. As a note to myself, we could improve this in the future with:

  1. Have ssrLoadModule use the Vite runtime under-the-hood.
  2. Create a special Rollup build that keeps the runtime lean when using shared code.

@patak-dev

This comment was marked as resolved.

@vite-ecosystem-ci
Copy link

📝 Ran ecosystem CI on 4c0d3fa: Open

suite result latest scheduled
analogjs success success
astro success success
histoire success success
ladle success success
laravel failure failure
marko success success
nuxt success success
nx failure failure
previewjs success success
qwik failure failure
rakkas success success
remix failure failure
sveltekit failure failure
unocss success success
vike success success
vite-plugin-pwa success success
vite-plugin-react failure failure
vite-plugin-react-pages success success
vite-plugin-react-swc success success
vite-plugin-svelte success success
vite-plugin-vue failure failure
vite-setup-catalogue success success
vitepress success success
vitest success success

Copy link
Member

@patak-dev patak-dev left a comment

Choose a reason for hiding this comment

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

Let's merge this PR to gather feedback until Vite 5.2, were it is expected that breaking changes will be applied. Let's continue the discussion here:

@patak-dev patak-dev merged commit 8b3ab07 into vitejs:main Feb 1, 2024
9 of 10 checks passed
@sheremet-va sheremet-va deleted the feat/vite-node-core branch February 1, 2024 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat: ssr p3-significant High priority enhancement (priority)
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

HMR API doesn't work with ssrLoadModule