vite icon indicating copy to clipboard operation
vite copied to clipboard

Library mode output not tree-shakeable when consumed by webpack

Open mgdodge opened this issue 4 years ago • 5 comments
trafficstars

Describe the bug

When writing a library using vite "library mode," the output is expected to be tree-shakeable regardless of where it is consumed. When consumed by vite, things work properly, but when consumed by webpack, the output is not tree-shakeable.

The repo provided has a folder for a very simple vue library built by vite, which should be tree shakeable. It also has two more folders with sample apps (one vite app, one vue cli app) that attempt to use the library. The sample repo's README should explain in more detail. Both sample apps are minimal, using the default output from npm init vite or vue create. The sample library uses the recommended configuration from the vite docs for library mode.

A bit of history behind tracking down this bug can be found here - the TL;DR is that the namespaceToStringTag: true option in the vite build process emits [Symbol.toStringTag]: "Module", in the output, and webpack chokes on it. Removing/commenting that line makes webpack in the vue cli app happy again, and I can't see any ill effects in the vite app, though my tests are rudimentary.

Reproduction

https://github.com/mgdodge/vue-compiler-treeshaking-bug

System Info

System:
    OS: Linux 4.19 Ubuntu 20.04 LTS (Focal Fossa)
    CPU: (4) x64 Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz
    Memory: 1.41 GB / 7.77 GB
    Container: Yes
    Shell: 5.8 - /usr/bin/zsh
  Binaries:
    Node: 14.15.4 - ~/.nvm/versions/node/v14.15.4/bin/node
    npm: 6.14.10 - ~/.nvm/versions/node/v14.15.4/bin/npm
  Browsers:
    Firefox: 85.0
  npmPackages:
    @vitejs/plugin-vue: ^1.9.2 => 1.9.2
    vite: ^2.6.2 => 2.6.2

Used Package Manager

npm

Logs

No response

Validations

mgdodge avatar Oct 01 '21 20:10 mgdodge

Maybe this is related: https://github.com/vitejs/vite/issues/2071#issuecomment-783601579

fsblemos avatar Oct 27 '21 19:10 fsblemos

I am seeing this too with a library build with vite and consumed in an app bootstrapped using CRA5 (which includes webpack@5)

Happy to provide an example repo if it helps. But FYI my library config looks like this:

import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import dts from "vite-dts";

const isExternal = (id: string) => !id.startsWith(".") && !path.isAbsolute(id);

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ["formatjs"],
      },
    }),
    dts(),
  ],
  build: {
    lib: {
      entry: "./src/index.ts",
      formats: ["es", "cjs"],
      fileName: (format) => `index.${format}.js`,
    },
    rollupOptions: {
      external: isExternal,
      output: {
        // Since we publish our ./src folder, there's no point
        // in bloating sourcemaps with another copy of it.
        sourcemapExcludeSources: true,
      },
    },
    sourcemap: true,
    minify: false,
  },
});

My libraries package.json includes sideEffects: false.

The generated ES code seems to be littered with /* @__PURE__ */ but it isn't working and the entire file is being bundled by webpack. In a Vite app it seems to work OK though but it would be great to figure out a solution to this as I am out of my depth.

robcaldecott avatar Feb 10 '22 17:02 robcaldecott

Has anyone found a solution to this? I'm in the same pickle with tree shaking vite library into a webpack project.

I have sideEffects: false set in package.json within the component library.

kinoli avatar Apr 15 '22 15:04 kinoli

For background into Webpack's behavior, sideEffects only optimizes module imports/exports -- this has no effect when everything is bundled into a single file (see https://github.com/webpack/webpack/issues/9337#issuecomment-507074474). Also see Clarifying tree shaking and sideEffects.

A solution to this is to not bundle, via Rollup's preserveModules:

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    minify: false,
    target: 'esnext', // use w/e here but don't set it too loose
    lib: {
      fileName: '[name]',
    },
    rollupOptions: {
      output: {
        preserveModules: true,
      },
    },
  },
})

CodyJasonBennett avatar Jul 24 '22 23:07 CodyJasonBennett

The unused code is not removed in Webpack because terser does not remove it.

var cat = /* @__PURE__ */ a("cat");
var dog = /* @__PURE__ */ a("dog");
var fish = /* @__PURE__ */ a("fish");

var components = /* @__PURE__ */ Object.freeze({
  __proto__: null,
  [Symbol.toStringTag]: "Module",
  Cat: cat,
  Dog: dog,
  Fish: fish
});

This is the output from terser for the code above.

var g=a("cat"),o=a("dog"),t=a("fish");Symbol.toStringTag;

If you set compress.passes = 2, most thing are removed:

Symbol.toStringTag;
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.optimization.minimizer('terser').tap(args => {
      const { terserOptions } = args[0]
      terserOptions.compress.passes = 2
      return args
    })
  }
}

Vite could set output.generatedCode.symbols = false/output.namespaceToStringTag = false as default. But that will cause other problems (#764).

sapphi-red avatar Sep 11 '22 08:09 sapphi-red

Has anyone found a solution to this?

wuyuweixin avatar Jan 09 '23 11:01 wuyuweixin

I was experiencing two issues when building with vite. I am using react and vite instead of vue and webpack but I am leaving this comment because I believe the solution to be the same and it took me so long to figure out.

  • esbuild doesn't mark some top-level exports as /* @__PURE__ */ by default. -- esbuild Pure -- ignore annotations
  • The bundle was being bundled into a single file and the consuming repo was unable to tree shake as expected as mentioned by above @CodyJasonBennett.

tl;dr to enable tree-shaking

  1. Mark all functions that are able to be dropped if unused, tree-shaken, with /* @__PURE__ */. (use caution)
// src/icon.tsx

/* @__PURE__ */
export const Icon = React.forwardRef<SVGSVGElement, IconProps>(
  (props, forwardedRef) => (
    <svg {...props} ref={forwardedRef}>
      <path
        d="M14.5"
        fill={props.color}
      />
    </svg>
  ),
)
  1. Preserve modules in your build step:
// vite.config.ts

rollupOptions: {
      output: {
        preserveModules: true,
      },
    },

How to validate:

In your dist/index.es.js check for the annotation /* @__PURE__ */

Will be tree shaken:

const Icon = /* @__PURE__ */ React.forwardRef(
  (props, forwardedRef) => React.createElement("svg", { ...props, ref: forwardedRef }, /* @__PURE__ */ React.createElement(
    "path",
    {
      d: "M14.5",
      fill: color
    }
  ))
);

Will Not be tree-shaken

const Icon = React.forwardRef(
  (props, forwardedRef) => React.createElement("svg", { ...props, ref: forwardedRef }, /* @__PURE__ */ React.createElement(
    "path",
    {
      d: "M14.5",
      fill: color
    }
  ))
);

hoop71 avatar Feb 15 '23 23:02 hoop71

I had the same problem building my library with Vite lib mode ..

Either, @hoop71 solution works but you have to mark everything with /* @__PURE__ */ or use rollup-plugin-pure to help with that.

Either, don't use lib mode and use rollup like I did :

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import dts from "vite-plugin-dts";
import pkg from "./package.json";
import { visualizer } from "rollup-plugin-visualizer";

export default defineConfig({
  esbuild: {
    minifyIdentifiers: false,
  },
  build: {
    rollupOptions: {
      preserveEntrySignatures: "strict",
      input: ["src/index.ts", "src/nav/index.ts"],
      external: [...Object.keys(pkg.peerDependencies)],
      output: [
        {
          dir: "dist",
          format: "esm",
          preserveModules: true,
          preserveModulesRoot: "src",
          entryFileNames: ({ name: fileName }) => {
            return `${fileName}.js`;
          },
        },
      ],
    },
  },
  plugins: [
    react({
      jsxRuntime: "classic",
    }),
    visualizer(),
    dts(),
  ],
});

⚠️ jsxRuntime : "classic" is removed from @vitejs/[email protected]... I have not upgrade it to v4 yet and handle automatic runtime. Read more

clementcreusat avatar May 05 '23 17:05 clementcreusat