Search code examples
javascriptvue.jsnpmvue-cli-3modularity

Application modularity with Vue.js and local NPM packages


I'm trying to build a modular application in Vue via the vue-cli-service. The main app and the modules are separated projects living in different folders, the structure is something like this:

-- app/package.json
      /src/**
-- module1/package.json
          /src**
-- module2/package.json
          /src**

The idea is to have the Vue app completely agnostic about the application modules that can be there at runtime, the modules themself are compiled with vue-cli-service build --target lib in a local moduleX/dist folder, pointed with the package.json "main" and "files" nodes.

My first idea (now just for development speed purposes) was to add the modules as local NPM packages to the app, building them with a watcher and serving the app with a watcher itself, so that any change to the depending modules would (I think) be distributed automatically to the main app.

So the package.json of the app contains dependencies like:

...
"module1": "file:../module1",
"module2": "file:../module2",
...

This dependencies are mean to be removed at any time, or in general be composed as we need, the app sould just be recompiled and everything should work.

I'm trying to understand now how to dynamically load and activate the modules in the application, as I cannot use the dynamic import like this:

import(/* webpackMode: "eager" */ `module1`).then(src => {
    src.default.boot();
    resolve();
});

Because basically I don't know the 'module1', 'module2', etc...

In an OOP world I would just use dependency injection retrieving classes implementing a specific interface, but in JS/TS I'm not sure it is viable.

There's a way to accomplish this?


Solution

  • Juggling with package.json doesn't sound like a good idea to me - doesn't scale. What I would do:

    1. Keep all available "modules" in package.json
    2. Create separate js file (or own prop inside package.json) with all available configurations (for different clients for example)
    module.exports = {
       'default': ['module1', 'module2', 'module3'],
       'clientA': ['module1', 'module2', 'module4'],
       'clientB': ['module2', 'module3', 'module4']
    }
    
    1. tap into VueCLI build process - best example I found is here and create js file which will run before each build (or "serve") and using simple template (for example lodash) generate new js file which will boot configured modules based on the value of some ENV variable. See following (pseudo)code (remember this runs inside node during build):
    const fs = require('fs')
    const _ = require('lodash')
    const modulesConfig = require(`your module config js`)
    
    const configurationName = process.env.MY_APP_CONFIGURATION ?? 'default'
    const modules = modulesConfig[configurationName]
    
    const template = fs.loadFileSync('name of template file')
    const templateCompiled = _.template(template)
    const generatedJS = templateCompiled({ `modules`: modules })
    fs.writeFileSync('bootModules.js', generatedJS)
    
    1. Write your template for bootModules.js. Simplest would be:
    <% _.forEach(modules , function(module) { %>import '<%= module %>' as <%= module %><% }); %>;
    
    1. import bootModules.js into your app
    2. Use MY_APP_CONFIGURATION ENV variable to switch desired module configuration - works not just during development but you can also setup different CI processes targeting same repo with just different MY_APP_CONFIGURATION values

    This way you have all configurations at one place, you don't need to change package.json before every build, you have simple mechanism to switch between different module configurations and every build (bundle) contains only the modules needed....