vuejs / vue-cli

πŸ› οΈ webpack-based tooling for Vue.js Development
https://cli.vuejs.org/
MIT License
29.76k stars 6.33k forks source link

Allow multiple entries when using build with the target flag. #3922

Open chopfitzroy opened 5 years ago

chopfitzroy commented 5 years ago

What problem does this feature solve?

It would allow users to build multiple libraries at once, ideally this would be usable with the --watch flag.

I have posted a detailed issue on the forum describing my use case for those interested, but after looking at the code I can see that this will most likely require updates to the internal structure of the Vue cli.

This is primarily aimed at people building modular component libraries on top of Vue (particularly ones that can be loaded from a CDN which is my use case).

For additional background I am trying to pre-compile my components (using --target lib) and then load them over http using the technique outlined here but that is outside of the context of what I am actually asking for.

What does the proposed API look like?

If no path is provided when using build with the --target command fallback to the entries config (particularly the one in vue.config.js) ie:

    config.entryPoints.delete("app");
    // Add entry points
    config
      .entry("component-one")
      .add("./src/components/ComponentOne.vue")
      .end()
      .entry("component-two")
      .add("./src/components/ComponentTwo.vue")
      .end();
LinusBorg commented 5 years ago

How would the --name option for all of these library be set individually?

chopfitzroy commented 5 years ago

@LinusBorg so currently if I run vue-cli-service build with the above entry config it will build component-one.js and component-two.js I am guessing this comes from the entry name? So that is how I would see that working, but setting the name individually could still work as you are going to have to add the path to each component manually anyway...

Originally I was messing around with being able to glob a directory i.e components/**/*.vue and then base the file name off the component file name, but then I realized this didn't really work as I actually only wanted to build a small subset of all my components as libraries as lots of the components were just smaller pieces of the components that became libraries, hence the needing to set up each one manually.

LinusBorg commented 5 years ago

I'm not talking about the filename. I'm talking about what webpack calls libraryTarget, and Vue CLI calls --name in the build command options. It's the name of the global variable that each component is available as when imported from a CDN as a UMD bundle.

chopfitzroy commented 5 years ago

Oh I see, sorry!

Ahhh either the entry name or possibly an additional resolver like name() that falls back to the entry name? I am not entirely sure whatever fits in best with the "Vue" way of doing things.

chopfitzroy commented 5 years ago

@LinusBorg, if this is not in the scope of the vue-cli is this the kind of functionality that could be achieved via webpack-chain? I have been kicking around with some ideas in my head but I think this functionality occurs at too lower level to be able to inject something to achieve this?

chopfitzroy commented 5 years ago

Is this the kind of change that should be opened as a RFC in https://github.com/vuejs/rfcs?

I am not sure if the change is significant enough to warrant an RFC.

morficus commented 5 years ago

This is primarily aimed at people building modular component libraries on top of Vue

πŸ™‹β€β™‚ hi, hello, yes. this is me. this is exactly what I'm trying to do.

Currently I'm just doing vue-cli-service build --target lib --name my-lib-name ./src/index.js (where index.js has a list of imports and named exports for each component). This results in a single output file... but consumers of the library would still be importing ALL components unless they have tree-shaking turned on.

The problem we are running in to now is that some consumers are doing SSR... and not all components are SSR-friendly (they relay on browser APIs like localStorage, etc). But because everything gets rolled in to a single file... even if they import an SSR-friend component, an error still happens because it's trying to evaluate the entire file (this is specific to non-production / local dev mode, where tree-shaking is never enabled).

So now we are looking to create an individual export file per component. But just realized that the vue-cli-service does not allow for multiple inputs nor multiple outputs (I tried doing vue-cli-service build --target lib ./src/**/*.vue which did not give an error.... but did produce a rather bizarre file with not my components haha).

I did come across this issue which is somewhat related: https://github.com/vuejs/vue-cli/issues/2744, which is not ideal but does get us a closer to what we need.

Anyways... that is just a very long-winded way of saying that I would love for vue-cli-service to support multiple and dynamic entry points πŸ™

morficus commented 5 years ago

I'm not talking about the filename. I'm talking about what webpack calls libraryTarget, and Vue CLI calls --name in the build command options.

@LinusBorg I think webpack calls this library (libraryTarget is the equivalent to --format in the Vue CLI).

But to answer your question, my recommendation would be to follow what webpack calls a multi-part library

module.exports = {
    // mode: "development || "production",
    entry: {
        alpha: "./alpha",
        beta: "./beta"
    },
    output: {
        path: path.join(__dirname, "dist"),
        filename: "MyLibrary.[name].js",
        library: ["MyLibrary", "[name]"],
        libraryTarget: "umd"
    }
}

In this example, --name would be "MyLibrary" and the "name" would come from the entry name.

chopfitzroy commented 4 years ago

Finally getting a chance to dig back into this, looks like #1065 is strongly related to this.

xfyre commented 4 years ago

Another use case for this is when you embed Vue into an existing non-SPA application. To modernize pages separately you'll need to build a Vue library for each one of them, so multiple entries is a necessity to avoid (1) invoking vue-cli-service multiple times during production build (2) supporting dev workflow when vue-cli-service should run in the background and watch for changes.

lukenofurther commented 4 years ago

We are using the exact use case that @xfyre talks about. Our site is running on a Java CMS that incorporates Vue components in its templates that we bundle up as libraries.

To achieve that we're using a manual Webpack build similar to the approach @morficus suggests - using the object syntax for the entry point config key. It would be helpful to be able to use Vue CLI instead of a manual Webpack build.

I spotted someone attempting a solution using Vue CLI on the Vue forums. They tapped the Webpack config to add multiple entry points. However, it didn't appear to work for them. I haven't attempted this approach yet.

sunsande commented 4 years ago

The following option works(tested around July 2020 with @vue/cli 4.4.6 ; vue@2.6.11 ): Create simpe vue.js project with the following structure image

ComponentA.vue and ComponentB.vue could be any of your choice.

ComponentA.js

import Vue from "vue";
import ComponentA from "./components/ComponentA.vue"

Vue.config.productionTip = false;

new Vue({
  render: h => h(ComponentA)
}).$mount("#componentA");

ComponentB.js is just like ComponentA.js with A replaced by B.

And !!! vue.config.js:

module.exports = {
    filenameHashing: false,
    pages: {
        componentA: {
            entry: 'src/componentA.js',
            template: 'public/index.html',
            filename: 'indexA.html',
            title: 'Component A',
            chunks: ['chunk-vendors', 'chunk-common', 'componentA']
        },
        componentB: {
            entry: 'src/componentB.js',
            template: 'public/index.html',
            filename: 'indexB.html',
            title: 'Component B',
            chunks: ['chunk-vendors', 'chunk-common', 'componentB']
        },
    },
}

see vuejs cli -> pages

After the build we get: image

Note that indexA.html and indexB.html are based on the index.html from the public folder - the *.js includes will be set correctly, but the mounting point <div id=app></div> will not correspond the one given in the Component*.js file. At the time of writing this post I do not know where to set that. Anyway in the common case one would need the *.js components only and will attach them to an existing web page. It is also quite easy to correct manually the mounting point in index*.html if needed, but there should be an option to set it properly beforehand.

I also agree with all the posts above that building multiple Vue components for embedding in existing web applications (no matter SPA or other) is not easy or user friendly. The example above shows that it is possible without any further programming or changing the functionality in vue.js. There should only be some command of a kind: npm run buildComponentsForEmbeddingInExistingWebApplication <component1> ... <componentN> (I am sure there is a better name :)), which will generate single js files per component ready to be included in existing web pages.

Maybe this is because the "hardcore" vue.js developers never work outside vue.js and show the embedding option only in the documentation test examples - only with manual hard-coded includes, but they do not bother to improve the vue.js tools in order to streamline such tasks. It is only one command using ready functionality - please add it if you read this. Thanks in advance!

Best regards. HTH

tlyau62 commented 3 years ago

May we have a api that is similar to the multi-pages config, but target on library instead?

semiaddict commented 3 years ago

FYI, I was able to build multiple entries as a library using @vue/cli-service 5.0.0-rc.1 and webpack 5 with the following:

vue.config.js

module.exports = {
    chainWebpack: (config) => {
        config.optimization.splitChunks(false);
        config.output
            .filename("MyLibrary.[name].js")
            .library({
                name: "MyLibrary",
                type: "assign-properties",
            });

        config.entryPoints.delete("app");
        config.entry("EntryOne").add("./src/entry-one/main.js").end();
        config.entry("EntryTwo").add("./src/entry-two/main.js").end();
    }
}

npm script:

"build": "vue-cli-service build",

This results in the following outout files:

However, tree-shaking seems to not work anymore.

semiaddict commented 2 years ago

Here's a first attempt at getting multiple entries to work in library builds: https://github.com/semiaddict/vue-cli/tree/multi-entry-lib
And here's an example usage with vue.config.js:

module.exports = {
    chainWebpack: (config) => {
        config.output.library({
          name: "MyLibrary",
          type: "assign-properties",
        });

        config.entryPoints.clear();
        config.entry("MyLibrary.EntryOne").add("./src/my-polyfill.js").add("./src/entry-one/main.js").end();
        config.entry("MyLibrary.EntryTwo").add("./src/entry-two/main.js").end();
    }
}

If an entry is provided in the cli command, it overrides the one(s) specified in vue.config.js. If an entry has multiple files (see EntryOneabove), the last file is considered the main entry file, and is altered by resolveLibConfig to conform to Vue configs, other files are treated as regular entry files.

I believe this doesn't introduce any breaking changes.

peynman commented 2 years ago

Here's a first attempt at getting multiple entries to work in library builds: https://github.com/semiaddict/vue-cli/tree/multi-entry-lib And here's an example usage with vue.config.js:

module.exports = {
    chainWebpack: (config) => {
        config.output.library({
          name: "MyLibrary",
          type: "assign-properties",
        });

        config.entryPoints.clear();
        config.entry("MyLibrary.EntryOne").add("./src/my-polyfill.js").add("./src/entry-one/main.js").end();
        config.entry("MyLibrary.EntryTwo").add("./src/entry-two/main.js").end();
    }
}

If an entry is provided in the cli command, it overrides the one(s) specified in vue.config.js. If an entry has multiple files (see EntryOneabove), the last file is considered the main entry file, and is altered by resolveLibConfig to conform to Vue configs, other files are treated as regular entry files.

I believe this doesn't introduce any breaking changes.

when is this gonna be merged, how we can use it right now :)

Swivelgames commented 2 years ago

I think a lot of this is superseded by ViteJS for Vue 3 projects.

As for Webpack projects, I believe the recommended route might actually be to use SplitChunks? I'm using this right now in order to split out vendor code:

vue.config.js

module.exports = {
    lintOnSave: false,
    configureWebpack: {
        optimization: {
            splitChunks: {
                cacheGroups: {
                    vendor: {
                        test: /[\\/]node_modules[\\/]/,
                        name: 'vendor',
                        chunks: 'all',
                    }
                }
            }
        }
    }
};

You could, theoretically, use this to generate chunks for each of your components. Especially if you're specifying them manually. But you could also do so automagically with something like this (though, you'll probably have to finagle it to work for your own projects):

vue.config.js

const requireContext = require('require-context');

const componentsPath = `./src/components`;
const includeSubDirs = false; // or true?
const components = requireContext(componentsPath, includeSubDirs, /\.vue$/);

const cacheGroups = components.map((filename) => ({
    test: `${componentsPath}/${componentsPath}`,
    name: filename.split('.').slice(0,-1).join('.'),
    chunks: 'all'
}));

module.exports = {
    lintOnSave: false,
    configureWebpack: {
        optimization: {
            splitChunks: { cacheGroups }
        }
    }
};

(of course, using path.join, and fs.realPathSync.native and all that jazz instead in order to make it cross-platform compatible with Windows and whatnot)

semiaddict commented 2 years ago

@Swivelgames, splitting chunks won't help here, as it doesn't create separate entries, it only allows to split an entry into smaller chunks, or create shared chunks. So I don't believe it solves the issue at hand.

The idea of multiple entries is to be able to share the same code base to export different libraries. In my use case, I'm exporting a player and an editor which share a large common code base.

I have been using the code in the PR https://github.com/vuejs/vue-cli/pull/6884 for a while, and works just fine in such a use case.

semiaddict commented 2 years ago

@Swivelgames, can you please indicate how ViteJs can help with this? I can't find anything in their docs about multiple entries in library mode. There seems to be a similar issue that was filed there and closed, but no sign of an actual resolution: https://github.com/vitejs/vite/issues/1043

Swivelgames commented 2 years ago

@semiaddict I understand the difference between Entries and Chunking. The OP was specifically looking for entries per Component. A viable solution for them might be to have a primary library entry, with each component as a shared chunk.

That being said, it doesn't resolve the issue of multiple entries, where are each entry contains all necessary dependencies (read: duplicating the dependency graph in some ways).

In the case of having an Editor and a Player may be as simple as having two entry files.

In my case, we have a entry.lib.ts and an entry.wc.ts β€” Each of which are the entry files, and vue-cli is executed on each with a different target name.

The way webpack works with multiple entries is similar to simply running Webpack twice.

As for Vite, I simply reference it because, while moving to Vue 3 is an undertaking, that is the future. ViteJS will be favored in terms of focus for adding features and functionality going forward. While Vue 2 (and thus vue-cli) don't magically disappear, the community will begin to transition over to ViteJS. For posterity's sake, it may make sense to start focusing on solutions that keep the future of this and your own project in mind :)

It's not what everyone wants to hear, but is something I personally know I have to come to terms with. So, just stating that point, is all.

semiaddict commented 2 years ago

Thank you @Swivelgames. However running separate builds is not the same as having multiple entries, as then you can't take advantage of shared chunks, and multiply the use of resources when in development. Vite looks really interesting, but I believe Vue CLI still has its place in the Vue 3 ecosystem. Anyway, this should likely be discussed elsewhere, as we are moving away from the issue's subject.