Vite on Nitro Setup (draft, practice)

Quick demo setting up Vite to get served by Nitro. Very basic rough draft.

We will break down the process of creating a Vite app with Nitro integration into several bite-sized lessons for egghead.io. Here's the proposed series of lessons:

  1. Creating a new Vite React TypeScript project

    • Use the Vite CLI to create a new project
    • Explore the initial project structure
  2. Adding Nitro to a Vite project

    • Install Nitropack
    • Create a basic Nitro configuration file
// https://nitro.unjs.io/config
export default defineNitroConfig({
  renderer: './renderer.ts',
  publicAssets: [
    {
      dir: 'dist'
    }

  ],
  1. Creating a custom renderer for Nitro
    • Create a basic renderer file
    • Implement a simple event handler
import process from 'node:process'
export default defineEventHandler(
  1. Setting up a Vite plugin for Nitro
    • Create a Vite plugin file
    • Implement basic Vite server creation and listening
import { defineNitroPlugin } from 'nitropack/runtime'
import { nitroApp } from 'nitropack/runtime/app'
import { ViteDevServer } from 'vite'

export default defineNitroPlugin(async () => {
  if( process.env.NODE_ENV === 'production') return

  const {createServer} = await import('vite')

  console.log('Starting the Vite Dev Server...')

  const vite = await createServer();

  await vite.listen()
  vite.printUrls()

  nitroApp.hooks.hook('request', (event) => {
    event.context.vite = vite
  })
})
  1. Configuring Nitro for development and production
    • Update Nitro config for public and server assets
    • Modify package.json scripts for Nitro and Vite
// https://nitro.unjs.io/config
export default defineNitroConfig({
  renderer: './renderer.ts',
  publicAssets: [
    {
      dir: 'dist'
    }

  ],
  serverAssets: [
    {
      baseName: 'vite',
      dir: '.dist/.vite'
    }
  ]

  1. Enhancing the renderer for development and production
    • Implement conditional rendering based on environment
    • Handle Vite's development server
    • Process production build assets
import process from 'node:process'
export default defineEventHandler(
  ...
  const manifest = await useStorage('assers:vite').getItem<string>('manifest.json')

  if(!manifest) {
    setResponseStatus(ctx, 500)
    return 'No manifest found'
  }



  console.log('manifest', manifest)

  const entryChunk = manifest['app/main.tsx']

  if(!entryChunk) {
    setResponseStatus(ctx, 500)
    return 'No entry chunk found'
  }

  const cssLinks = entryChunk.css.map(link => `<link rel="stylesheet" href="/${link}"/>`).join('\n')
  const scriptLinks = `<script type="module" src="/${entryChunk.file}"></script>`

  const template = `<!DOCTYPE html>
                      <html lang="en">
                      <head>
                        <meta charset="UTF-8">
                        <meta name="viewport" content="width=device-width, initial-scale=1.0">
                        <title>Vite Nitro Demo</title>
                        ${cssLinks}
                      </head>
                      <body>
                        <div id="root"></div>
                        ${scriptLinks}
                      </body>
                    </html>`

  return template
})
  1. Optimizing Vite configuration for the Nitro setup
    • Configure build options for manifest generation
    • Set up dependency optimization
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  appType: 'mpa',
  plugins: [react()],
  build: {
    manifest: true,
    rollupOptions: {
      input: './app/main.tsx'
    }
  },
  optimizeDeps: {
    include: ['react/jsx-runtime'],
    exclude: ['@react-refresh']
  }
})
  1. Adding error handling to the renderer
    • Implement basic error checks for missing manifest or entry chunks
  const manifest = await useStorage('assers:vite').getItem<string>('manifest.json')

  if(!manifest) {
    setResponseStatus(ctx, 500)
    return 'No manifest found'
  }



  console.log('manifest', manifest)

  const entryChunk = manifest['app/main.tsx']

  if(!entryChunk) {
    setResponseStatus(ctx, 500)
    return 'No entry chunk found'
  }
  1. Final touches and preparing for deployment
    • Exclude dist folder in tsconfig
    • Pin Node.js version using Volta

Each of these lessons provides a logical start and end point, accomplishing a specific task in the process of integrating Vite with Nitro. They follow a step-by-step progression that builds upon previous lessons, giving learners a comprehensive understanding of the setup process.

Transcript

[00:08] We're going to start by creating a Vita app. We're going to name the project. And we're going to select React and we're going to select TypeScript. And now we have Vite installed so we can cd into the new folder. And from here we can...

[00:41] It's not there. So let's open our project. Open our project. See what was built for us. So when we install a new Vita app we get a config.

[01:11] It's very basic. We're running the React plugin. Looks like we might need to pnpm install after we do that. I don't know that it did that for us. So that looks like it's done now.

[01:30] That's good. Now that's good. So let's see what all dependencies that we have. So we have React itself we have some ESLint it's only React hooks and React refresh so that's nice. Maybe check out the ESLint config inside of there.

[01:52] Looks like we won't have to spend a lot of time in there. Inside of the package we have dev so we can see what it looks like running pmpm dev And now we can open that in the browser and hit our counter and everything looks good. So we have a Vite server running. It's, I think a single page app by default. I'm going to change it to a multi-page app and let's see what happens then.

[03:00] It doesn't change anything. Let's see what else we get. So inside of our source we have some assets. We have the React logo. We have some CSS, some basic CSS for the application.

[03:36] We have this, which is the Vite demo that we saw. Some more CSS, which I assume is for that index. Yeah, that's important. Well, let's see where that's. All right, and then we have main, which I assume is the entry point.

[04:04] And we can commit this. And we can commit this. All right, we're gonna initialize and get repository. And then we are going to add all of our stuff. Cause it has nice defaults and then we will commit.

[04:28] Great. Right. So we're building a Nitro app, and I'm going to just manually add this. I'm going to add Nitro pack, and what we want to do is actually add the latest. So pmp install NitroPack and latest, great.

[05:58] And for Nitro to function we're going to need to add a config. And we can do that by adding a new file and we are going to add Nitro config. Nitro config.config.ts. And that's gonna look something like, oops, that's gonna look something like this. So from nitroconfig.ts, we're going to export the default defined nitroconfig, and we're going to have a custom render, and that is going to be something we're going to create, and we're going to say render.ts, and a dev proxy, which for now will just be blank.

[08:50] All right, so we need the render. We created that. So let's go ahead and add the render. So render.ts. And the render looks at something like this, where we are defining an event handler, and we're logging that out.

[09:15] We're logging that out. And then this is going to actually render with Vite. Back in. Back in the package.json, we are actually gonna, let's see, we're gonna take away everything in. Go.

[09:59] Nitro's really running the show here, So we're gonna change that, let Nitro do its thing. Let's see what happens if we dev, pmpm dev. All right, Nitro server has built, open that up. And it looks like we get a big blank screen. But it does say handle render.

[10:26] So that is a good sign over here in the console. And if we look back at our renderer We see that that actually checks out What we're not seeing here is VT but Well, how do we start the VEET server then? All right, so the next thing that we need to do is actually create a plugin inside of the plugins folder. And here we're going to create a plugin called Vite. So, vite.ts.

[11:52] And from there, we're going to import the create server from Vite. And now I need to export default define Nitro plugin and that is an async function so we can call that and instead of here we're going to go ahead and console log, Vite plugin. Now we're going to create Vite. And it looks like this where we say await create server. There we go.

[12:53] So this is a very simple server. We're going to await vlisten. And Finally, we are going to vt.printurls. So, we'll come back over here. I'm gonna go ahead and close this terminal window and we are going to restart our Nitro.

[13:15] So, pnpm dev. Now we can see that the Vite plugin actually announces and if we come here and if we come here so Vite is running but Nitro is not displaying whatever it is running yet. So it shows our URLs, we have that going, we have the plugin, we have the render I'm gonna view source on this and it's this main.tsx Let's see what we have going. Source we do have main. Is not in an app folder.

[14:23] So that is a difference here. So I'm going to change this from source to app, which is a default change. I guess we'll update imports and See if that makes any changes over here. Vite's complaining. Failed to load source.main.

[14:51] So we are making progress. So inside of our Vite config, see if That's correct. Check in here, that looks correct. That looks fine. We have our render is called, we're gonna check inside the app folder in main.

[15:18] Doop doop, everything looks good there. It's maybe a matter of restarting the server. So some place where you're specifically looking at the source. So I'm going to check that out. Figure out where that might be.

[15:52] In our app TSX. Nope. And our app TSX, nope. Index HTML. So app, there we go.

[16:06] And bingo. Oh, not bingo. All right, sweet. So now, after we've made those updates and we've changed it to the app folder. We could probably use source.

[16:20] We're going to use app just to differentiate because we're in the Nitro space. But now we have a Nitro app running Vite inside of it. So let's commit. So now Vite is on Nitro. That definitely took some changes.

[16:53] We could slim it down at this point to make the example a little nicer. All right, so let's change this. So if we deploy it, we want to use built assets in production. So we're going to change our Nitro config. So I'm gonna come in here and we have the underneath the dev proxy.

[18:10] This actually looks something like this where this would be star star. Like that and then for the V, but we're not using that right now. So here we're gonna say public assets and that is going to be a directory and that directory is dist for distribution. And below public assets, we're going to have server assets. And we're gonna have a base name here.

[19:06] And that base name is going to be Vite. And the directory will be .dist.vite. Dot Vite. Now inside of our package.json, we want to also call ViteBuild when we build this because this is a Vite and Nitro application. And we need to update our Vite plugin.

[19:44] So We are hand crafting a Vite plugin. So we can come in here and we don't need a console log so much anymore. It's fine. Instead, we're gonna come down here and we are going to say const createServer and we're going to do this inline so we're going to make this an asynchronous import. We're just going to bail here completely.

[20:23] If this is production, so if. We're literally just going to return. We don't want to do this in production. We're going to use the build process so it's a little different. Alright, so now we have the create server.

[21:00] Server. There we go. There we go. So down here, after we print the V URLs, we are actually going to look at Nitro app, and we're going to grab into hooks and the hook that we're going to get is the request hook. This delivers the event.

[21:50] We're going to look into that. Within there, we can say event.context.Vit equals Vit. So this gives VT a little hook into Now we're going to declare Module H3 Module H3. That actually needs to come under, there we go. Hold the beat dev server in, I'm gonna go ahead and fix that little bit of formatting issues because it's disturbing.

[22:50] There we go. Let's see if this runs. It Looks like we're still running. React logo is actually missing, that's interesting. Missing app assets, so that was just because of our change of rules.

[23:14] So I'm gonna go ahead and change that though. I didn't change that though. Main app, React logo. I'll learn about that later. All right, so now we are going to look at our render.

[24:10] And this needs to be updated. So similarly, this is going to be different if we are in production or in development. I'm gonna import process from node process. All right, so we're gonna define the event handler and that handles the render. And we have this, which is basically our dev server.

[24:57] So we're going to come up and I'm going to go ahead and grab that. And I'm going to say, if process. And in this case we want development we can come in here and we can say const server address And this is going to equal the context and inside of the context, we've added Vite. So we can get access to that. We can say resolved URLs and we can say local.

[25:43] That's going to give us the local server address. Close that. Now, I can return this. I'm going to pop this out just to make it readable for me. And now instead of hard coding our server address as we were before.

[26:04] We make that nice and configurable. And we can just plop that in. So I'm going to grab that string and plop it in there. And then also plop it in there. Great.

[26:27] Now, below that, If we are in production, we're going to have a manifest. And we're going to wait, and we're going to use storage. I'm going to say assets, Vite. And we're going to get an item. And we know this is a string, so I can pass that in.

[27:04] Oops. And we're gonna say manifest.json. Log that out just to see what it is. Now we have an entry chunk, const. And this is in the manifest.

[27:37] This is app main.tsx. We have some CSS links. Map over those and for each link that ends up looking like this. So We're actually going to join the links with a new line. So the link style sheet, we have the href.

[28:30] We're actually going to also add a forward slash in front of our href. We'll close that tag. Everything else looks great. So in a similar way we're going to have script links and that simply looks like this where we have a script and we're going to drop the entry chunk that file into our script links. Now we're going to have, oops, caught the tail of that.

[29:08] Now we're going to have our template, so const template. That looks like this. Inside of the head, we can add whatever we need at the bottom of the body, right? So we're going to say, script links inside of our body, we're going to have a div, and the div ID is going to be root and let's see UTF-8 HTML language we got that we can give this title in this case that's in the head body. That looks great.

[29:58] Finally, we return the template. I'm going to loop this over here. Awesome. Loop that out. There we go.

[30:12] So we have the CSS, we have the template is getting returned. Looks great. PMPM dev. Still looking good. Ish.

[30:33] That's running here. We can see it running, but it's no longer running on our localhost. So let's see what we did. I assume that's our render. I think I need it to do that.

[31:06] I don't think I needed to do that. But I do need my dev server, I think. No, that's just a type. It's not in here. That is not in here.

[31:54] That is in my Vite plugin. And we'll come over here and it's something in this render that's causing issues. So let's see if we can console log the server address, see what that does for us. Restart the dev server. Maybe the node environment is not what we expected.

[33:16] Server address. That's interesting because the server address is an array. And Down here, I'll also notice that there is no forward slash. So I'll remove those forward slashes. Hey, there we go.

[33:45] The React logo is still missing. That's interesting. But we are rendering and we should have, So we'll do something like pmpm build, pmpm preview. All right, and the built version just works, so that's nice. Our assets are there.

[34:41] And that's running on the server on localhost 3000. Fantastic. So let's add some simple error handling in here inside of the renderer because for instance if we do not have the manifest not have the manifest, we are going to set response status. I want to set the response status to context and then 500 and we'll return no manifest found. Similarly, if we do not find an entry chunk.

[36:26] There we go, we don't have any way to enter it, great. And down in our tsconfig, I am going to go ahead and add, let's see, At the root, I'm going to exclude. We are going to exclude our dist folder. We don't want that coming up in any circumstances. All right.

[37:14] I'm gonna commit this. Submit this. See, we're not using the dev proxy so we can do that. And a similar way in our Vite config, we don't have to worry about that. We are going to add build and we want to build the manifest is true.

[38:12] And we're going to add roll up options. And for our roll up options, we can say, just input, and I'm just gonna say, like this, as a string, app, main TSX. We have some options here. Let's see, we have our plugin set and we're gonna optimize our dependencies. So we can say optimize dependencies.

[38:48] And in that case, we can exclude the JSX runtime and, or excuse me, React refresh, and we can include the JSX runtime. Time. There we go, we're gonna commit that. We can actually pin our node version, so volta, we're going to pin it to 2012, which is the Great. Yeah, and we're set up and the next step would be to deploy.