Search code examples
javascriptvue.jsrollup

optimize rollup config to enable tree shaking


I'd like to have advices on how to build a convenient-to-use and tree-shaking optimized build. I'm using rollup to package a UI library made of multiple components.

My architecture is :

/src
  /index.js
  /components
    /index.js
    /component1
      /index.js
      /Comp.vue
    ...
    /componentN
      /index.js
      /Comp.vue
  /directives
    /index.js
    /directive1
      /index.js
    ...
    /directiveN
      /index.js

the src/components/index.js looks like

export { default as Comp1 } from './component1
...
export { default as CompN } from './componentN

the src/directives/index.js looks like

export { default as Directive1 } from './directive1
...
export { default as DirectiveN } from './directiveN

Each internal index.js is just a binding for convenience, such as

import Comp from './Comp.vue'
export default Comp`

Finally the src/index.js will gather all with :

import { * as components } from './components'
import { * as directives } from './directives'

export { components, directives }

When building, the rollup config looks like :

{
  input: 'src/index.js',
  output: {
    file: 'dist/lib.esm.js',
    format: 'esm',
}

(of course i'm avoiding all the transpiling uglifying plugins, i think they'd be noise to this issue)

So this build looks nice, and works, but...

  1. It's high inconvenient to use :
import { components } from 'lib'
const { Comp1 } = components
  1. This construction probably also breaks tree shaking at use, because we import the full components object, when only Comp1 is needed.

I understand that I should not be the one caring about tree shaking, but rather providing a tree shaking capable library, and that's what this is about. When testing my build with the most simple @vue/cli template, the full library got imported, even @vue/cli claims to have webpack-treeshaking feature enabled out of the box.

I don't mind building separate files instead of one big esm build, but as much as i recall, one file build with tree shaking were possible. My fear of building separate files is that a CompA could internally need CompB, and if the user also need CompB, in that case it could probably be duplicated in the build (as in, one external use version and one internal use version).

I'm clueless on how to proceed to optimize. Any pointer is highly welcomed.


Solution

  • As for now, the only valid solution I could find is to build separately all the files in the same tree structure inside a dist/ folder. I decided to build the files to provide Vue fileformat style blocks without further need for build or configuration from end consumer.

    It looks like this after build :

    /src
      /index.js
      /components
        /index.js
        /component1
          /index.js
          /Comp.vue
        ...
        /componentN
          /index.js
          /Comp.vue
      /directives
        /index.js
        /directive1
          /index.js
        ...
        /directiveN
          /index.js
    
    /dist
      /index.js
      /components
        /index.js
        /component1
          /index.js
        ...
        /componentN
          /index.js
      /directives
        /index.js
        /directive1
          /index.js
        ...
        /directiveN
          /index.js
    

    I created a small recursive function to find all 'index.js' and used this list with rollup multi entrypoint feature. hopefully, rollup creates all subfolders so there's no need for checks or mkdir -p.

    // shorthand utility for 'find all files recursive matching regexp (polyfill for webpack's require.context)'
    const walk = (directory, regexp) => {
      let files = readdirSync(directory)
    
      if (directory.includes('/examples'))
        return []
    
      return files.reduce((arr, file) => {
        let path = `${directory}/${file}`
        let info = statSync(path)
    
        if (info.isDirectory())
          return arr.concat(walk(path, regexp))
        else if (regexp.test(file))
          return arr.concat([path])
        else
          return arr
      }, [])
    }
    
    // ...
    
    const esm = walk(`${__dirname}/src`, /^index\.js$/)
      .map(file => ({
        input: file,
    
        output: {
          format: 'esm',
          file: file.replace(`${__dirname}/src`, CONFIG.module)
        },
        ...
      }))
    

    The last part of the process is to copy/paste package.json into dist/, cd into it and npm publish from it... This was integrated into our CI tasks, as it's not directly related to rollup or build, but rather publishing.

    It's not perfect, but it's the only way I found due to lack of inputs. I hope it'll help someone.