Going Viteless in 2026

Or how you may not need Vite ecosystem to develop TypeScript libraries

Published on

Vite is the only bundler I use. Vitest is the only testing framework I use. Evan You, the creator of these tools, is truly a blessing to the JavaScript™ community. You may know him for the Vue framework, but in my opinion, his Vite-branded projects are much more impactful.

For me, using these tools is a no-brainer: I don’t need to install webpack to bundle, babel to transpile TypeScript, and jest to test… Then, to make sure these tools work together, I have to create a million configs and use other tools to make them cooperate. It’s a vicious cycle of “another npm package will fix it.”

Run vite and the web app is served in watch mode. Run vitest and all tests run in watch mode. Want code coverage? Run vitest run --coverage and it just works, again.

However, at times vite and vitest seem like overkill. It’s like bringing a bazooka to a knife fight. In this article, I will show that only 3 packages are needed to develop TypeScript libraries: typescript, tsx, and @types/node.

The code accompanying this article is on GitHub.

Aha Moment

After setting up yet another TypeScript library, I noticed that node:test exports functions such as describe, test, and it. The node:assert package looks like something I can use for testing…

Testing API is stable since v20.0.0. I am using Node.js 24 if you want to follow along.

Test With Node.js

It’s easy. Use tsx to let node run TypeScript directly. The command to run tests is node --import tsx --test. Want tests to rerun on code change? Add the --watch flag. Need test coverage? Add --experimental-test-coverage.

Using node:test and node:assert for testing is quite straightforward

example.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function throwError(message: string): never {
  throw new Error(message);
}
example.test.ts
import { describe, it } from "node:test";
import assert from "node:assert";
import { add, throwError } from "./example.js";

it("can sum numbers", () => {
  const actual = add(1, 2);
  const expected = 3;

  assert.strictEqual(actual, expected);
});

it("can handle errors", () => {
  // by error type (Error is of type ErrorConstructor)
  assert.throws(() => throwError("test error"), Error);

  // with message assertion
  assert.throws(() => throwError("test error"), Error, "test error");
});

// Bonus! Golang inspired
describe("table tests", () => {
  const testCases = [
    { name: "positive numbers", a: 1, b: 2, expected: 3 },
    { name: "double infinity", a: Infinity, b: Infinity, expected: Infinity },
    { name: "subtract self", a: -Infinity, b: Infinity, expected: NaN },
    { name: "IEEE 754 :)", a: 0.1, b: 0.2, expected: 0.30000000000000004 },
  ];

  testCases.forEach(({ name, a, b, expected }) => {
    it(name, () => {
      const result = add(a, b);
      assert.strictEqual(
        result,
        expected,
        `Expected add(${a}, ${b}) to equal ${expected}, got ${result}`,
      );
    });
  });
});

The output of node --import tsx --test is

✔ can sum numbers (0.59279ms)
✔ can handle errors (0.325195ms)
▶ table tests
  ✔ positive numbers (0.196521ms)
  ✔ double infinity (0.108285ms)
  ✔ subtract self (0.120578ms)
  ✔ IEEE 754 :) (0.078078ms)
✔ table tests (0.822425ms)
ℹ tests 6
ℹ suites 1
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 160.479201

What About Mocking?

Certainly! It is supported :) Here is a snippet on how to mock functions and time.

mocking.test.ts
import { it, mock } from "node:test";
import assert from "node:assert";

function functionRunner(fn: (arg: string, date: Date) => void) {
  const now = new Date();
  return {
    run: (arg: string) => fn(arg, now),
  };
}

it("can mock functions", () => {
  const mockFn = mock.fn();
  mock.timers.enable({ now: new Date("2000-01-01T00:02:02Z"), apis: ["Date"] });

  const runner = functionRunner(mockFn);
  runner.run("test");

  assert.strictEqual(mockFn.mock.calls.length, 1);
  assert.deepStrictEqual(mockFn.mock.calls[0]?.arguments, [
    "test",
    new Date("2000-01-01T00:02:02Z"),
  ]);
});

Vitest embeds Sinon.JS to do mocking, similarly to how it uses Jest under the hood for testing. Node.js owns its testing stack instead of gluing other code together.

Build With tsc

Building the code is as simple as running tsc.

I like to have my tests alongside the code, and I strongly suggest not to publish transpiled tests. To achieve this, create a tsconfig.build.json file that references the base tsconfig.json but excludes files matching the test pattern **/*.test.ts.

What Are The Downsides?

If someone tells you that the new solution has no downsides, they are probably scamming you. There are a few things I miss from Vitest.

Inline Snapshots

Inline snapshots are my favorite way to test complicated data structures. The first advantage is that they can be generated by the test runner. The second benefit is that assertion is done with strings. This decouples implementation from testing.

expect(onDataMock.mock.calls).toMatchInlineSnapshot(`
  Array [
    Array [
      Object {
        "id": "1",
      },
    ],
    Array [
      Object {
        "id": "2",
      },
    ],
  ]
`);

Flexible Environment

If you want to run the tests outside of Node.js, Vitest is the way to go. Vite supports environments such as jsdom, happy-dom, and node. It can also run tests in real browsers by using playwright.

Type Testing

This is a little-known Vitest feature, but it’s great for type-centric libraries.

expectTypeOf({ docId: '123' }).toEqualTypeOf<{ docId: string }>();

Let’s Wrap It Up

When running npm install, only 8 packages are added.

added 8 packages, and audited 9 packages in 532ms

2 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Other notable benefits are:

The proper viteless library setup in 2026 looks like the following:

package.json
{
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  },
  "files": [
    "dist",
    "src"
  ],
  "scripts": {
    "dev": "node --import tsx --test --watch",
    "test": "node --import tsx --test",
    "coverage": "node --import tsx --test --experimental-test-coverage",
    "build": "rm -rf ./dist && tsc -p tsconfig.build.json",
    "prepublishOnly": "npm run test && npm run build"
  },
  "dependencies": {},
  "devDependencies": {
    "typescript": "^5.9.3",
    "@types/node": "^25.2.1",
    "tsx": "^4.21.0"
  }
}
tsconfig.build.json
{
  "extends": "./tsconfig",
  "compilterOptions": {
    "rootDir": "./src",
    "outDir": "./dist",
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,
  }
  "exclude": ["**/*.test.ts"],
}