Skip to content

Why does it... ?

You might have a lot of questions about why the generated project is structured the way it is. This page aims to answer some of those questions.

To learn more about the built files in the dist directory see Files in dist.

Influences

Much of the configuration is based on create-vue. Sticking close to that tool makes it easier for everyone involved.

But applications and libraries need different things.

Libraries such as Vue core, Vue Router and Pinia have also heavily influenced the project structure used by this tool, especially the use of a packages directory and pnpm workspaces. Various other tools, such as simple-git-hooks, lint-staged and VitePress, have been chosen to align with those projects.

Those projects use rollup directly for their builds, rather than Vite. Vite already uses rollup behind the scenes, but using it directly is more flexible. Using Vite as a wrapper has a few advantages:

  • Vite is familiar to most members of the Vue community.
  • Using Vite keeps us closer to create-vue.
  • Vite has its own ecosystem of useful plugins.

In particular, the libraries mentioned above don't use .vue files in their source code. Compiling .vue files with rollup is certainly possible, but it's more convenient to reuse the same toolchain used to build Vue applications.

Multiple packages

The project is split into multiple packages. By default, these are in a directory called packages, plus a root package. Each package has its own package.json file.

These packages form a pnpm workspace, configured via pnpm-workspace.yaml. The workspace allows certain actions to be applied to all packages in one go, for example, running pnpm install at the root will install dependencies for all the packages.

Splitting the package.json in this way helps to keep things modular. It also helps to reduce the amount of noise in the package.json that gets published to the npm registry.

The project's root package.json uses preinstall and postinstall hooks. It's important that these are not in the package.json that's published to the npm registry, otherwise they would run when someone tries to install your package.

TypeScript configuration

The TypeScript configuration is mostly done of a per-package basis. The configuration files are closely based on the files created by create-vue.

A root-level tsconfig.json is only created if ESLint is included. That is just used to check eslint.config.ts, which is at the root of the project. There currently aren't any other TS files that live outside the sub-packages.

Where possible we use vue-tsc --build to perform the type checks, the same as create-vue, but for the docs package that doesn't currently seem to be possible. We have "vitePressExtensions": [".md"] set in vueCompilerOptions, which allows .md files to be processed a bit like .vue files, allowing any Vue syntax to be type-checked. That setting triggers an error if we use vue-tsc --build, so we check the code and config files separately instead.

ESLint configuration

The ESLint configuration is very closely based on create-vue.

While it is theoretically possible to have separate ESLint configurations for each package, that's unlikely to be useful in practice, so the configuration is at the root level of the project.

ESLint Stylistic can be used as a formatter. See https://eslint.style/guide/why for an explanation of the benefits of that package over dedicated formatters such as Prettier. If you want to use Prettier instead then you'll need to configure it yourself, at least for now.

simple-git-hooks and lint-staged

simple-git-hooks is widely used in the Vue and Vite ecosystems to run tasks when files are committed to git. This helps to catch mistakes early.

Currently, we support using simple-git-hooks for running the type checks and running ESLint, via lint-staged. Using lint-staged ensures that only staged files are checked (i.e. those that are going to be included in the commit). By default, the project runs ESLint with the --fix flag, to automatically fix any problems, but you can remove that flag if you're concerned about code being unexpectedly mangled by the formatter.

Many projects also use simple-git-hooks to impose restrictions on the commit message. This isn't currently something configured by the tool.

We use a postinstall target in scripts to update the git hooks, ensuring they stay up to date for anyone developing the package. The documentation for simple-git-hooks warns against doing that, but it's important to note that the postinstall is in the project's root package.json, not the package.json that's published to the npm registry. It's only a problem if it's published to the registry.

.gitignore

The .gitignore is similar to create-vue.

We use a single .gitignore, rather than having one in each package. This leads to some entries that target specific parts of individual packages, which might more naturally be configured using a separate .gitignore within that specific package.

A key reason for using a single file is that we can easily integrate it into other tools, such as ESLint.

Vite configuration

The separate page Files in dist explains the reasoning behind some of the most important parts of vite.config.mts.

The Vue plugin is configured with componentIdGenerator: 'filepath'. This controls how the ids are generated for <style scoped>. The default strategy hashes the file contents, but only after other parts of the build have processed the file, leading to inconsistent hashes between development and production builds. Using the filepath instead helps to keep the ids stable across all the files in dist, so that any combination of JS and CSS files should work together.

vite-plugin-dts is used to generate the .d.ts file. By default, this generates a separate .d.ts file for each input file, but rollupTypes: true merges them into a single file. Despite the naming similarity, that setting is unrelated to the build tool rollup.

As we can't build all the output files we need in one go, we instead run Vite three times. emptyOutDir is set to false, so that we don't lose the build artifacts from the other stages of the build. rimraf is used to clear the dist directory at the start of the build process.

In rollupOptions we configure external: ['vue']. This tells rollup to keep any imports from the vue package as imports, rather than pulling all the code into the built library. For output formats that don't support import it will be rewritten accordingly, e.g. using require() for CommonJS. For global (IIFE) builds, there is the extra setting globals: { vue: 'Vue' }, which tells rollup to rewrite imports like import { ref } from 'vue' as const { ref } = Vue, or code that's equivalent.

__DEV__ and __TEST__

The project supports 'global variables' for __DEV__ and __TEST__. The __TEST__ variable isn't included by default and requires the --extended flag to opt in.

While they are configured as global variables from a TypeScript perspective, and will be interpreted that way by tools such as IDEs and ESlint, they are actually statically replaced by Vite during the build. They also need to be configured in the docs and playground packages, as those packages use the library's source code directly.

If you need to add other variables like these, e.g. __BROWSER__, __CI__, __SSR__, you'll need to configure those yourself, using __DEV__ and __TEST__ as a starting point. In some cases you may need to make other changes to the build to generate extra files in dist, so consumers of your package can use the alternative builds.

For __TEST__ we replace the value with a simple true or false, which can be configured using Vite's define option.

For __DEV__ it's a little more complicated, because in some builds we replace it with !(process.env.NODE_ENV === "production"). This allows the downstream bundler to make the decision about what mode we're in. We can't use define for a complex value like this, so @rollup/plugin-replace is used instead.

jiti

The package jiti is included so that ESLint can use a TypeScript configuration file: