Skip to content
Open
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
40 changes: 40 additions & 0 deletions .github/workflows/typescript.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
### ### Type check
###
### Type checks the frontend assets with TypeScript's checkJs.
###
### #### Assumptions
###
### 1. A docker compose service named `node` can be run and `npm` can be run
### inside the `node` service.
### 2. `npm run typecheck` is defined in `package.json`.

on: pull_request

name: Type check

env:
COMPOSE_USER: runner

jobs:
typescript:
name: Type check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Cache node_modules
uses: actions/cache@v6
with:
path: node_modules
key: node-modules-${{ hashFiles('package-lock.json') }}
restore-keys: node-modules-

- name: Setup network
run: docker network create frontend

- name: Install dependencies
run: docker compose run --rm node npm install

- name: Type check
run: docker compose run --rm node npm run typecheck
1 change: 1 addition & 0 deletions .typecheck-strict-paths
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
assets/client/util/id-from-path.js
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file.
`@eslint-react` flat config, run via `task coding-standards:eslint:check` and a new CI job.
`exhaustive-deps` is error-level in `assets/client` and `assets/shared` (outside the slide
templates) and warn elsewhere pending burn-down.
- Added JSDoc type checking of the frontend assets with a grow-only strict ratchet
(`task code-analysis:assets`): files listed in `.typecheck-strict-paths` are checked with
`noImplicitAny`, so an unannotated shape fails the build at every use site.

## [3.0.0-rc8] - 2026-08-24

Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,28 @@ task phpstan:generate-baseline

PHPStan [rule level](https://phpstan.org/user-guide/rule-levels) is set to level 6.

### Frontend type checking

The frontend assets are type checked through JSDoc annotations with TypeScript's `checkJs`
([`tsconfig.checkjs.json`](tsconfig.checkjs.json)):

```shell
task code-analysis:assets
```

`noImplicitAny` is on, which means reading or writing through an unannotated shape is an error — so a
missing `@type` annotation fails the build at every use site. Enforcing that across the whole tree at
once is not realistic, so the gate is a grow-only ratchet: only files listed in
[`.typecheck-strict-paths`](.typecheck-strict-paths) fail the build.

**Add every `assets/` file you touch in a PR to `.typecheck-strict-paths` and burn its findings to zero
in that same PR.** The list only grows.

Two notes on the annotation idiom: typedefs live next to the code as JSDoc, and quoted keys (the JSON-LD
`"@id"`) need the inline object-literal form — `@typedef {{ "@id": string, ... }} Name` — because
`@property` cannot express them. Write one `@typedef` per comment block; several in one block silently
fail to parse.

## Upgrade Guide

See [UPGRADE.md](UPGRADE.md) for upgrade guides.
Expand Down
6 changes: 6 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ tasks:
cmds:
- task composer -- code-analysis

code-analysis:assets:
desc: "Runs type checking on frontend assets (checkJs strict ratchet)."
cmds:
- task compose -- run --rm node npm run typecheck

phpstan:generate-baseline:
desc: "Generates the PHPStan baseline file."
cmds:
Expand All @@ -219,6 +224,7 @@ tasks:
cmds:
- task coding-standards:check
- task code-analysis
- task code-analysis:assets
- task rector:check
- task test:api
- task test:frontend-local
Expand Down
2 changes: 1 addition & 1 deletion assets/client/util/id-from-path.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @param {object} string - The url to cut id from.
* @param {unknown} string - The url to cut id from.
* @returns {string|boolean} The id or false.
*/
function idFromPath(string) {
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

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

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"test:unit": "vitest run",
"test:unit:watch": "vitest",
"lint:js": "eslint 'assets/**/*.{js,jsx,ts,tsx}'",
"lint:js:fix": "eslint 'assets/**/*.{js,jsx,ts,tsx}' --fix"
"lint:js:fix": "eslint 'assets/**/*.{js,jsx,ts,tsx}' --fix",
"typecheck": "node scripts/typecheck-strict.mjs"
},
"devDependencies": {
"@eslint-react/eslint-plugin": "^5.18.6",
Expand All @@ -18,6 +19,8 @@
"@rtk-query/codegen-openapi": "^2.0.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/crypto-js": "^4.2.0",
"@types/lodash": "^4.17.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.6.0",
Expand Down
44 changes: 44 additions & 0 deletions scripts/typecheck-strict.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Ratchet: noImplicitAny type checking enforced only for files listed in
// .typecheck-strict-paths. The list is grow-only; add every assets/ file you
// touch in the same PR and burn its errors to zero.
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";

const paths = readFileSync(".typecheck-strict-paths", "utf8")
.split("\n")
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#"));

let out = "";
try {
execSync("npx tsc -p tsconfig.checkjs.json --pretty false", {
encoding: "utf8",
});
} catch (e) {
out = e.stdout ?? "";
}

const lines = out.split("\n").filter(Boolean);

// Config-level or global failures carry no file prefix; never let them pass
// silently.
const globalErrors = lines.filter(
(line) => /error TS\d+/.test(line) && !/^assets\//.test(line),
);
if (globalErrors.length > 0) {
console.error(globalErrors.join("\n"));
console.error("\ntsc failed before file-level checking; aborting.");
process.exit(2);
}

const offending = lines.filter((line) => paths.some((p) => line.startsWith(p)));
const total = lines.filter((line) => /error TS\d+/.test(line)).length;

if (offending.length > 0) {
console.error(offending.join("\n"));
console.error(`\n${offending.length} error(s) in strict-checked files.`);
process.exit(1);
}
console.log(
`typecheck:strict OK (${paths.length} file(s) enforced; ${total} error(s) remain repo-wide, informational).`,
);
24 changes: 24 additions & 0 deletions tsconfig.checkjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"checkJs": true,
"noImplicitAny": true,
"strict": false,
"jsx": "react-jsx",
"target": "es2022",
"module": "esnext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"types": ["vite/client", "vite-plugin-svgr/client"]
},
"include": [
"assets/**/*.js",
"assets/**/*.jsx",
"assets/**/*.ts",
"assets/**/*.tsx"
]
}