What does Vite+ unify?

The starting point is a small job: a browser tool that checks a JSON-LD object. As such a tool grows, Vite, TypeScript, ESLint, Prettier and Vitest live side by side with separate dependencies and separate configuration files. This article shows how Vite+ can pull that sprawl together, using a single application: a new project, the migration of an existing one, tests and CI.

Vite+ 1.0.0 shipped on 28 September 2026, and that launch is this article's starting point. But I ran the walkthrough's recorded Vite+ commands with 1.1.0: when I checked on 10 October 2026, GitHub's latest release was 1.1.0, published on 7 October 2026, and the latest tag on npm pointed to it as well. So 1.0.0 is a milestone, not the current version; the post-migration versions, outputs and configurations belong to 1.1.0.

What is Vite+? A unified command line (vp) and a shared configuration layer on top of existing tools. “Unify” does not mean everything is compiled into a single executable: Vite, Rolldown, Vitest, Oxlint, Oxfmt, tsdown and Vite Task are provided by the integrated toolchain. Plain Vite offers a development server and a production build (its production bundler is Rolldown); Vite+ adds linting, formatting, testing and task running to the same workflow. Vite+ is MIT-licensed; the commercial licence plan in the original announcement is outdated. You need to validate your existing Vite plugins and framework assumptions separately in your own project.

The diagram at the top of the page compares the example project's separate configuration surface (eslint.config.js, .prettierrc.json, vite.config.ts) with a shared entry point. It is a qualitative drawing, not a measurement. Nor does it mean every configuration file disappears: the manifest, the lockfile and tsconfig.json stay.

The article keeps the two routes apart. If you are starting a new project, follow the fresh-demo route (section 3). If you have an existing project, follow the jsonld-demo route: we validate the classic setup (section 5), then migrate it with vp migrate (section 6). You do not need to undo a new project to learn the migration; the routes are independent and carry the same small application.

Pin the version and the environment

Vite+ comes in two forms. The local CLI, installed in the project as the vite-plus dependency, pins the version with the project and relies on an existing Node runtime. The optional global vp can also manage runtimes and package managers. For the installation commands, see the Vite+ getting-started page: a shell script and a Windows PowerShell installer are documented. I did not run those scripts: I downloaded the official Linux archive, compared its SHA-256 digest with the published one and ran it with all Vite+ directories redirected to a temporary folder. That tests the archive, not the installer scripts, and I did not perform a signature or attestation verification either.

The Node requirement is exactly the range ^22.18.0 || ^24.11.0 || >=26.0.0; do not shorten it to “Node 20+” or just “Node 22+”. The example project was tried with Node 24.11.0 and npm 11.6.1, and I pinned that in two places: 24.11.0 in the .node-version file and [email protected] in the packageManager field of package.json. Your package manager may have a minimum of its own (section 3 has a real example): do not blindly pair the newest npm with the oldest supported Node.

vp --version tells you the active versions; vp toolchain gives the detail and, with the --global flag, shows the global installation. This was the record in the example project:

Output of vp --version: the local vite-plus 1.1.0, the tool versions and the environment.text
01$ vp --version02vp v1.1.003 04Local vite-plus:05  vite-plus  v1.1.006 07Tools:08  vite             v8.3.309  rolldown         v1.2.1210  vitest           v5.0.311  oxfmt            v0.72.012  oxlint           v1.87.013  oxlint-tsgolint  v7.0.200314  tsdown           v0.23.015 16Environment:17  Package manager  npm v11.6.118  Node.js          v24.11.0 (.node-version)

An important distinction: the Vite, Rolldown and Vitest inside Vite+ 1.1.0 need not match the latest separately released versions. The table shows the situation on 10 October 2026.

Versions provided by Vite+ 1.1.0 and the current separate releases
ToolProvided by Vite+ 1.1.0Current separate release
Vite8.3.38.3.4
Rolldown1.2.121.2.13
Vitest5.0.35.0.3
Oxlint1.87.0not checked
Oxfmt0.72.0not checked
oxlint-tsgolint7.0.2003not checked
tsdown0.23.0not checked
Vite Taskrevision 7d69d6577ecf6bd83deee32186de59918a712873not checked

No version number is exposed for Vite Task, so we do not invent one and give only the revision. “Not checked” means that tool's separate release was not compared.

The lockfile is part of the pinning too. vp install uses the package manager the project has chosen and does not replace it; here npm produces package-lock.json. In a real project, commit it so that CI installs the same dependency tree; the workflows in section 8 begin with vp install --frozen-lockfile for that reason.

A new project and a tiny UI

The new-project route starts with vp create. With the vite shorthand the template is handed to Vite's own generator, and the project folder's name is given after the -- marker. In my first attempt I used the --directory jsonld-demo form and the command exited with code 1: The --directory option is only available for builtin and bundled @org templates. That is why the tutorial uses only the form that succeeded.

I ran the command with flags that switch off interaction and the agent, editor, hooks and git options, choosing npm, in a separate sibling folder and not on top of the existing project. The output:

Output of vp create, exit code 0. The duration was measured on this machine, once.text
01$ vp create vite --package-manager npm --no-interactive --no-agent --no-editor --no-hooks --no-git -- fresh-demo --template vanilla-ts --no-interactive02●  npm@latest installing...03●  [email protected] installed04◇  Generating project…05●  Running: npx create-vite fresh-demo --template vanilla-ts --no-interactive 06   --no-immediate --no-rolldown07npm warn cli npm v12.2.0 does not support Node.js v24.11.0. This version of npm supports the following node versions: `^22.22.2 || ^24.15.0 || >=26.0.0`. You can find the latest version at https://nodejs.org/.08npm warn exec The following package was not found and will be installed: [email protected]09npm notice run npx10npm notice run 'create-vite' fresh-demo --template vanilla-ts --no-interactive --no-immediate --no-rolldown11│12◇  Scaffolding project in /tmp/viteplus-research-oyosbm_d/fresh-demo...13│14└  Done. Now run:15 16  cd fresh-demo17  npm install18  npm run dev19 20◆  ✔ Created vite.config.ts in fresh-demo/vite.config.ts21●  Installing dependencies...22●  Dependencies installed23●  Formatting code...24●  Code formatted25◇ Scaffolded fresh-demo with Vanilla + TypeScript26• Node 24.11.0  npm 12.2.027✓ Dependencies installed in 13s28→ Next: cd fresh-demo && vp run

Do not skip the warning in the output. The script installed npm@latest, that is, it selected npm 12.2.0, and it reported that this version does not support Node 24.11.0: its own requirement is ^22.22.2 || ^24.15.0 || >=26.0.0. The command still exited with code 0. I corrected it like this: I replaced the template's demo files with the application in this article, wrote packageManager: [email protected] into package.json and 24.11.0 into the .node-version file, deleted the generated demo sources and images, and reinstalled the dependencies. None of that happened automatically; I did all of it by hand and on purpose, and I did not repeat the create command after the correction. The alternative is to choose a newer Node that satisfies the requirements of both Vite+ and your npm version.

After the correction, fresh-demo and jsonld-demo carried the same set of files; only the package name differed. The tree below is jsonld-demo after migration. node_modules/ and dist/ are generated folders and are not committed.

jsonld-demo/: the file tree after migration.text
01jsonld-demo/02├── .gitignore03├── .node-version          # 24.11.004├── index.html             # labelled form and live text result05├── package.json           # toolchain pin, scripts, core alias06├── package-lock.json      # generated; retain and commit in a real project07├── tsconfig.json          # TypeScript source selection and strict options08├── vite.config.ts         # lint + fmt + test09└── src/10    ├── main.ts            # UI event wiring11    ├── validate.ts        # pure local-policy validator12    └── validate.test.ts   # 17 cases
Final file tree of the migrated jsonld-demo/ project: .gitignore, .node-version, index.html, package.json, package-lock.json, tsconfig.json, vite.config.ts, and src/ with main.ts, validate.ts and validate.test.ts. Each file's role is written beside it; node_modules/ and dist/ appear separately under “Generated / not committed”.
After migration there is no single settings file: the Node version, packages, vite.config.ts, tsconfig.json and source code each do their own job, and the 17 tests live in validate.test.ts.

tsconfig.json covers only the src folder, turns on strict mode and emits nothing (noEmit); the migration in section 6 did not change it.

tsconfig.json: the source selection and strict options.json
01{02  "compilerOptions": {03    "target": "ES2022",04    "module": "ESNext",05    "lib": ["ES2022", "DOM", "DOM.Iterable"],06    "moduleResolution": "Bundler",07    "noEmit": true,08    "strict": true,09    "skipLibCheck": true,10    "types": ["vite/client"]11  },12  "include": ["src"]13}

The UI is small on purpose: index.html contains a text field tied to a label, a button and a pre that announces the result; role="status" and aria-live="polite" mark the result to be announced as text.

index.html: the labelled form and the result announced as text.html
01<!doctype html>02<html lang="en">03  <head>04    <meta charset="UTF-8" />05    <meta name="viewport" content="width=device-width, initial-scale=1.0" />06    <title>JSON-LD local checks</title>07  </head>08  <body>09    <main>10      <h1>JSON-LD local checks</h1>11      <p>This demo checks a small Article policy, not rich-result eligibility.</p>12      <form id="validator">13        <label for="source">JSON-LD source</label><br />14        <textarea id="source" rows="9" cols="60" spellcheck="false">15{"@context":"https://schema.org","@type":"Article","headline":"Hello"}</textarea16        ><br />17        <button type="submit">Check local fields</button>18      </form>19      <pre id="result" role="status" aria-live="polite"></pre>20    </main>21    <script type="module" src="/src/main.ts"></script>22  </body>23</html>
src/main.ts: event wiring; the result is written with textContent.ts
01import { validateJsonLd } from "./validate";02 03const form = document.querySelector<HTMLFormElement>("#validator");04const source = document.querySelector<HTMLTextAreaElement>("#source");05const result = document.querySelector<HTMLPreElement>("#result");06 07if (!form || !source || !result) {08  throw new Error("Missing validator UI elements.");09}10 11form.addEventListener("submit", (event) => {12  event.preventDefault();13  result.textContent = JSON.stringify(validateJsonLd(source.value), null, 2);14});

main.ts finds three elements and throws if one is missing; on submit it prevents the default behaviour and writes the result with textContent. The text the user types is never parsed as HTML; keep that behaviour. The limit is plain: the core was verified with unit tests and the UI built successfully, but I did not run browser-interaction or accessibility tests.

The boundary of the JSON-LD core

The core that the UI calls is in src/validate.ts: a pure function that knows nothing about the DOM, the network or files; it takes a string and returns a result with three fields.

src/validate.ts: JSON syntax, the root object and the local Article policy.ts
01export type ValidationResult = {02  jsonValid: boolean;03  localValid: boolean;04  issues: string[];05};06 07export function validateJsonLd(source: string): ValidationResult {08  let value: unknown;09  try {10    value = JSON.parse(source);11  } catch {12    return { jsonValid: false, localValid: false, issues: ["Invalid JSON."] };13  }14 15  if (typeof value !== "object" || value === null || Array.isArray(value)) {16    return {17      jsonValid: true,18      localValid: false,19      issues: ["Expected one JSON object."],20    };21  }22 23  const data = value as Record<string, unknown>;24  const issues: string[] = [];25  if (data["@context"] !== "https://schema.org") {26    issues.push("@context must be https://schema.org.");27  }28  if (data["@type"] !== "Article") {29    issues.push("@type must be Article for this demo.");30  }31  if (typeof data.headline !== "string" || data.headline.trim() === "") {32    issues.push("headline must be a nonempty string.");33  }34  return { jsonValid: true, localValid: issues.length === 0, issues };35}

Keep three layers apart. The first is JSON syntax: if JSON.parse fails, both jsonValid and localValid are false. The second is that the root is a single object: null, an array, a number or a plain string is valid JSON (jsonValid: true) but does not pass this policy. The third is the local Article policy: @context must be exactly the string https://schema.org, @type must be exactly Article and headline must be a nonempty string. All three problems are added to the issues list in one go; the core does not stop at the first error.

We bind the result of JSON.parse to the unknown type; the compiler does not let us touch the value without verifying its type. First we confirm with typeof, null and Array.isArray that the root is an object, and only then do we read it as Record<string, unknown>. That as is a declaration, not a proof; the checks before it provide the assurance.

The real lesson is in the result's field names: jsonValid, localValid and issues stay separate, because valid JSON does not mean passing the local policy, and passing the local policy does not mean full JSON-LD conformance or Google rich-result eligibility. This demo checks one object, the exact https://schema.org context string, the value @type: Article and a nonempty headline. It deliberately does not implement context or type arrays, @graph, JSON-LD expansion, remote contexts, every Schema.org type or all of Google's requirements. A valid JSON-LD document can fail this policy by design; the tests explicitly check that type and context arrays are rejected.

The headline requirement is a tutorial policy; it is not a claim that it alone satisfies Google's Article requirements. Eligibility for a feature and appearing in results are separate matters; Google's introduction to structured data explains them in its own words.

The JSON-LD Validator on this site is a different and much more capable tool: it checks syntax with line and column positions, JSON-LD structure and Schema.org value types, and keeps Google profiles separate. The demo here is neither its implementation nor a shrunken version of it, just a small core written to show the toolchain.

Validate the classic setup first

Before migrating, draw a baseline: does the same application work with the classic tools? The jsonld-demo in this section is a test fixture deliberately pinned to older versions: Vite 8.3.4, Vitest 4.1.10, ESLint 9.39.1, Prettier 3.6.2, TypeScript 5.9.3 and typescript-eslint 8.46.1. They were chosen for a migration example, not as a recommendation for new projects; during installation npm also printed a deprecation warning for ESLint 9.39.1.

package.json (classic): the scripts and the pinned development dependencies.json
01{02  "name": "jsonld-demo",03  "private": true,04  "version": "0.0.0",05  "type": "module",06  "packageManager": "[email protected]",07  "scripts": {08    "dev": "vite",09    "build": "vite build",10    "preview": "vite preview",11    "check": "prettier --check . && eslint . && tsc --noEmit",12    "format": "prettier --write .",13    "test": "vitest run"14  },15  "devDependencies": {16    "@eslint/js": "9.39.1",17    "eslint": "9.39.1",18    "prettier": "3.6.2",19    "typescript": "5.9.3",20    "typescript-eslint": "8.46.1",21    "vite": "8.3.4",22    "vitest": "4.1.10"23  }24}

The check script chains three separate commands with &&: the format check, ESLint and tsc --noEmit. The test script runs vitest run. The configuration lives in three separate places:

vite.config.ts (classic): the Vitest configuration.ts
01import { defineConfig } from "vitest/config";02 03export default defineConfig({04  test: {05    environment: "node",06    include: ["src/**/*.test.ts"],07  },08});
eslint.config.js (classic).js
01import js from "@eslint/js";02import tseslint from "typescript-eslint";03 04export default tseslint.config(05  { ignores: ["dist/**"] },06  js.configs.recommended,07  ...tseslint.configs.recommended,08);
.prettierrc.json (classic).json
01{02  "semi": true,03  "singleQuote": false,04  "printWidth": 10005}

The test file is identical to its final form in a later section; the only difference is its first line. validate.ts, main.ts, index.html and tsconfig.json stayed byte-identical before and after the migration.

The classic first line of validate.test.ts; the migration replaced it with vite-plus/test.ts
01import { describe, expect, it } from "vitest";

After installation and formatting, the classic setup gave a clean npm run check, npm run build finished successfully, and the test output was:

npm test (classic), exit code 0. The duration was measured on this machine, once.text
01$ npm test02> [email protected] test03> vitest run04 05 06 RUN  v4.1.10 /tmp/viteplus-research-oyosbm_d/jsonld-demo07 08 09 Test Files  1 passed (1)10      Tests  17 passed (17)11   Start at  17:43:0612   Duration  100ms (transform 14ms, setup 0ms, import 22ms, tests 4ms, environment 0ms)

That is our baseline: 17/17 tests, clean static checks, a successful production build. Before migrating I did not delete the original package-lock.json or the installed Vitest 4.1.10; that was the state I tried, so do not delete them either.

vp migrate and reviewing the changes

Migrating an existing project is a single command: vp migrate. The documentation says to run it at a monorepo root and takes Vite 8+ and Vitest 4.1+ as its starting baseline; our classic setup met that. I ran its non-interactive form:

Output of vp migrate, exit code 0. The duration was measured on this machine, once.text
01$ vp migrate --no-interactive --no-agent --no-editor --no-hooks02●  Prettier configuration detected. Auto-migrating to Oxfmt...03●  Formatting code...04●  Code formatted05◇ Migrated . to Vite+ 1.1.006• Node 24.11.0  npm 11.6.107✓ Dependencies installed in 58s08• 3 config updates applied, 2 files had imports rewritten09• ESLint rules migrated to Oxlint10• Prettier migrated to Oxfmt11! Warnings:12  - Skipped 2 rules:13    - 2 Unsupported14      - no-dupe-args: Superseded by strict mode.15      - no-octal: Superseded by strict mode.

Read the output, not just the verdict. The tool found the Prettier configuration and migrated it to Oxfmt, migrated the ESLint rules to Oxlint, applied three configuration updates and rewrote the imports in two files: the configuration file and the test file. The last lines are a warning: two rules were skipped, no-dupe-args and no-octal. According to the tool's diagnostic both are “superseded by strict mode”; I did nothing further about them.

The generated package.json is below; I left it as it is, without correcting anything.

package.json (after migration): the complete generated file.json
01{02  "name": "jsonld-demo",03  "private": true,04  "version": "0.0.0",05  "type": "module",06  "packageManager": "[email protected]",07  "scripts": {08    "dev": "vp dev",09    "build": "vp build",10    "preview": "vp preview",11    "check": "vp fmt --check . && vp lint . && tsc --noEmit",12    "format": "vp fmt .",13    "test": "vp test run"14  },15  "devDependencies": {16    "typescript": "5.9.3",17    "vite": "npm:@voidzero-dev/[email protected]",18    "vite-plus": "1.1.0"19  },20  "overrides": {21    "vite": "npm:@voidzero-dev/[email protected]"22  }23}

The scripts were converted to vp commands. eslint, prettier, @eslint/js, typescript-eslint and vitest left the development dependencies; typescript stayed. vite is now the alias npm:@voidzero-dev/[email protected], and the same value is written under overrides; vite-plus was added as 1.1.0. So the project gets Vite through the Vite+ core: vp --version shows 8.3.3, which Vite+ 1.1.0 brings, not the separately released 8.3.4. The migration also removed eslint.config.js and .prettierrc.json; tsconfig.json stays.

The configuration was gathered in a single vite.config.ts. Because the generated file also contains a long list of migrated lint rules, I give two excerpts here; both are copied literally, and I did not replace the omitted rule list with a short configuration of my own.

vite.config.ts (excerpt 1/2): the import and the start of the lint section; the rule list is omitted.ts
01import { defineConfig } from "vite-plus";02 03export default defineConfig({04  lint: {05    plugins: ["oxc", "typescript", "unicorn"],06    categories: {07      correctness: "warn",08    },09    env: {10      builtin: true,11    },12    ignorePatterns: ["dist/**"],
vite.config.ts (excerpt 2/2): the lint.options, fmt and test sections.ts
01    options: {02      typeAware: true,03      typeCheck: true,04    },05    jsPlugins: [06      {07        name: "vite-plus",08        specifier: "vite-plus/oxlint-plugin",09      },10    ],11  },12  fmt: {13    semi: true,14    singleQuote: false,15    printWidth: 100,16    sortPackageJson: false,17    ignorePatterns: [],18  },19  test: {20    // Vitest v4 compatibility: preserve mock call history.21    // Remove after tests no longer rely on calls from setup or earlier tests.22    // https://viteplus.dev/guide/vitest-v5#remove-unneeded-compatibility-settings23    // https://vitest.dev/guide/migration/#clearmocks-is-enabled-by-default24    clearMocks: false,25    environment: "node",26    include: ["src/**/*.test.ts"],27  },28});

Places to notice: under lint.options both typeCheck and typeAware are on. The fmt section carries the semi, singleQuote and printWidth settings from Prettier. The test section keeps the old environment and include values and adds clearMocks: false.

I did not write this line: the migration added it to preserve mock call history for Vitest v4 compatibility; the comment beside it says when it can be removed. Our tests do not use mocks, yet I left the setting. Removing it would be a change that needs its own validation, and I did not try that cleanup.

This is a result observed in one project. It promises no equivalence for your ESLint or Prettier plugins and custom rules; read the diff line by line and judge the skipped rules and the added settings in your own project.

Checks, tests and the build through one entry point

After the migration, verification comes down to three commands. First the tests themselves: the final test file is below, identical to the classic file except for the first line.

src/validate.test.ts: 17 tests; the import comes from vite-plus/test.ts
01import { describe, expect, it } from "vite-plus/test";02import { validateJsonLd } from "./validate";03 04const article = {05  "@context": "https://schema.org",06  "@type": "Article",07  headline: "A small toolchain example",08};09 10describe("validateJsonLd", () => {11  it.each(["{", "", '{"headline": }'])("rejects malformed JSON %j", (source) => {12    expect(validateJsonLd(source)).toEqual({13      jsonValid: false,14      localValid: false,15      issues: ["Invalid JSON."],16    });17  });18 19  it.each(["null", "[]", "42", '"text"'])("rejects a non-object root %j", (source) => {20    expect(validateJsonLd(source)).toEqual({21      jsonValid: true,22      localValid: false,23      issues: ["Expected one JSON object."],24    });25  });26 27  it("accepts the local Article policy", () => {28    expect(validateJsonLd(JSON.stringify(article))).toEqual({29      jsonValid: true,30      localValid: true,31      issues: [],32    });33  });34 35  it("reports every missing local field", () => {36    expect(validateJsonLd("{}").issues).toHaveLength(3);37  });38 39  it("rejects another context", () => {40    expect(41      validateJsonLd(JSON.stringify({ ...article, "@context": "https://example.com" })).issues,42    ).toContain("@context must be https://schema.org.");43  });44 45  it("rejects another type", () => {46    expect(validateJsonLd(JSON.stringify({ ...article, "@type": "Product" })).issues).toContain(47      "@type must be Article for this demo.",48    );49  });50 51  it.each([undefined, "", "   ", 12])("rejects an invalid headline %j", (headline) => {52    expect(validateJsonLd(JSON.stringify({ ...article, headline })).issues).toContain(53      "headline must be a nonempty string.",54    );55  });56 57  it("does not support type arrays in this local policy", () => {58    expect(validateJsonLd(JSON.stringify({ ...article, "@type": ["Article"] })).localValid).toBe(59      false,60    );61  });62 63  it("does not support context arrays in this local policy", () => {64    expect(65      validateJsonLd(JSON.stringify({ ...article, "@context": ["https://schema.org"] })).localValid,66    ).toBe(false);67  });68});

The 17 cases are: three malformed JSON inputs, four non-object roots, one acceptance, the joint reporting of missing fields, another context, another type, four invalid headline values, and the rejection of type and context arrays. The cases stayed as they were in the migration; only describe, expect and it now come from vite-plus/test instead of vitest.

vp check runs formatting and linting together. Because lint.options.typeCheck is on in the example configuration, type checks are included. typeAware is a separate option that turns on rules requiring type information, and it is on too; oxlint-tsgolint provides that path. The output:

Output of vp check, exit code 0. The durations were measured on this machine, once.text
01$ vp check02note: You are running `vp check` as a Vite+ built-in command. If you meant to run the check npm script, use `vpr check` instead.03pass: All 7 files are correctly formatted (319ms, 24 threads)04pass: Found no warnings, lint errors, or type errors in 4 files (317ms, 24 threads)

The first line reminds you of an important distinction: bare vp check is Vite+'s built-in combined command. The check script in package.json, on the other hand, is still a separate command sequence after the migration (vp fmt --check . && vp lint . && tsc --noEmit), and I did not normalise it. Same-named package scripts do not override the built-in commands; to run the project script, use vp run <script> or vpr <script>. vp check --fix tries to fix problems; without --fix the command checks and does not intentionally rewrite files.

vp test --run runs the bundled Vitest once; the --run flag avoids an interactive watch session.

Output of vp test --run, exit code 0: 17/17 tests. The duration was measured on this machine, once.text
01$ vp test --run02note: You are running `vp test` as a Vite+ built-in command. If you meant to run the test npm script, use `vpr test` instead.03 04 RUN  v5.0.3 /tmp/viteplus-research-oyosbm_d/jsonld-demo05 06 07 Test Files  1 passed (1)08      Tests  17 passed (17)09   Start at  17:44:3510   Duration  113ms (transform 53%, import 25%, tests 14%, worker 8%)

vp build makes the production build and writes its output under dist/. A successful build is not evidence of type correctness or behaviour; that is why we keep the static check and the tests as separate gates.

Output of vp build, exit code 0. The duration was measured on this machine, once.text
01$ vp build02note: You are running `vp build` as a Vite+ built-in command. If you meant to run the build npm script, use `vpr build` instead.03transforming...04✓ 5 modules transformed.05rendering chunks...06computing gzip size...07dist/index.html                0.83 kB │ gzip: 0.48 kB08dist/assets/index-DLmJpvNw.js  1.49 kB │ gzip: 0.76 kB09 10✓ built in 32ms
What each command shows and what it does not
CommandWhat it showsWhat it does not show
vp checkFormatting and lint are clean; type checking is clean too because typeCheck is onBehavioural correctness
vp test --runThe core's 17 unit tests passThat the UI works in a browser or is accessible
vp buildA production bundle is producedType correctness or behaviour

The table describes scope; it is not a measurement result.

Map of the vp command by purpose: vp create, vp migrate and vp install for Setup; vp dev to Vite for Develop; vp check to Oxfmt and Oxlint (type checks when typeCheck is enabled) and vp test --run to Vitest for Verify; vp build to Vite/Rolldown for Production output; and an optional Library branch with vp pack to tsdown. A note below covers project scripts with vp run.
Groups the commands by purpose, not as a sequence: vp check runs static checks, vp test --run runs behavior tests, and they are separate gates. Same-named package.json scripts do not replace built-in commands.

A short look at the other commands. vp dev opens the development server and vp preview a local preview of the production build. vp pack is the tsdown-based library workflow; this small UI did not need it. vp run runs project scripts and vp toolchain shows the active local versions. These rest on the documentation; vp dev, vp preview and vp pack are not in the verification record.

The durations in the outputs were measured on this machine, once: each is the time the command run at that stage reported itself; no external stopwatch or comparative measurement was used. Caches and process start-up costs differ from stage to stage, so do not compare the stages, or the classic and post-migration times, with each other. The article makes no speed claim. The meaningful result is this: the classic and the migrated setups gave 17/17 tests, clean checks and a successful build; fresh-demo, carrying the same application, passed the same three gates too.

GitHub Actions and GitLab CI

The two configurations below are documented configurations adapted from the official CI guide and the versioned setup-vp input definitions; they were not run on a remote CI. I did run the commands themselves (vp install --frozen-lockfile, vp check, vp test --run, vp build) locally, and all succeeded; I did not submit the YAML files to GitHub or GitLab. They are a proposed sequence, not a picture of a successful remote run: setup, check, test, build; there is no deployment step.

GitHub Actions workflow (documented configuration; not run on a remote CI).yaml
01name: CI02on: [push, pull_request]03permissions:04  contents: read05jobs:06  verify:07    runs-on: ubuntu-latest08    steps:09      - uses: actions/checkout@v410      - uses: voidzero-dev/[email protected]11        with:12          version: '1.1.0'13          node-version: '24.11.0'14          cache: true15          run-install: false16      - run: vp install --frozen-lockfile17      - run: vp check18      - run: vp test --run19      - run: vp build

voidzero-dev/[email protected] is an exact version tag and was the latest setup-vp release at the time of the check (published on 21 September 2026). The old floating @v1 tag no longer receives updates, so use an exact tag or a commit. version: '1.1.0' pins the Vite+ version and node-version: '24.11.0' pins Node. setup-vp normally installs dependencies itself; here I turned that off with run-install: false so that the strict lockfile gate is visible and nothing is installed twice. actions/checkout@v4 is a concrete example, not a claim that it is the latest version; in a real repository you can choose an approved full SHA. The steps run in order, and if one fails the job stops.

GitLab CI/CD configuration (documented; not run on a remote CI).yaml
01include:02  - remote: 'https://raw.githubusercontent.com/voidzero-dev/setup-vp/v1.21.1/gitlab/setup-vp.yml'03    inputs:04      setup-ref: 'v1.21.1'05      version: '1.1.0'06      run-install: 'false'07 08verify:09  extends: .setup-vp10  image: node:24.11.011  script:12    - vp install --frozen-lockfile13    - vp check14    - vp test --run15    - vp build16  artifacts:17    paths:18      - dist/

On the GitLab side, the remote include and setup-ref must point to the same tag (v1.21.1). The template does not install Node; the job image provides it (image: node:24.11.0). Nor does the template configure GitLab caching by itself: this minimal example has no dependency cache, and you can add one separately with your runner's cache policy. artifacts only keeps the dist/ output.

A verification flow for CI: Setup, vp install --frozen-lockfile, vp check, vp test --run, vp build and the dist/ production output. Type checking is enabled in the config. If a check or test fails, the flow stops in an error state; there is no deployment step. Setup notes: on GitHub Actions setup-vp can provide Node and package-manager caching; GitLab uses the Node image the job supplies and configures its cache separately; dependency caching and task-result caching are separate.
Shows the flow of the documented configuration: install from the lockfile, then static checks, tests and the build in order; if a check or test fails, later steps do not run.

One more distinction: dependency caching (the package manager's data) and task-result caching (Vite Task's) are separate concepts. vp install --frozen-lockfile succeeded locally in the migrated project; npm printed deprecation warnings for some transitive packages. I tried none of this remotely: no GitHub or GitLab runner was triggered, no remote cache service was tried, and no platform other than Linux x64 was run.

The migration decision and practice

A migration decision should rest on evidence, not on a promise of speed. The checklist that comes out of this trial:

  • Runtime: is Node inside ^22.18.0 || ^24.11.0 || >=26.0.0, and is your package manager's minimum met?
  • Behaviour: do the same tests give the same result before and after migration (here 17/17)?
  • Warnings: have you read the skipped rules, the rewritten imports and the added compatibility settings?
  • Lockfile: is it committed, and does CI install with --frozen-lockfile?
  • Version alignment: does vp --version show the Vite and Vitest versions you expect?
  • Static checks: are typeCheck and typeAware deliberately on or off?
  • Project-specific parts: have Vite and ESLint plugins and framework assumptions been validated separately?

The scope is plain too: this article tried a small TypeScript application, on a single Linux x64 machine, with Node 24.11.0 and npm 11.6.1. A framework, a monorepo, other operating systems, a remote CI and browser behaviour were not tried; you need your own validation for those.

If you work with JSON-LD, you can use the site's JSON-LD Validator and its JSON Formatter & Validator; the latter formats JSON and shows errors as line and column. The demo in this article does not replace them.

In short: unifying the toolchain can reduce configuration files and commands, but knowing what you have verified is still your job. The yardstick: 17/17 tests, a clean static check, a successful build and a diff that has been read.

Official sources and further reading

The version details and command behaviour in this article were checked against these sources on 10 October 2026; the example project was tried with Vite+ 1.1.0, released after the 1.0 launch of 28 September 2026:

  1. Vite+ 1.1.0 release and exact bundled versions
  2. Vite+ latest-release API and publication timestamp
  3. Vite+ 1.0.0 release
  4. VoidZero: announcing Vite+ 1.0
  5. Vite+ 1.1.0 MIT licence
  6. Vite+ 1.1.0 CLI package and Node engine
  7. Vite+: getting started
  8. Vite+: project-local CLI, dependencies, aliases and version pinning
  9. Vite+: global CLI
  10. Vite+: creating projects
  11. Vite+: migrate to Vite+
  12. Vite+: exact migration rules
  13. Vite+: Vitest 5 migration compatibility
  14. Vite+: check command and conditional type checking
  15. Vite+: lint and type-aware configuration
  16. Vite+: format
  17. Vite+: test
  18. Vite+: build and preview
  19. Vite+: pack, powered by tsdown
  20. Vite+: run and the built-in/script distinction
  21. Vite+: environment management
  22. Vite+: CI integrations
  23. setup-vp v1.21.1 release
  24. setup-vp v1.21.1 README
  25. setup-vp v1.21.1 GitHub Action inputs
  26. setup-vp v1.21.1 GitLab template
  27. Vite: getting started and bundler
  28. Vite 8.3.4 upstream release
  29. Vitest 5.0.3 upstream release
  30. Rolldown 1.2.13 upstream release
  31. Google: structured-data introduction and eligibility
  32. Vite+ package record on npm
  33. Vite+ 1.1.0 release checksums
  34. Node.js 24.11.0 release checksums

The walkthrough uses Vite+ 1.1.0, checked on 10 October 2026, following the 28 September 2026 1.0 launch; 1.0.0 is the launch milestone, not the version that was tried. Tagged release and code URLs preserve that version, while living documentation can change. For Vite Task a revision, not a version number, is given. The CI configurations were not run on a remote CI, the installer scripts were not run, and no browser interaction or accessibility audit was performed for the UI.

✳

A single entry point gathers the commands; knowing what you have verified is still your job.

Explore more articles ↗