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

Including server dependencies in the production build #24

Closed
iMrDJAi opened this issue Feb 3, 2023 · 7 comments
Closed

Including server dependencies in the production build #24

iMrDJAi opened this issue Feb 3, 2023 · 7 comments
Labels
enhancement New feature or request

Comments

@iMrDJAi
Copy link

iMrDJAi commented Feb 3, 2023

The generated production build lacks the external dependencies defined on package.json. As a result, we can't just copy the /dist directory into a container and expecting it to work. While in the development phase the server could just use the packages from the /node_modules directory that is located in the project root, it has to be included inside /dist/server when the /dist directory is standalone.

Copying the /node_modules manually after the build step may work, but it includes packages that aren't necessarily referenced in the server code (e.g. dev dependencies, dependencies used in the client code), so we're increasing the build size for no reason.

I have tried including those packages inside Vite's ssr.noExternal array, however, Vite isn't capable yet of transpiling all node packages, for example node-canvas that includes a native node module (.node binary) requires a proper loader that Vite lacks, let alone the increased build time that is caused by processing tons of files!

So, my proposal is the following:

1- Listing all dependencies used in the server code.
2- Writing them inside /dist/server/package.json.
3- Performing npm install right after the building process ends.

For the first step, I've tried writing a simple plugin to list all imported dependencies:

const config = defineConfig(({ mode }) => {
  return {
    // ...
    plugins: [
      (() => {
        // import pk from './package.json'
        const deps = Object.keys(pk.dependencies)
        const usedDeps = new Set<string>()
        return {
          name: 'Logger',
          id: 'logger',
          enforce: 'pre',
          resolveId(source, importer, options) {
            const usedDep = deps.find(dep => source.startsWith(dep))
            if (usedDep) {
              usedDeps.add(usedDep)
              console.log('\n')
              console.log(source, importer, options)
            }
          },
          buildEnd() {
            console.log(usedDeps)
          }
        }
      })(),
      vavite({
        serverEntry: './index.ts',
        serveClientAssetsInDev: true
      }),
      // ...
    ],
    // ...
  }
}

But it only lists packages imported from the client code. I assume that the server build process is isolated? Thoughts?

@cyco130
Copy link
Owner

cyco130 commented Feb 6, 2023

Hi,

Installing and building on one machine and copying used modules and build artifacts into a different machine (or container) isn't even supposed to work. Machines have to have the exact same Node version and OS. For example native modules you install on Windows will not work on Linux if you copy them over. My recommendation would be to run the build inside the container itself.

If, for some reason, you don't want to do that, you can hand roll a manual solution like the one you describe. In any case, it would be out of scope for vavite and it would have to be handled at Vite level.

One trick I used to use was this:

  • Make sure dependencies only contain deps that are needed on the server (so client-only deps and build-only deps go to devDependencies.
  • Build on my local machine and then run npm pack
  • Upload/copy the tarball to the production machine.
  • Untar and install only production deps.
  • Run.

This is still susceptible to Node version and native module mismatches though, so I don't do it anymore.

@cyco130 cyco130 closed this as completed Feb 6, 2023
@iMrDJAi
Copy link
Author

iMrDJAi commented Feb 7, 2023

@cyco130 Hello, thank you for your insights.

At this point I'm convinced that a manual solution is the way to go. To be clear I'm using GitHub actions to build the project then I copy the files to a docker container then I push it to the cloud, both environments are running Linux, so I never faced a compatibility issue. Building on the container itself would be much slower, the approach I'm taking saves a lot of time, but copying node_modules still causes a bottle neck, probably npm pack would help optimizing that.

My vision for an ultimate Vite setup is being able to have a truly standalone server build, just like how it used to be with Webpack, but most likely it still too early for that.

@jensbodal
Copy link
Contributor

Hi,

Installing and building on one machine and copying used modules and build artifacts into a different machine (or container) isn't even supposed to work. Machines have to have the exact same Node version and OS. For example native modules you install on Windows will not work on Linux if you copy them over. My recommendation would be to run the build inside the container itself.

If, for some reason, you don't want to do that, you can hand roll a manual solution like the one you describe. In any case, it would be out of scope for vavite and it would have to be handled at Vite level.

One trick I used to use was this:

  • Make sure dependencies only contain deps that are needed on the server (so client-only deps and build-only deps go to devDependencies.
  • Build on my local machine and then run npm pack
  • Upload/copy the tarball to the production machine.
  • Untar and install only production deps.
  • Run.

This is still susceptible to Node version and native module mismatches though, so I don't do it anymore.

The issue for me here is that I don’t mind doing the build on the machine/container that will run the code, but if possible I’d like to do the build and bundle it if possible to remove unnecessary code/libs that aren’t actually needed in the container to reduce the overall size of the container. It’s fine if that’s outside the scope of vavite but that’s where I’m coming from when looking for existing issues on this topic.

@cyco130
Copy link
Owner

cyco130 commented Apr 1, 2023

I have a possible solution for this, based on @vercel/nft. Stay tuned :)

@cyco130 cyco130 reopened this Apr 1, 2023
@awhiteside1
Copy link

👋 , I've been trying to achieve a similar 'distributable build' using Vavite and brillout/vite-plugin-ssr. Happy to share more context, but the TL;DR; is a lightweight PaaS that can run pre-build apps from a private registry, and route public URLs to them based on a configuration editable in a CMS. The goal of which to allow easy creation and deployment of 'micro apps' in our marketplace without developers needing to understand docker, underlying infra, or wiring up anything more than an npm package.

We ran into an issue similar to @iMrDJAi 's , related to the auto-import feature of vite-plugin-ssr expecting the build time env to exist 'as-is' at runtime. . I initially got around it by replacing the relative path with a runtime path provided by node's resolve module, however ran into issues when multiple apps were run on the same node instance since it used global variables to register userland code. I didnt have much luck trying to get Vite to bundle the server for me, but I was successful in using vercel/ncc out of the box to re-bundle the server after vite was done with it, which spit out a set of static files similar to Next's standalone feature.

I plan to make a vite plugin to handle all this nicely, but as a proof of concept running pnpm vavite && pnpm ncc build dist/server/index.mjs -o dist/out then copying dist/out into a distroless node docker image seems to work without much fuss. I'd prefer to not have this much obfuscation as part of the build process, but it seems to work at least to unblock my current needs, and hopefully those finding this.

@cyco130 cyco130 added the enhancement New feature or request label May 23, 2023
@cyco130
Copy link
Owner

cyco130 commented Jul 26, 2023

I haven't built a ready-to-use package yet but I have a small script that can do the trick. Assuming your server entry is in dist/server/index.js and your client assets are in dist/client, this script copies every file that is needed by the app into the /out/ folder.

import { nodeFileTrace } from "@vercel/nft";
import { spawn } from "node:child_process";
import { mkdir } from "node:fs/promises";

const result = await nodeFileTrace(["./dist/server/index.js"], {});
const files = [...result.fileList];
const out = "/out/";

await mkdir(out, { recursive: true });

// spawn("rsync", ["-Rrl", "dist/client", ...files, out], {
spawn("cp", ["-ar", "--parents", "dist/client", ...files, out], {
  stdio: "inherit",
}).on("exit", (code) => {
  process.exit(code);
});

@vercel/nft creates a list of files and the script uses cp (or rsync) to copy them into the /out directory. Then you can copy that dir from your build machine/image into the runner image, for example.

This will probably fail in a monorepo where some of the files are outside of the current root but one can accommodate for that with a little bit of effort.

I will close this issue because I think this is out of scope for vavite but if anyone creates a ready-to-use solution on top of this idea, I'd love to hear about it.

@cyco130 cyco130 closed this as completed Jul 26, 2023
@thomasjm
Copy link

thomasjm commented Oct 6, 2023

this script copies every file that is needed by the app into the /out/ folder.

This looks like it doesn't do all the niceties of actual bundling, right? Like dead code elimination, tree-shaking etc. to reduce the size.

FWIW, I think I've almost got real bundling working using a pure Vite method, by just adding an additional input to build.rollupOptions. The relevant config looks like this:

export default defineConfig(async (env: ConfigEnv) => ({
  plugins: [
    // Work around https://github.com/vikejs/vike/issues/1163
    {
      name: "no-external-vike",
      config(config, _env) {
        if (env.ssrBuild) config.ssr.external = [];
      },
    },
  ],

  ssr: env.ssrBuild
    ? {
        noExternal: [/^(?!(process))/],
      }
    : {},

  build: {
    rollupOptions: {
      input: env.ssrBuild
        ? {
            index: "server/index_prod.ts",
          }
        : {},
    },
  },
}));

The only problem is that one import, import { renderPage } from "vike/server";, is still getting externalized, even if I add vike to noExternal. I suspect Vike is doing something itself to the rollup config here. But anyway, this feels like it's close to the standalone server build with Webpack mentioned above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants