vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
208k stars 33.69k forks source link

Make vue available to other libraries without having to import it #8278

Open KaelWD opened 6 years ago

KaelWD commented 6 years ago

Title needs work, idk what to call this.

What problem does this feature solve?

Writing a component library with typescript requires importing Vue so you can use Vue.extend(...) to get typings. This causes problems when webpack decides to load a different instance of vue. See https://github.com/vuetifyjs/vuetify/issues/4068

What does the proposed API look like?

Local registration to accept a function that synchronously returns a component, calling it with the parent vue instance.

The library can then do:

export default function MyComponent (Vue: VueConstructor) {
  return Vue.extend({ ... })
}

And be used like:

import MyComponent from 'some-library'

export default {
  components: { MyComponent }
}

Of course that would then cause other problems, particularly where we use methods directly from other components. Maybe something that adds types like Vue.extend() but doesn't have any runtime behaviour would be better instead?

// When used, this will behave the same as a bare options object, instead of being an entire vue instance
export default Vue.component({
  ...
})
ktsn commented 6 years ago

You may want to use https://github.com/ktsn/babel-plugin-remove-vue-extend

KaelWD commented 6 years ago

👌

How does that handle stuff like mixins (usage) or directly calling methods?

EDIT: Didn't rtfm, this looks perfect. I'll have to try it out to see how well it actually works though. I might have to massage the types a bit to get #2 working.

posva commented 6 years ago

@ktsn can we close this or is there anything we could do in Vue core to improve the situation?

KaelWD commented 6 years ago

The plugin doesn't work on everything, it seems to assume that all exports are Vue.extend() image

EDIT: Or not, I still get the same error if I wrap that component.

KaelWD commented 6 years ago

I'm fine with having to use Vue.extend, it's just not ideal that it creates a standalone instance. It would be nice if vue still resolved component references and everything else the same way as with bare options objects. It isn't really a problem anyway unless one of our users messes up and somehow ends up with two different vue imports.

yyx990803 commented 6 years ago

Alternatively we may try to detect duplicate Vue imports.

KaelWD commented 6 years ago

Yeah that would be helpful, the current errors don't really indicate what's actually going on very well. Is our current setup likely to be a problem with vue-test-utils and createLocalVue though? vuejs/vue-test-utils#532 seems similar to this one.

KaelWD commented 6 years ago

image

https://github.com/vuetifyjs/vuetify/blob/c722f79b92550405e94ec6bb0129ad56ac869aed/src/mixins/routable.ts#L69 https://github.com/vuetifyjs/vuetify/blob/c722f79b92550405e94ec6bb0129ad56ac869aed/src/components/VBtn/VBtn.ts#L150

uris-dev commented 6 years ago

A few months ago I have asked for help with an issue related to this one on stackoverflow. I've been living with my temporary solution since then (passing the Vue instance to the library initializer, and not importing Vue in the library), but I'm not very happy about it and now I'm stuck because Vuetify also started to use typescript and creates a separate Vue instance and it's not clear how to tell to webpack (or to Vue) that it's always the same instance that has to be used. The solution provided at the top of this thread does not help me much, since I'm not using babel. Are you aware of an existing solution to have a package and dependencies, all written in typescript and compiled with webpack, to share the same Vue instance?

uris-dev commented 6 years ago

The solution should also allow to use .vue files in packages, which, with the original proposal at the top of this thread would probably not be possible.

uris-dev commented 5 years ago

After giving a hard look at the problem, the only solution that I found at the moment (with webpack) is to add vue as an external import wherever you import it. In webpack.config.js

externals: {
    vue: 'Vue'
},

and then import vue.js with a script tag (or an equivalent solution) in your html file. This avoids that vue is instantiated more than once. Depending on your package and subpackage structure, you probably need to import your subpackages and or vuetify also as external modules.

That means that you have to pack them as standalone libraries, with, in webpack.config.js

    output: {
        path: 'path/to/your/output',
        filename: 'build.js',
        library: 'LibraryName',
    },

and in tsconfig.json

"compilerOptions": {
    ...
    "module": "es2015"
    ...
}

As a side effect with this solution you can also work with vue files in your libraries. The proposed solution at the top of this issue would in any case not allow that, because you cannot inject the vue constructor in a vue file (as far as I understand). If you are working with tests suites like karma and phantomjs you need to inject vue and the other external libraries there as well. This is now a workaround and a better solution would be if vue would have at initialization an option to detect if there is another VueConstructor outside the actual bundle and use it (as an option, because that may or may not be desired).

KaelWD commented 5 years ago

I use this to make sure the same file is always loaded, you can omit the path.resolve if there's no symlinks or other weird directory setup being used.

resolve: {
  alias: {
    'vue$': path.resolve(__dirname, '../node_modules/vue/dist/vue.runtime.esm.js')
    // 'vue$': 'vue/dist/vue.runtime.esm.js'
  }
}
wfischer42 commented 4 years ago

I had the "duplicate import" problem in a monorepo project using Lerna, and it seems to be related to the same problem when using npm link or yarn link. My project has a shared component package that multiple apps import, including an Electron app and a browser extension. It seems that lerna bootstrap uses symlinks, ultimately leading to some bundle confusion, in which Vue is separately imported for the main repo and the dependency, thereby causing the '$listeners and $attrs are readonly' errors.

From what I've found, the common generalized solution seems to be to mark your dependency as an 'external' in whatever build configuration you're using.

I'm using Electron with electron-builder and @vue/cli. I solved this problem by adding this to my vue.config.js:

module.exports = {
  pluginOptions: {
    electronBuilder: {
      // List native deps here if they don't work
      externals: ['my-shared-components'],
    }
  }
}