Make NPM package with Vue Single File Component (SFC) working with Vue 2 and 3

November 22, 2021 ≈ 3 minutes 29 seconds

Recently I made a single file upload component for Vue with drag'n'drop support. I made it suitable for my Vue2 and Vue3 projects and while publishing on NPM, I thought there would be a simple way to make it installable for both Vue2 and Vue3 users. But there was a number of catches.

Even if your component is perfectly compatible with two versions (no composition API, no v-model, etc), you can't easily build it into universal .esm.js, because of different render functions in both.

So what to do?

(1) Different branches — Every time I made a change, I must rebuild and publish two different versions for Vue2 and Vue3. Also, there would be two different installation instructions.

(2) Figure out a way to make package universal

The first way doesn't feel right for the component, perfectly compatible with two versions. So I decided to figure out the second, using:

vue-sfc-rollup for easy rollup.js configuration

@LinusBorg answer to alvarosaburido question

● NPM postinstall hook


Scaffold a minimal setup for compiling a Vue2 SFC

npx vue-sfc-rollup
cd single-file-upload-for-vue && npm install && npm run build

You would get:

.browserslistrc 
.gitignore         
babel.config.js 
build                     
    rollup.config.js
dev             
    serve.js  (renders serve.vue and mounts to #app)   
    serve.vue (Vue component with our component inside)
dist
    single-file-upload-for-vue.esm.js (ECMAScript module - import * from *)
    single-file-upload-for-vue.min.js (minified version for unpkg)
    single-file-upload-for-vue.ssr.js (CommonJS module - require())
node_modules
package-lock.json
package.json
src
    entry.esm.js (input for rollup.config.js ECMAScript module config)
    entry.js (input for rollup.config.js)
    single-file-upload-for-vue.vue (our SFC)

Repeat previous step for Vue3, to get rollup.config.js

Move rollup.config.js for Vue3 into your Vue2 build folder and add _vue3 to the name. You would get two configs in build/ folder:

rollup.config.js
rollup.config_vue3.js

Open rollup.config_vue3.js and change dist/ path into dist/vue3/, after mkdir -p dist/vue3/


Install missing devDependencies for Vue3 SFC compilation

npm i --save-dev @vue/compiler-sfc postcss rollup-plugin-postcss

Also, Vue2 and Vue3 use different versions of rollup-plugin-vue, so you must install both:

"rollup-plugin-vue": "^5.1.9",
"rollup-plugin-vue3": "npm:rollup-plugin-vue@^6.0.0",

Change scripts section in package.json

We need different builds for Vue2 and Vue3. Vue2 would be in dist/ folder and Vue3 in dist/vue3. Don't bother yourself with postinstall.js and build.php, we get to it in the next steps.

"scripts": {
    "build": "npm run build-vue2 && npm run build-vue3 && php ./build/build.php",
    "build-vue2": "cross-env NODE_ENV=production rollup --config build/rollup.config.js",
    "build-vue3": "cross-env NODE_ENV=production rollup --config build/rollup.config_vue3.js",
    "postinstall": "node ./postinstall.js"
},

Write build/build.php to concatenate minified versions for unpkg

That min.js with two compiled modules (for Vue2 and Vue3) would be used for CDN loads with <script> tag. Module would be chosen based on Vue.version[0]===2

$file1 = file_get_contents('./dist/single-file-upload-for-vue.min.js');
$file2 = file_get_contents('./dist/vue3/single-file-upload-for-vue.min.js');

$file1 = str_replace('var SingleFileUploadForVue=', "var SingleFileUploadForVue=Vue.version[0]==='2'?", $file1);
$file1 = substr($file1, 0, -1).':';

$file2 = str_replace('var SingleFileUploadForVue=', '', $file2);

file_put_contents('./dist/min.js', $file1 . $file2);

The downside of that approach is that the min.js is double the size, which isn't a big problem for small components (16kb vs 8kb). Would love to find a better approach though.


Write postinstall.js to move suitable version of module after installation

After package is installed, npm runs postinstall.js which in our case detects which version of Vue is used. And if it's Vue3, copies modules from dist/vue3 into dist/ folder.

const fs = require('fs')
const path = require('path')
let dir = path.resolve(__dirname, 'dist')

function loadModule(name) {
    try { return require(name) } catch (e) { return undefined }
}

function copy(name) {
    const src = path.join(dir, 'vue3', name)
    const dest = path.join(dir, name)

    let content = fs.readFileSync(src, 'utf-8')

    try { fs.unlinkSync(dest) } catch (error) { }
    fs.writeFileSync(dest, content, 'utf-8')
}

const Vue = loadModule('vue')

if (!Vue || typeof Vue.version !== 'string') {
    console.warn('[single-file-upload-for-vue] Vue is not found. Please run "npm install vue" to install.')
} else if (Vue.version.startsWith('3.')) {
    console.log(`[single-file-upload-for-vue] installing for vue3 from dir ${dir}`)
    copy('single-file-upload-for-vue.esm.js')
    copy('single-file-upload-for-vue.ssr.js')
}

Then add postinstall.js into files section of package.json

"files": [
    "postinstall.js",
    "dist/*",
    "src/**/*.vue"
]

That's it! There are not so many creators of npm packages with Vue SFC components, and most (I think) would go with branch solution. So that article is mostly for me. But if you read it and found it useful, share and hit the like button at the top, to let me know that there are like-minded people out there :)

PS: @beaubus/single-file-upload-for-vue package


Subscribe to our newsletter

If you provide url of your website, we send you free design concept of one element (by our choice)

Subscribing to our newsletter, you comply with subscription terms and Privacy Policy