Skip to content
Merged
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ npm-debug.log*
.npm

.DS_Store

# Written and removed by src/buildTypecheck.test.ts; only present mid-test.
src/buildTypecheckProbe.generated.ts
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ npm ci
npm run build # vite library build -> dist/index.js + rolled-up index.d.ts
npm test # vitest
npm run test:e2e # real signalk-server + real QuestDB; never in CI
npm run typecheck # tsc --noEmit (the build itself does not typecheck)
npm run typecheck # tsc --noEmit (also run inside the build, via vite-plugin-checker)
npm run lint # eslint flat config
npm run format # prettier --write
```
Expand Down Expand Up @@ -52,7 +52,11 @@ requires. `engines.node` is the field that says that.

On top of the base, `tsconfig.json` is strict and then some — `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `noImplicitReturns`, `noUnusedLocals`. Keep them on; they caught real bugs when they went in.

Vite transpiles without type checking, so **`npm run build` passing does not mean the types are sound.** CI runs `typecheck` separately and so should you.
Vite transpiles without type checking on its own, so the build runs `tsc` through
`vite-plugin-checker` and fails on the first error — **`npm run build` passing does mean the
types are sound.** `npm run typecheck` is still the quick way to check them without
building, and CI runs it separately. The checker is skipped under vitest, where a type
error in an unrelated file should not stop the suite.

## Architecture

Expand Down
205 changes: 205 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"typescript": "^5.9.3",
"typescript-eslint": "^8.66.0",
"vite": "^8.2.2",
"vite-plugin-checker": "^0.14.5",
"vite-plugin-dts": "^5.0.3",
"vitest": "^5.0.0"
}
Expand Down
45 changes: 45 additions & 0 deletions src/buildTypecheck.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { spawnSync } from 'node:child_process'
import { rmSync, writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'

// vite transpiles without typechecking, so the build only rejects unsound
// source because vite.config.ts adds vite-plugin-checker. That guarantee is
// worth an executed test rather than a comment: the plugin is skipped when
// VITEST is set, which is exactly the environment this suite runs in, so a
// regression here would otherwise be invisible from inside the suite.
//
// The build is spawned with VITEST removed, which is what the real `npm run
// build` looks like.
const PROBE = new URL('./buildTypecheckProbe.generated.ts', import.meta.url)
const VITE_CLI = new URL('../node_modules/vite/bin/vite.js', import.meta.url)

function buildWithoutVitest() {
const env = { ...process.env }
delete env.VITEST
// The vite CLI is run through node rather than npx: `npx` is npx.cmd on
// Windows, which spawnSync cannot execute without a shell, and CI runs this
// suite there.
return spawnSync(process.execPath, [fileURLToPath(VITE_CLI), 'build'], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
env,
encoding: 'utf8',
})
}

describe('the build', () => {
it('fails on a type error the transpiler would accept', () => {
// Valid JavaScript once the types are erased, so only a typechecker
// rejects it — which is the whole point of the assertion.
writeFileSync(PROBE, 'export const broken: number = "not a number"\n')

try {
const result = buildWithoutVitest()

expect(result.status).not.toBe(0)
expect(`${result.stdout}${result.stderr}`).toContain('TS2322')
} finally {
rmSync(PROBE, { force: true })
}
}, 120_000)
})
Loading
Loading