Files in dist
When you build your new package, you'll find various files dist. Here we'll try to explain what they are and why you need them.
Each file targets a different use case. You'll find several of the files referenced in package.json, which tells other tools which file to use. If you're familiar with those fields in package.json then you should be able to get some sense of what those files are for.
A project will look something like this, using the name my-lib as an example:
📁 packages
📁 my-lib
📁 dist
📁 src
📄 package.json
📄 vite.config.mtsInside dist you should see files like these:
📁 dist
📄 my-lib.cjs
📄 my-lib.css
📄 my-lib.d.ts
📄 my-lib.esm.dev.mjs
📄 my-lib.esm-browser.prod.js
📄 my-lib.esm-bundler.prod.mjs
📄 my-lib.global.dev.js
📄 my-lib.global.prod.js
📄 my-lib.prod.cssWe'll go into detail about these files later, but in brief:
- Files with a
.cssending are CSS. - The file ending
.d.tscontains the TypeScript types. - The other files (with
.cjs,.jsand.mjsendings) are JavaScript files. - Files with
devin their names are intended to be used during development. - Files with
prodin their names are intended to be used in production. esmindicates that a file is an ES module (i.e. it supportsimport/export)..cjsindicates a CommonJS file (i.e. it supportsrequire()).globalindicates that a file exposes the library via a global variable.
Depending on the specifics of your project, it's possible that you don't need all of these files, or maybe you need more alternatives for other use cases. Either way, you can change which files are built by modifying vite.config.mts and package.json.
Building .vue files
Libraries that use .vue files face specific challenges. The default project configuration assumes that you do want to use .vue files in your library source code.
There are two ways to handle .vue files in libraries:
- Include the raw
.vuefiles in the built library. - Compile the
.vuefiles down to.jsfiles.
The key difference is when the files are compiled. The first approach forces the consuming application to compile them, whereas the second approach performs the compilation as part of the library's build.
There are pros and cons with either approach, but the project scaffolded with this tool uses the second approach.
Development vs production
For applications, the distinction between development and production is usually clear.
For libraries, there are two build processes to consider: building the library itself and building the application. The consuming application might also not have a build process, e.g. for sites using libraries from a CDN.
Rather than a clear split between development and production, we instead have three stages: developing the library, developing the consuming application, and finally production. The first stage, developing the library itself, doesn't impact what we publish to npm, so we can ignore that here.
The production stage is also relatively straightforward.
The stage that poses the most challenges is developing the consuming application. We need to build our library in such a way that it'll work well during that stage.
When building .vue files, the SFC compiler needs to know whether to generate a development or production build. There are several differences between development and production builds, but for now we'll just focus on three of them:
- A development build is fully compatible with Vue Devtools, whereas a production build only has limited Devtools support. Externally available properties, like
$propsand$attrs, will be shown in the Devtools in either build, but component state inside<script setup>will only be exposed in development builds. - Type-based prop definitions are compiled to a helpful runtime equivalent in a development build, whereas a production build will aim for minimal code.
- A development build includes the full (absolute) file path of each
.vuefile in the built code, under a property called__file.
You can see the first two differences for yourself in the Vue Playground at https://play.vuejs.org/. The compiled component is visible in the JS tab, and you can toggle between DEV and PROD using the button at the top of the page. The difference in the __file property isn't apparent without the full Vite build-chain.
Having two different compiled forms means we need to commit to which one we want when we build the library.
Including the full path in __file is a headache. The build output of a library should be consistent between users, so it shouldn't depend on where the project is stored on the local filesystem. The full path can also potentially include sensitive information, such as usernames or company names, risking doxxing the library's author.
Ideally we'd want a third mode here, aimed at building development builds of libraries. Devtools support and type-based prop compilation should work like a development build, but features like the __file property should behave like production, as they aren't relevant to developing the downstream application.
That doesn't exist, but we can get close by using a production build with prodDevtools: true. That enables Devtools support, but sadly we won't get the improved runtime props definition.
To accommodate this, we need to generate dev and prod builds, even when the downstream application is using a bundler. The dev builds still use production mode, but with prodDevtools: true. The package.json then tells the downstream bundler which one to use in each case.
SSR
The SFC compiler can also generate special SSR builds for .vue files, allowing them to skip VNodes on the server and just generate HTML strings directly. You can see this in the Vue Playground at https://play.vuejs.org/, in the SSR tab.
Currently, the project created using this scaffolding tool does not generate special SSR builds. It should still work with SSR, but it'll use the slower VNode-based approach.
The files
<name>.cjs
This is a CommonJS build. It is intended to be used in Node applications that use require().
Some features of this build:
- The file is not minified, as it isn't used in the browser.
- The global
__DEV__flag will depend on the runtime value ofprocess.env.NODE_ENV. This is only relevant if you're using it in your library code. - SFCs will be compiled in production mode, without
prodDevtools: true.
There is an argument for having separate dev and prod builds, but the generated project won't currently be configured that way.
<name>.css and <name>.prod.css
These are the built CSS files for the project. The <style> sections of any components will end up in these files.
The prod file is minified, but they should otherwise be the same. If the consuming application is using a bundler then it should minify the CSS itself, so the minified build is only really relevant to applications that don't have a build step.
There currently isn't any support in the generated project for splitting the CSS file into smaller chunks.
Consuming applications will need to include the CSS file manually. With a bundler that would usually mean importing it, without a bundler it'd usually be a <link rel="stylesheet"> in the HTML file.
<name>.d.ts
This file contains the TypeScript types. This should include type information for anything exported by the package, including any components.
Tooling in the consuming application should pick this up automatically, as it's referenced from package.json.
<name>.esm.dev.mjs
This file exposes the library as an ES module. It is intended to be used during development, either with a bundler or directly in the browser via <script type="module"> and import maps (see the section about <name>.esm-browser.prod.js for more information about direct browser usage).
Some features of this build:
- The file is not minified, to make debugging easier.
- The global
__DEV__flag will be set totrue. This is only relevant if you're using it in your library code. - SFCs will be compiled in production mode, but with
prodDevtools: true.
In production, you would use either <name>.esm-browser.prod.js or <name>.esm-bundler.prod.mjs instead.
This build is suitable for use in the Vue Playground:
If your library needs to differentiate between bundlers and direct browser usage during development then you may need to adjust the build to generate more files.
<name>.esm-browser.prod.js
This file is an ES module build intended to be used directly in the browser, not via a bundler. For example:
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js",
"@skirtle/example-vue-lib": "https://unpkg.com/@skirtle/example-vue-lib/dist/example-vue-lib.esm-browser.prod.js"
}
}
</script>
<script type="module">
import { ExampleComponent } from '@skirtle/example-vue-lib'
// ...
</script>It's a production build, so in real code the versions should be pinned.
An import map is required. While browsers do support importing directly from a URL, that wouldn't be able to resolve imports from vue inside the library. So, for example, this wouldn't work:
<script type="module">
// This won't work.
// It won't be able to resolve 'vue' inside the imported file.
import { ExampleComponent } from 'https://unpkg.com/@skirtle/example-vue-lib/dist/example-vue-lib.esm-browser.prod.js'
// ...
</script>This build is suitable for use in the Vue Playground:
In particular, note the configuration in the Import Map tab.
Some features of this build:
- The file is minified, reducing its size. Note that Vite doesn't fully minify
esmbuilds for libraries. - The global
__DEV__flag will be set tofalseand dead code removed. This is only relevant if you're using it in your code. - SFCs will be compiled in production mode, without
prodDevtools: true.
During development, you'd normally use <name>.esm.dev.mjs instead.
<name>.esm-bundler.prod.mjs
This is a production ES module build, intended to be used by a bundler.
Some features of this build:
- The file is not minified. The bundler will be expected to handle that.
- The global
__DEV__variable will depend on the bundler's value forprocess.env.NODE_ENV. - SFCs will be compiled in production mode, without
prodDevtools: true.
From a bundler's perspective, the only significant difference between this build and the <name>.esm.dev.mjs build is that .vue files are built without prodDevtools: true. If you aren't using .vue files in your library code then this file can be used in both development and production. That would be similar to libraries like Vue core, Vue Router and Pinia, which just have an esm-bundler build, with no distinction between dev and prod. In that scenario, <name>.esm.dev.mjs is only used in the browser, so it could be renamed to something like <name>.esm-browser.dev.js.
<name>.global.dev.js and <name>.global.prod.js
Global builds can be used without build tools, just by including a <script> tag in the HTML page. They are built using the IIFE format.
The library is exposed using a global variable. For example, the library @skirtle/example-vue-lib can be used something like this:
<body>
<div id="app">
<example-component></example-component>
</div>
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/@skirtle/example-vue-lib/dist/example-vue-lib.global.dev.js"></script>
<script>
Vue.createApp({
components: {
ExampleComponent: ExampleVueLib.ExampleComponent
}
}).mount('#app')
</script>
</body>You can see a complete, running example in this JSFiddle.
The global build of Vue creates a global variable called Vue, exposing createApp. The global build of @skirtle/example-vue-lib creates a global variable called ExampleVueLib, allowing us to access ExampleComponent.
In production applications, the prod builds of both vue and @skirtle/example-vue-lib should be used instead, and exact versions should be pinned in the URL.
The differences between the dev and prod builds are:
prodis minified,devisn't.devsets__DEV__totrue,prodsets it tofalse, with any dead code removed.devbuilds useprodDevtools: true.
build:dev, build:neutral, build:prod
The default build for the generated project is split into 3 targets in scripts, to accommodate the files we need to generate. If you decide to add or remove more built files then you may also need to add or remove build targets from package.json, as well as adjusting vite.config.mts.
A single Vite build can produce multiple files, but those files must share most of their build options and only one file can be created for each format.
There's nothing special about having 3 builds, that's just how many we need to create the combinations we need. We generate 3 esm files in dist, so we need at least 3 builds to achieve that.
Roughly speaking:
build:dev- unminified development builds that can be served to the browser without further build tools:<name>.esm.dev.mjsand<name>.global.dev.js.build:neutral- unminified builds that won't go directly to the browser:<name>.cjsand<name>.esm-bundler.prod.js.build:prod- minified production builds to be served directly to the browser:<name>.esm.prod.jsand<name>.global.prod.js
The TypeScript types only need to be generated once, so they get bolted onto build:neutral.
Each build will also generate a CSS file, but two of them will be the same and will be given the same name.