config-vp
v1.2.8
Published
Shared vite-plus configuration — opinionated defaults for linting, formatting, task running, staged checks, and VSCode setup. Optional Vue and pack layers with deep-merge overrides.
Maintainers
Readme
config-vp
One shared vite-plus config for all your packages. Pick a project type and get linting, formatting, testing, building, and ready-to-run tasks — with zero boilerplate per package.
What you get
- Linting — a strict oxlint ruleset (TypeScript, unicorn, import, jsdoc, promise) with stylistic and import-sorting rules.
- Formatting — an oxfmt profile (single quotes, semicolons, trailing commas, Tailwind class sorting, import sorting).
- Tasks —
check,test,build,dev,release, and more, tailored to your project type and runnable withvpr <task>(short forvp run). - Pre-commit checks — staged files are checked and auto-fixed.
- VSCode setup — a one-command task to wire up the oxc extension at your workspace root.
Everything is opinionated and works out of the box. You can still customize any piece (see Customizing).
Prerequisites
config-vp is driven by vite-plus — the vp CLI that runs your whole dev lifecycle (install, dev server, lint, format, test, build).
Already have vp? Jump to Quick start.
Don't have it yet? Install it once, globally:
Linux / macOS
curl -fsSL https://vite.plus | bashWindows
irm https://viteplus.dev/install.ps1 | iexThen check it's available:
vp --versionQuick start
1. Add the config to your package:
vp i -D config-vp@^1.2.8This also pulls in everything the config needs (oxlint, oxfmt, and the lint plugins). If a compatible vite-plus is already in your workspace, that one is reused.
2. Create vite.config.ts:
Important: Assign to a
constand export it — don't writeexport default defineVitePlusConfig(...)inline.vpreads your default export statically to discover tasks; a direct call hides them andvprreports "Task not found". See Troubleshooting.
Expand the pattern that matches your package (every pattern uses the fully-typed config hook; see Customizing):
import { defineVitePlusConfig } from 'config-vp';
const config = defineVitePlusConfig({
type: 'lib',
config: c => {
c.pack = {
...c.pack,
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
};
},
});
export default config;import { defineVitePlusConfig } from 'config-vp';
const config = defineVitePlusConfig({
type: 'lib:vue',
config: c => {
c.pack = {
...c.pack,
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
};
},
});
export default config;import vue from '@vitejs/plugin-vue';
import { defineVitePlusConfig } from 'config-vp';
import { fileURLToPath, URL } from 'node:url';
import vueDevTools from 'vite-plugin-vue-devtools';
const config = defineVitePlusConfig({
type: 'vue',
config: c => ({
...c,
// loosen a few rules for app code — see Customizing → Disabling or tweaking lint rules
lint: {
...c.lint,
rules: {
...c.lint.rules,
'typescript/consistent-type-definitions': 'off',
'typescript/no-unsafe-assignment': 'off',
'typescript/no-unsafe-return': 'off',
},
},
plugins: [vue(), vueDevTools()],
resolve: {
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
},
}),
});
export default config;Build the config in a small nuxt.config.vite.ts and wire it in:
// nuxt.config.vite.ts
import type { NuxtConfig } from 'nuxt/schema';
import { defineVitePlusConfig } from 'config-vp';
export const vite = defineVitePlusConfig({
type: 'nuxt:spa', // or 'nuxt:ssr'
config: c => {
// any Vite/vite-plus options the app needs, e.g. plugins, define, test, …
},
}) as NuxtConfig['vite'];// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt/config';
import { vite } from './nuxt.config.vite';
export default defineNuxtConfig({ vite });The export const vite = … form is itself a const export, so vp discovers any tasks you add in the config hook.
For a config or scripts-only package, call defineVitePlusConfig() with no type:
import { defineVitePlusConfig } from 'config-vp';
const config = defineVitePlusConfig();
export default config;Already have a
vite.config.ts? If you scaffolded withcreate-vue(or any other generator), the file ships withtest,build, and tooling settings baked in. config-vp owns linting, formatting, testing, and building — so it's safe to delete all of that and keep only what's genuinely app-specific: yourplugins,resolve/alias,define,server, and the like. The pattern you picked above is exactly what's left.
3. Wire up VSCode (once per workspace root — see VSCode setup):
vpr setup:vscode4. Run something (vpr is short for vp run):
vpr check # lint + format, with autofix
vpr test # run tests
vpr build # build (vp pack / vite build / nuxt build, per type)That's it.
Project types
Set type to match what you're building. It drives Vue lint rules, library packaging, and the build/dev/release tasks.
| type | For | build | dev |
| --------------- | ------------------------------------ | --------------- | ----------------- |
| lib | A TypeScript library | vp pack | vp pack --watch |
| lib:vue | A Vue component library | vp pack | vp pack --watch |
| lib:nuxt | A Nuxt-targeted library | vp pack | vp pack --watch |
| vue | A Vue (Vite) application | vite build | vite dev |
| nuxt:spa | A Nuxt app, statically generated | nuxt generate | nuxt dev |
| nuxt:ssr | A Nuxt app, server-side rendered | nuxt build | nuxt dev |
| no type field | Shared root / non-buildable packages | none | none |
The per-type config patterns live in Quick start — expand the one that matches your package.
Tasks
Run any task with vpr <task> — short for vp run <task>. Tasks are cached and run in dependency order across your workspace.
Always available:
| Task | Command | Purpose |
| --------------- | -------------------------------------- | -------------------------------- |
| check | vp check --fix | Lint + format, with autofix |
| test | vp test --run --passWithNoTests | Run the test suite once |
| test:watch | vp test --passWithNoTests | Run tests in watch mode |
| test:coverage | vp test --coverage --passWithNoTests | Run tests with a coverage report |
--passWithNoTestsmeans a package with no test files passes instead of failing — sotest/releasedon't break in packages that don't have tests yet.
Added by type:
| Task | When | Command |
| --- | --- | --- |
| build | any type | see Project types |
| dev | any type | see Project types |
| release | lib* types | vp check --fix && vp test --run --passWithNoTests && vp pack && vpx bumpp && pnpm publish |
All built-in tasks are emitted as objects, so the
confighook can tweak one in place — e.g.c.run.tasks.release.command = '…'.
Workspace root only:
| Task | Command |
| -------------- | ----------------- |
| setup:vscode | vp-setup-vscode |
Run it once per workspace root:
vpr setup:vscodesetup:vscode is added automatically to workspace roots (any package with a pnpm-workspace.yaml or a workspaces field, plus standalone projects). Nested sub-workspaces each get their own. It opens an interactive checklist — everything is on by default; toggle with space, confirm with enter — and applies what you keep:
- oxc editor settings — format-on-save and fix-all in
.vscode/settings.json. - vite-plus extension — adds the
VoidZero.vite-plus-extension-packrecommendation to.vscode/extensions.jsonand installs it via thecodeCLI when available. - Blue git decorations — tints modified files so they don't read as warnings.
- Hide warning highlights — a transparent
editorWarningthat hides the auto-fixable lint warnings.
Comments and settings you've already customized are preserved. With no terminal attached (e.g. CI) it applies everything non-interactively.
For a whole-workspace lint/format, use the built-in
vp check. To build every package, usevpr -r build.
Overriding a task with a package.json script
Your package.json scripts win. If a package defines a script with the same name as a generated task, config-vp drops its own task and your script takes over — no clash, no config needed:
// package.json
{
"scripts": {
"build": "tsx scripts/build.ts", // replaces the generated `build` task
},
}vpr build # runs your script, not `vp pack`This is the simplest way to special-case one task while keeping every other default. (Without it, vp errors with "Task build conflicts with a package.json script of the same name".) For richer changes — tweaking a task's command, cache behavior, or adding new tasks — use the config hook instead; reach for a script only when you want to shadow a task outright.
Customizing
There's one customization hook: config. It receives the fully built config — lint, fmt, staged, run tasks, and (for lib*) pack, all already populated — and you change whatever you want with plain JS. Mutate it in place, or return a new object (a returned value wins; otherwise the mutated argument is used). It's fully typed, so autocomplete shows you exactly what's there.
const config = defineVitePlusConfig({
type: 'lib',
config: c => {
c.pack = {
...c.pack,
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
}; // tweak packaging
c.lint.rules['no-console'] = 'error'; // add a lint rule (base rules stay)
c.run.tasks.release.command = 'my-release'; // change a built-in task
},
});
export default config;Because you hold the real config, there are no merge semantics to learn — you decide what to keep ({ ...c.pack, … }) and what to replace (c.pack = { … }). To drop something, delete it: delete c.pack (skip packaging), delete c.run.tasks.release.
Mutate or return
The hook works two ways — use whichever reads better:
// Mutate in place and return nothing — best for a few targeted tweaks:
defineVitePlusConfig({
type: 'lib',
config: c => {
c.pack = {
...c.pack,
entry: ['src/index.ts'],
};
},
});
// Return a new object — best when you want to build the result explicitly:
defineVitePlusConfig({
type: 'lib',
config: c => ({
...c,
pack: {
...c.pack,
entry: ['src/index.ts'],
},
}),
});A returned value wins; if you return nothing, the mutated argument is used. Don't do both.
Disabling or tweaking lint rules
Every rule lives in c.lint.rules. Set one to 'off' to disable it, or reassign it to change its severity/options — spread ...c.lint.rules first so the rest of the ruleset stays intact:
const config = defineVitePlusConfig({
type: 'vue',
config: c => ({
...c,
lint: {
...c.lint,
rules: {
...c.lint.rules,
'typescript/no-unsafe-assignment': 'off',
'typescript/consistent-type-definitions': 'off',
'no-console': 'warn',
},
},
}),
});
export default config;The rule name is exactly what the editor (or vpr check) prints in the diagnostic — e.g. oxc(typescript/no-unsafe-assignment) → 'typescript/no-unsafe-assignment'. Mutating in place works too: c.lint.rules['typescript/no-unsafe-assignment'] = 'off'.
Your existing Vite config goes here
Anything in a normal Vite / vite-plus config — plugins, resolve, define, server, optimizeDeps, worker, test, build, extra run.tasks, … — is just a field you set in the hook. Nothing config-vp-specific to learn; keep writing Vite config.
const config = defineVitePlusConfig({
type: 'nuxt:spa',
config: c => {
c.plugins = [...tailwindcss()];
c.define = {
'import.meta.env.VITE_RELEASE': JSON.stringify(release),
};
c.optimizeDeps = { exclude: ['some-wasm-dep'] };
c.server = {
fs: {
allow: ['../..'],
},
};
c.test = { exclude: ['e2e/**'] };
c.run.tasks.preview = {
command: 'nuxt preview',
cache: false,
};
c.run.tasks.deploy = { command: 'vpx tsx scripts/deploy.ts' };
},
});
export default config;Ignore patterns (lint + format)
ignorePatterns sets the ignore globs for both the linter and the formatter. Pass an array to set the whole list, or a function to derive it from the built-in defaults:
defineVitePlusConfig({
type: 'lib',
// add to the defaults:
ignorePatterns: defaults => [...defaults, 'generated/**', 'vendor/**'],
});
defineVitePlusConfig({
type: 'lib',
// …or set the list outright:
ignorePatterns: ['only-this/**'],
});Disable packaging
A lib* type includes packaging by default. To skip it, delete pack in the hook:
defineVitePlusConfig({
type: 'lib',
config: c => {
delete c.pack;
},
});Options
| Option | Type | Description |
| --- | --- | --- |
| type | ProjectType | 'lib' \| 'lib:vue' \| 'lib:nuxt' \| 'vue' \| 'nuxt:spa' \| 'nuxt:ssr'. Omit for shared/root packages. |
| ignorePatterns | string[] \| (defaults: string[]) => string[] | Ignore globs for lint and fmt. Array sets the list; function derives it from the defaults. |
| config | (config) => config \| void | Customization hook — receives the fully built config to mutate in place and/or return. |
Default ignore patterns
The base list applied to both linting and formatting (override or extend it via ignorePatterns):
*.log* **/.output **/.vp-tsconfig
**/.nuxt-storybook **/node_modules **/.vp-tmp-vite
**/.nuxt **/dist .env
**/.nitro **/.vue-types package-lock.json
**/.cache pnpm-lock.yamlAPI
The package exports a single function, defineVitePlusConfig, plus the types ConfigOptions (its argument), ProjectType, and ResolvedConfig (the value passed to the config hook).
Troubleshooting
vpr <task> says "Task not found" (but vp pack works)
Your config almost certainly uses an inline default export. vp discovers tasks by statically reading the default export, and a direct call expression hides them. Use the const form:
// ❌ tasks are invisible to `vpr`
export default defineVitePlusConfig({ type: 'lib' });
// ✅ assign, then export
const config = defineVitePlusConfig({ type: 'lib' });
export default config;Tasks show up duplicated or ambiguous across the workspace
Every workspace package needs a unique name in its package.json. An unnamed package (commonly the repo root) registers its tasks without a namespace, colliding with others. Give the root a name, e.g. "name": "my-workspace-root".
vp crashes with Cannot convert undefined or null to object
The globally-installed vp and your workspace's vite-plus are different versions. Match the workspace vite-plus (in package.json and any pnpm.overrides) to vp --version, then run vp install.
License
MIT
