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:
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.
| Tool | Provided by Vite+ 1.1.0 | Current separate release |
|---|---|---|
| Vite | 8.3.3 | 8.3.4 |
| Rolldown | 1.2.12 | 1.2.13 |
| Vitest | 5.0.3 | 5.0.3 |
| Oxlint | 1.87.0 | not checked |
| Oxfmt | 0.72.0 | not checked |
| oxlint-tsgolint | 7.0.2003 | not checked |
| tsdown | 0.23.0 | not checked |
| Vite Task | revision 7d69d6577ecf6bd83deee32186de59918a712873 | not 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:
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 runDo 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.
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 casestsconfig.json covers only the src folder, turns on strict mode and emits nothing (noEmit); the migration in section 6 did not change it.
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.
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>textContent.ts01import { 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.
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.
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:
01import { defineConfig } from "vitest/config";02 03export default defineConfig({04 test: {05 environment: "node",06 include: ["src/**/*.test.ts"],07 },08});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);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.
vite-plus/test.ts01import { 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:
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:
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.
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.
lint section; the rule list is omitted.ts01import { 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/**"],lint.options, fmt and test sections.ts01 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.
vite-plus/test.ts01import { 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:
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.
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.
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| Command | What it shows | What it does not show |
|---|---|---|
vp check | Formatting and lint are clean; type checking is clean too because typeCheck is on | Behavioural correctness |
vp test --run | The core's 17 unit tests pass | That the UI works in a browser or is accessible |
vp build | A production bundle is produced | Type correctness or behaviour |
The table describes scope; it is not a measurement result.
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.
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 buildvoidzero-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.
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.
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 --versionshow the Vite and Vitest versions you expect? - Static checks: are
typeCheckandtypeAwaredeliberately 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:
- Vite+ 1.1.0 release and exact bundled versions
- Vite+ latest-release API and publication timestamp
- Vite+ 1.0.0 release
- VoidZero: announcing Vite+ 1.0
- Vite+ 1.1.0 MIT licence
- Vite+ 1.1.0 CLI package and Node engine
- Vite+: getting started
- Vite+: project-local CLI, dependencies, aliases and version pinning
- Vite+: global CLI
- Vite+: creating projects
- Vite+: migrate to Vite+
- Vite+: exact migration rules
- Vite+: Vitest 5 migration compatibility
- Vite+: check command and conditional type checking
- Vite+: lint and type-aware configuration
- Vite+: format
- Vite+: test
- Vite+: build and preview
- Vite+: pack, powered by tsdown
- Vite+: run and the built-in/script distinction
- Vite+: environment management
- Vite+: CI integrations
- setup-vp v1.21.1 release
- setup-vp v1.21.1 README
- setup-vp v1.21.1 GitHub Action inputs
- setup-vp v1.21.1 GitLab template
- Vite: getting started and bundler
- Vite 8.3.4 upstream release
- Vitest 5.0.3 upstream release
- Rolldown 1.2.13 upstream release
- Google: structured-data introduction and eligibility
- Vite+ package record on npm
- Vite+ 1.1.0 release checksums
- 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 ↗