Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ jobs:
- name: Lint
run: pnpm run lint

- name: Check type test discovery
run: pnpm run test:discovery

- name: Check types
run: pnpm run test:types

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ pnpm-debug.log*

# tests
coverage/
/packages/types/__tests__/types-typecheck-discovery-*.spec.ts
/packages/utils/__tests__/utils-typecheck-discovery-*.spec.ts
/packages/core/tests/core-typecheck-discovery-*.spec.ts
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ pnpm install --frozen-lockfile
pnpm run fmt:check
pnpm run lint
pnpm run test:types
pnpm run test:discovery
pnpm run test
pnpm run coverage
pnpm run build
```

Oxfmt and Oxlint handle formatting and linting, Vitest runs tests and coverage, TypeScript checks declarations, and tsdown builds and validates publishable packages with publint and Are the Types Wrong.
Oxfmt and Oxlint handle formatting and linting, Vitest runs tests and coverage, TypeScript checks source and test files, and tsdown builds and validates publishable packages with publint and Are the Types Wrong.

Each package's `tsconfig.test.json` includes its source and existing `.spec.ts` tests. Both `test:types` and Vitest use it, so `test`, `test:v`, and `coverage` check type assertions too. The source-only `tsconfig.json` files remain separate for package builds.

## Releases

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dev": "turbo run dev",
"test": "turbo test",
"test:types": "turbo run test:types",
"test:discovery": "node scripts/check-type-test-discovery.mjs",
"test:v": "vitest --typecheck",
"coverage": "vitest run --coverage",
"fmt": "oxfmt .",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"build": "tsdown",
"dev": "tsdown --watch",
"test": "vitest run --typecheck",
"test:types": "tsc --noEmit"
"test:types": "tsc -p tsconfig.test.json"
},
"dependencies": {
"@ylfjuk-core/types": "workspace:*",
Expand Down
7 changes: 7 additions & 0 deletions packages/core/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src", "tests"]
}
7 changes: 7 additions & 0 deletions packages/core/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ const {
} = baseTsconfig;

export default defineProject({
test: {
typecheck: {
enabled: true,
include: ["**/*.spec.ts"],
tsconfig: "tsconfig.test.json",
},
},
resolve: {
conditions,
},
Expand Down
20 changes: 16 additions & 4 deletions packages/types/__tests__/last-of-union.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { describe, expectTypeOf, it } from "vitest";

import type { IsDisjointUnion } from "../src/is-disjoint-union";
import type { LastOfUnion } from "../src/last-of-union";

describe("LastOfUnion", () => {
it("should extract the last element in a union", () => {
type Actual = LastOfUnion<1 | 2 | 3 | 4 | 5 | "bob" | 6>;
type Expected = 6;
it("should extract one member from a union", () => {
type Members = 1 | 2 | 3 | 4 | 5 | "bob" | 6;
type Actual = LastOfUnion<Members>;

expectTypeOf<Actual>().toEqualTypeOf<Expected>();
// Union ordering is not guaranteed by TypeScript.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then this type is kind of useless

expectTypeOf<Actual>().toExtend<Members>();
expectTypeOf<Actual>().not.toBeNever();
expectTypeOf<IsDisjointUnion<Actual>>().toEqualTypeOf<false>();
});

it("should preserve a single member", () => {
expectTypeOf<LastOfUnion<"bob">>().toEqualTypeOf<"bob">();
});

it("should return never for an empty union", () => {
expectTypeOf<LastOfUnion<never>>().toBeNever();
});
});
18 changes: 14 additions & 4 deletions packages/types/__tests__/union-to-tuple.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@ import { describe, expectTypeOf, it } from "vitest";
import type { UnionToTuple } from "../src/union-to-tuple";

describe("UnionToTuple", () => {
it("should convert a union into an ordered tuple", () => {
type Actual = UnionToTuple<1 | 2 | 3 | 4 | 5 | "bob" | 6>;
type Expected = [1, 2, 3, 4, 5, "bob", 6];
it("should include each union member once in a tuple", () => {
type Members = 1 | 2 | 3 | 4 | 5 | "bob" | 6;
type Actual = UnionToTuple<Members>;

expectTypeOf<Actual>().toEqualTypeOf<Expected>();
// Union ordering is not guaranteed by TypeScript.
expectTypeOf<Actual[number]>().toEqualTypeOf<Members>();
expectTypeOf<Actual["length"]>().toEqualTypeOf<7>();
});

it("should convert never into an empty tuple", () => {
expectTypeOf<UnionToTuple<never>>().toEqualTypeOf<[]>();
});

it("should convert a single member into a one-element tuple", () => {
expectTypeOf<UnionToTuple<"bob">>().toEqualTypeOf<["bob"]>();
});
});
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"scripts": {
"build": "pnpm run test && pnpm run test:types && publint && attw --pack --profile esm-only",
"test": "vitest run --typecheck",
"test:types": "tsc --noEmit"
"test:types": "tsc -p tsconfig.test.json"
},
"peerDependencies": {
"typescript": "^7.0.2"
Expand Down
7 changes: 7 additions & 0 deletions packages/types/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src", "__tests__"]
}
7 changes: 7 additions & 0 deletions packages/types/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ const {
} = baseTsconfig;

export default defineProject({
test: {
typecheck: {
enabled: true,
include: ["**/*.spec.ts"],
tsconfig: "tsconfig.test.json",
},
},
resolve: {
conditions,
},
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"build": "tsdown",
"dev": "tsdown --watch",
"test": "vitest run --typecheck",
"test:types": "tsc --noEmit",
"test:types": "tsc -p tsconfig.test.json",
"lint": "oxlint"
},
"dependencies": {
Expand Down
7 changes: 7 additions & 0 deletions packages/utils/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src", "__tests__"]
}
7 changes: 7 additions & 0 deletions packages/utils/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ const {
} = baseTsconfig;

export default defineProject({
test: {
typecheck: {
enabled: true,
include: ["**/*.spec.ts"],
tsconfig: "tsconfig.test.json",
},
},
resolve: {
conditions,
},
Expand Down
69 changes: 69 additions & 0 deletions scripts/check-type-test-discovery.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { unlinkSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const root = fileURLToPath(new URL("../", import.meta.url));
const npmExecPath = process.env.npm_execpath;
assert.ok(npmExecPath, "Run this check via `pnpm run test:discovery` (npm_execpath is unset).");
const [command, ...prefixArgs] = /\.[cm]?js$/i.test(npmExecPath)
? [process.execPath, npmExecPath]
: [npmExecPath];
const fixtures = [];

function pnpm(...args) {
const result = spawnSync(command, [...prefixArgs, ...args], {
cwd: root,
encoding: "utf8",
timeout: 60_000,
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
});
assert.ifError(result.error);
assert.equal(result.status, 1, result.stdout + result.stderr);
return result.stdout + result.stderr;
}

try {
for (const [name, directory] of [
["types", "__tests__"],
["utils", "__tests__"],
["core", "tests"],
]) {
const filename = `${name}-typecheck-discovery-${randomUUID()}.spec.ts`;
const path = new URL(`../packages/${name}/${directory}/${filename}`, import.meta.url);
writeFileSync(
path,
'import { expectTypeOf, it } from "vitest";\nit("typecheck discovery", () => {\nexpectTypeOf<number>().toEqualTypeOf<string>();\n});\n',
{ flag: "wx" },
);
fixtures.push({ filename, path });
const output = pnpm("--dir", `packages/${name}`, "run", "test:types");
assert.ok(output.includes(`${filename}(3,`) && output.includes("error TS2344"), output);
console.log(`${name}: test:types rejected the invalid assertion`);
}

const output = pnpm("exec", "vitest", "run", ...fixtures.map(({ filename }) => filename));
for (const { filename } of fixtures) {
assert.match(
output,
new RegExp(`FAIL[^\n]*${filename.replaceAll(".", "\\.")}[^\n]*\r?\nTypeCheckError:`),
);
}
console.log("Root Vitest rejected the invalid assertion in all three projects");
} finally {
const errors = [];
for (const { path } of fixtures) {
try {
unlinkSync(path);
} catch (error) {
errors.push(error);
}
}
if (errors.length) {
console.error(
new AggregateError(errors, "Could not remove all typecheck discovery fixtures"),
);
process.exitCode = 1;
}
}
Loading