Include importable modules from outside project folder for webpack HMR Vue.js

  • Vue-cli: v3.11.0
  • webpack: v4.40.2
  • babel: 7.6.0
  • node: v10.16.3

I just ran into a similar situation tonight where I wanted to move out a few custom libraries into an external shared lib directory for agile development and mocked testing. I just started learning the Vue/webpack ecosystem about two weeks ago, and I can definitely say it's been an adventure with webpack being a daunting beast in figuring out all the mystical knobs and buttons that have to be aligned just right.

./devel  (webpack alias: TTlib)
+-- lib
|   +-- widget.js
|   +-- frobinator.js
|   +-- suckolux3000.js
+-- share (suckolux3000.js taps into this directory)
|   +-- imgs
|   +-- icons
|   |   +-- img
|   |   +-- svg
+-- MainProject
|   +-- vue.config.js
|   +-- package.json
|   +-- node_modules
|   +-- src
+-- TestProject
|   +-- vue.config.js
|   +-- package.json
|   +-- node_modules
|   +-- src

My concern was making sure that webpack HMR would work, and I can say you were close, but did not go far enough. Fortunately the fix was pretty simple.

Here are my vue.config.js additions:

module.exports = {
  configureWebpack: {
    resolve: {
      alias: {
        'TTlib': '/devel/lib'
      },
      modules: ['/devel/lib']
    },
    resolveLoader: {
      modules: ['/devel/lib']
    }
  }
};

Now from within any of my *Project components, I can just use:

import frobinator from 'TTlib/frobinator'

Now if I edit any of the project or (external) lib files, webpack HMR will kick in and refresh the running 'yarn serve' daemon and emit the changes I've made.

Hope that helps!


I had been using Terra's solution for a while for a similar situation and this works fine, but there was one downside for me, namely if the shared code also requires node_modules content of their own, then intellisense didn't work and when building the project, errors would appear (Cannot find module ...) even though everything does work fine in the browser. Maybe it's because I'm using TypeScript? Either way, using separate packages or advanced tools like bit seemed like too much of a hassle, so I went to look for a simpler solution without the downside.

I've put my attempts in this repository: https://github.com/brease-colin/vue-typescript-shared where there's one project folder (project1) and three shared folder attempts, all linked in project1 at the same time, all with their own downside(s), but I'm planning on using attempt 3 as it's not really a downside for us.

BTW, there's one issue that was common for all three, but maybe that was my VSCode acting weird. If opening the root folder in VSCode, then Intellisense within Vue files didn't understand any of the three solutions' imports, while if opening the project1 folder in VSCode, it understood imports for all three. The below description of each attempt assumes opening project1 in VSCode.

Main files to look at are:

  • project1/src/components/HelloWorld.vue : vue file that attempts to reuse a shared composition function and a shared component
  • project1/src/data/ProjectDummies.ts : ts file that attempts to reuse a shared ts file
  • project1/tsconfig.json
  • project1/vue.config.js
  • shared[1/2/3]/components/Header[1/2/3].vue : a shared component that imports from @vue/composition-api.

Attempt 1: shared1 with alias @s1

This is a Typescript version of Terra's answer. I've only added a path and two include paths to the tsconfig file to make intellisense work. Also check out shared1/tsconfig.json, because there I've added the same alias (@s1), so references between shared files go well.

Pros: no additional setup needed, all works well in the browser, no warnings / errors in console, you can easily open shared1 as additional folder in your VSCode workspace, so you can edit all shared1 files.

Cons: references to node_modules do not work in VSCode and an error is output in the terminal for that as well. As such, no intellisense / no type safety when using the classes in your project folder. That last part is a big con for me.

Attempt 2: shared2 with alias @s2

To try and fix it, I've added a package.json to the shared folder and installed the required packages (in this case: @vue/composition-api) for the shared files. This made the intellisense work and the terminal output error upon build is also gone. However, now the code breaks down on runtime in the browser, because the dependencies are added twice and imports in the shared folder do not refer to the same module code. This won't give errors in all types of dependencies, but in some cases, some constants are initialized that should be the same across the whole codebase. The errors I got were:

[Vue warn]: onMounted is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup().

[Vue warn]: Error in data(): "Error: [vue-composition-api] must call Vue.use(VueCompositionAPI) before using any function."

found in

---> <Header2> at shared2/components/Header2.vue
   <HelloWorld> at src/components/HelloWorld.vue
     <Home> at src/views/Home.vue
       <App> at src/App.vue
         <Root>

I've tried fixing this by changing the module resolve / resolveLoader, making sure that it first checks the main dir's node_modules before it tries looking the 'regular' way, but it didn't seem to help.

modules: [
  path.resolve(__dirname, 'node_modules'),
  'node_modules',
],

Maybe someone has a way to fix it properly?

Pros: Intellisense works, no more build errors.

Cons: Runtime errors, no working application, plus extra complexity / risks by having to keep the modules at the same version numbers between project1 / shared2 to keep intellisense working consistently.

Attempt 3: shared3 with alias @s3

Code wise, attempt 3 is not so different from attempt 1, but using a simple trick, everything works as I wanted it. The trick is using a symlink and I've used a multiplatform npm package called symlink-dir to do that easily. I've actually added it as a dev dependency to the project1's package.json:

npm install --save-dev symlink-dir

Afterwards, I've made the symlink as below, which you have to do once every time you clone the repository.

npx symlink-dir ../shared3/ shared3/

Now, to prevent from checking in the code twice, you'll have to add a line to your .gitignore:

// .gitignore
*/shared3/

Because the shared code is now within your project folder, you could even access it without an alias, but to more easily let files in the shared folder access eachother, I do prefer a fixed alias, so I added it by adding the following configuration to vue.config.js and tsconfig.json:

// vue.config.js
configureWebpack: {
  resolve: {
    alias: {
      '@s3': path.resolve(__dirname, 'shared3'),
    },
  }
},

// tsconfig.json
"compilerOptions": {
  // only needed for auto completion(?)
  "paths": {
    "@s3/*": ["shared3/*"],
  },
},

Although I would prefer a config only solution to improve shared1, I'm still very happy with this end result as it works perfectly for us as a small team.

Pros: No warnings, errors, etc.: all works as if the code is in the project, which kind of is the case, while still having the code accessible in other projects.

Cons: Every developer needs to create the symlink manually before their code will compile / work with intellisense. Then again, they also have to run npm install for that.