Skip to content

Commit 965cd21

Browse files
committed
Enforce strict tsconfig and fix type errors
1 parent 4828b59 commit 965cd21

35 files changed

+269
-107
lines changed

.github/workflows/ci.yml

+16
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ jobs:
3939
- name: Run ESLint
4040
run: npm run lint
4141

42+
typecheck:
43+
runs-on: ubuntu-latest
44+
name: Type check
45+
steps:
46+
- name: Checkout the repository
47+
uses: actions/checkout@v4
48+
- name: Set up Node.js
49+
uses: actions/setup-node@v4
50+
with:
51+
node-version-file: .tool-versions
52+
cache: npm
53+
- name: Install dependencies
54+
run: npm ci
55+
- name: Check types using TypeScript compiler
56+
run: npm run typecheck
57+
4258
docs:
4359
runs-on: ubuntu-latest
4460
name: Docs check

docs/react-testing-library.md

+29-29
Large diffs are not rendered by default.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"lint:fix": "eslint . --fix",
2424
"format": "prettier --write .",
2525
"format:check": "prettier --check .",
26+
"typecheck": "tsc",
2627
"docs": "node scripts/docs.js",
2728
"release": "release-it"
2829
},

scripts/docs.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ async function generateDocs() {
3939
}
4040

4141
/**
42-
* @param {string[]} names
42+
* @param {import('./helpers/types.js').ConfigName[]} names
4343
* @returns {Promise<import('./helpers/types.js').ExportedConfig[]>}
4444
*/
4545
function loadConfigs(names) {
@@ -89,10 +89,13 @@ async function loadPeerDependencies(configs) {
8989
{},
9090
);
9191

92+
/** @type {Record<string, {optional: boolean}>} */
93+
const peerDependenciesMeta = packageJson.peerDependenciesMeta;
94+
9295
return Object.entries(packageJson.peerDependencies).map(([pkg, version]) => ({
9396
pkg,
9497
version,
95-
optional: packageJson.peerDependenciesMeta[pkg]?.optional ?? false,
98+
optional: peerDependenciesMeta[pkg]?.optional ?? false,
9699
usedByConfigs: pkgConfigs[pkg] ?? [],
97100
}));
98101
}
@@ -198,6 +201,11 @@ async function generateConfigDocs(config, allConfigs, peerDeps) {
198201
* @returns {import('./helpers/types.js').RuleData} Rule data
199202
*/
200203
function findRuleData(id, config, rules) {
204+
const meta = rules[id];
205+
if (!meta) {
206+
throw new Error(`Can't find metadata for rule ${id}`);
207+
}
208+
201209
const entry =
202210
findRuleEntry(
203211
config.flatConfig.filter(
@@ -231,7 +239,7 @@ function findRuleData(id, config, rules) {
231239

232240
return {
233241
id,
234-
meta: rules[id],
242+
meta,
235243
level,
236244
...(Array.isArray(entry) &&
237245
entry.length > 1 && {

scripts/helpers/configs.js

+14-20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { md } from 'build-md';
55
import { getEnabledRuleIds } from './rules.js';
66

7+
/** @type {Record<import('./types.js').ConfigName, import('build-md').FormattedText>} */
78
const configDescriptions = {
89
javascript: md`Default config, suitable for any ${md.bold('JavaScript/TypeScript')} project.`,
910
typescript: md`Config for strict ${md.bold('TypeScript')} projects.`,
@@ -20,11 +21,11 @@ const configDescriptions = {
2021
'react-testing-library': md`Config for projects using ${md.bold('React Testing Library')} for testing.`,
2122
};
2223

23-
/** @type {(keyof typeof configDescriptions)[]} */
24+
/** @type {(import('./types.js').ConfigName)[]} */
2425
// @ts-expect-error keys won't be any string
2526
export const configNames = Object.keys(configDescriptions);
2627

27-
/** @type {Record<keyof typeof configDescriptions, import('./types.js').Icon>} */
28+
/** @type {Record<import('./types.js').ConfigName, import('./types.js').Icon>} */
2829
const configIcons = {
2930
javascript: 'material/javascript',
3031
typescript: 'material/typescript',
@@ -41,7 +42,7 @@ const configIcons = {
4142
'react-testing-library': 'other/testing-library',
4243
};
4344

44-
/** @type {Partial<Record<keyof typeof configDescriptions, string>>} */
45+
/** @type {Partial<Record<import('./types.js').ConfigName, string>>} */
4546
const configPatterns = {
4647
graphql: '*.graphql',
4748
jest: '*.test.ts',
@@ -52,13 +53,13 @@ const configPatterns = {
5253
'react-testing-library': '*.spec.tsx',
5354
};
5455

55-
/** @type {Partial<Record<keyof typeof configDescriptions, string>>} */
56+
/** @type {Partial<Record<import('./types.js').ConfigName, string>>} */
5657
const configExtraPatterns = {
5758
angular: '*.html',
5859
storybook: '.storybook/main.ts',
5960
};
6061

61-
/** @type {Set<(keyof typeof configDescriptions)>} */
62+
/** @type {Set<(import('./types.js').ConfigName)>} */
6263
const testConfigs = new Set([
6364
'jest',
6465
'vitest',
@@ -71,7 +72,7 @@ const eslintConfig = 'eslint.config.js';
7172

7273
const tsConfigDocsReference = md`Refer to ${md.link('./typescript.md#🏗️-setup', "step 3 in TypeScript config's setup docs")} for how to set up tsconfig properly.`;
7374

74-
/** @type {Partial<Record<keyof typeof configDescriptions, import('build-md').FormattedText>>} */
75+
/** @type {Partial<Record<import('./types.js').ConfigName, import('build-md').FormattedText>>} */
7576
export const configsExtraSetupDocs = {
7677
typescript: md`${md.paragraph(
7778
md`Because this config includes rules which require type information, make sure to configure ${md.code('parserOptions.project')} in your ${md.code('eslint.config.js')} points to your project's tsconfig.
@@ -207,7 +208,7 @@ const angularExtraEslintrc = `,
207208
}
208209
}`;
209210

210-
/** @type {Partial<Record<keyof typeof configDescriptions, string>>} */
211+
/** @type {Partial<Record<import('./types.js').ConfigName, string>>} */
211212
export const configsExtraEslintrc = {
212213
angular: angularExtraEslintrc,
213214
ngrx: angularExtraEslintrc,
@@ -236,30 +237,24 @@ export const configsExtraEslintrc = {
236237

237238
/**
238239
* Get description for given config.
239-
* @param {string} name Config file name without extension
240+
* @param {import('./types.js').ConfigName} name Config file name without extension
240241
*/
241242
export function configDescription(name) {
242-
if (!(name in configDescriptions)) {
243-
throw new Error(`No description found for config ${name}`);
244-
}
245243
return configDescriptions[name];
246244
}
247245

248246
/**
249247
* Get icon name for given config.
250-
* @param {string} name Config file name without extension
248+
* @param {import('./types.js').ConfigName} name Config file name without extension
251249
* @returns {import('./types.js').Icon}
252250
*/
253251
export function configIcon(name) {
254-
if (!(name in configIcons)) {
255-
throw new Error(`No icon found for config ${name}`);
256-
}
257252
return configIcons[name];
258253
}
259254

260255
/**
261256
* Get file pattern for given config.
262-
* @param {string} name Config file name without extension
257+
* @param {import('./types.js').ConfigName} name Config file name without extension
263258
*/
264259
export function configPattern(name) {
265260
if (!(name in configPatterns)) {
@@ -270,7 +265,7 @@ export function configPattern(name) {
270265

271266
/**
272267
* Get additional file pattern for given config.
273-
* @param {string} name Config file name without extension
268+
* @param {import('./types.js').ConfigName} name Config file name without extension
274269
* @returns {string | undefined}
275270
*/
276271
export function configExtraPattern(name) {
@@ -279,16 +274,15 @@ export function configExtraPattern(name) {
279274

280275
/**
281276
* Is config targetting some testing framework?
282-
* @param {string} name Config file name without extension
277+
* @param {import('./types.js').ConfigName} name Config file name without extension
283278
*/
284279
export function isConfigForTests(name) {
285-
// @ts-expect-error the point is to check if string is a union
286280
return testConfigs.has(name);
287281
}
288282

289283
/**
290284
* Imports flat config array by name.
291-
* @param {string} name Config file name without extension
285+
* @param {import('./types.js').ConfigName} name Config file name without extension
292286
* @returns {Promise<import('@typescript-eslint/utils').TSESLint.FlatConfig.ConfigArray>}
293287
*/
294288
export async function importConfig(name) {

scripts/helpers/format-config.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const setupLink = '../README.md#🏗️-setup';
1515

1616
/**
1717
* Format Markdown documentation for given config.
18-
* @param {string} config Config name
18+
* @param {import('./types.js').ConfigName} config Config name
1919
* @param {import('./types.js').RuleData[]} rules List of rules included in config
2020
* @param {import('./types.js').ExtendedConfig[]} extended List of extended Code PushUp configs
2121
* @param {import('./types.js').PeerDep[]} peerDeps Peer dependencies
@@ -41,7 +41,7 @@ export function configRulesToMarkdown(
4141

4242
/**
4343
* Generate docs on how to setup given config
44-
* @param {string} config Config name
44+
* @param {import('./types.js').ConfigName} config Config name
4545
* @param {import('./types.js').PeerDep[]} peerDeps Peer dependencies
4646
*/
4747
function setupDocs(config, peerDeps) {
@@ -96,7 +96,7 @@ function setupDocs(config, peerDeps) {
9696

9797
/**
9898
* Generate docs on rules included in given config
99-
* @param {string} config Config name
99+
* @param {import('./types.js').ConfigName} config Config name
100100
* @param {import('./types.js').RuleData[]} rules List of rules included in config
101101
* @param {import('./types.js').ExtendedConfig[]} extended List of extended Code PushUp configs
102102
* @param {{hideOverrides?: boolean}} options Extra options
@@ -251,6 +251,7 @@ function formatRuleOverrides(rule) {
251251

252252
/**
253253
* @param {unknown} options
254+
* @returns {string}
254255
*/
255256
function optionsPreview(options) {
256257
if (Array.isArray(options)) {

scripts/helpers/format-readme.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212

1313
/**
1414
* Format Markdown documentation for README
15-
* @param {string[]} configs Config names
15+
* @param {import('./types.js').ConfigName[]} configs Config names
1616
* @param {import('./types.js').PeerDep[]} peerDeps Peer dependencies
1717
* @param {Record<string, string[]>} extended Map of extended configs
1818
*/
@@ -29,7 +29,7 @@ export function configsToMarkdown(configs, peerDeps, extended) {
2929

3030
/**
3131
* Generate docs with overview of all configs and relationships between them
32-
* @param {string[]} configs Config names
32+
* @param {import('./types.js').ConfigName[]} configs Config names
3333
* @param {Record<string, string[]>} extended Map of extended configs
3434
*/
3535
function configsOverviewDocs(configs, extended) {

scripts/helpers/packages.js

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export function sortPeerDeps(peerDeps) {
5151
return Number(a.optional) - Number(b.optional);
5252
})
5353
.reduce(
54+
/** @param {Record<string, (import('./types.js').PeerDep)[]>} acc */
5455
(acc, peerDep) => ({
5556
...acc,
5657
[packageIcon(peerDep.pkg)]: [

scripts/helpers/plugins.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@ const pluginDocsUrls = {
5454
playwright:
5555
'https://github.com/playwright-community/eslint-plugin-playwright#readme',
5656
promise: 'https://github.com/eslint-community/eslint-plugin-promise#readme',
57-
'react-testing-library':
58-
'https://github.com/testing-library/eslint-plugin-testing-library#readme',
5957
react: 'https://github.com/jsx-eslint/eslint-plugin-react#readme',
6058
'react-hooks':
6159
'https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks#readme',
6260
rxjs: 'https://github.com/cartant/eslint-plugin-rxjs#readme',
6361
'rxjs-x': 'https://github.com/JasonWeinzierl/eslint-plugin-rxjs-x#readme',
6462
sonarjs: 'https://github.com/SonarSource/eslint-plugin-sonarjs#readme',
6563
storybook: 'https://github.com/storybookjs/eslint-plugin-storybook#readme',
64+
'testing-library':
65+
'https://github.com/testing-library/eslint-plugin-testing-library#readme',
6666
unicorn: 'https://github.com/sindresorhus/eslint-plugin-unicorn#readme',
6767
vitest: 'https://github.com/veritem/eslint-plugin-vitest#readme',
6868
};
@@ -71,18 +71,20 @@ const pluginDocsUrls = {
7171
* @param {string} plugin
7272
*/
7373
export function pluginIcon(plugin) {
74-
if (!(plugin in pluginIcons)) {
74+
const icon = pluginIcons[plugin];
75+
if (!icon) {
7576
throw new Error(`No icon found for plugin ${plugin}`);
7677
}
77-
return pluginIcons[plugin];
78+
return icon;
7879
}
7980

8081
/**
8182
* @param {string} plugin
8283
*/
8384
export function pluginDocs(plugin) {
84-
if (!(plugin in pluginIcons)) {
85+
const docsUrl = pluginDocsUrls[plugin];
86+
if (!docsUrl) {
8587
throw new Error(`No docs URL found for plugin ${plugin}`);
8688
}
87-
return pluginDocsUrls[plugin];
89+
return docsUrl;
8890
}

scripts/helpers/rules.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,16 @@ export function getRulesMetadata(config, ruleIds, eslint, dummyFile) {
8080
suppressedMessages: [],
8181
},
8282
]);
83-
return [id, rules[id]];
83+
const meta = rules[id];
84+
if (!meta) {
85+
return null;
86+
}
87+
return [id, meta];
8488
}
8589
const plugin = plugins[rule.plugin];
8690
const ruleDef = plugin?.rules?.[rule.name];
8791
if (typeof ruleDef === 'object') {
88-
/** @type {import('@typescript-eslint/utils').TSESLint.RuleMetaDataWithDocs} */
92+
/** @type {import('@typescript-eslint/utils').TSESLint.RuleMetaDataWithDocs<string>} */
8993
// @ts-expect-error assuming valid metadata
9094
const meta = ruleDef.meta;
9195
return [id, meta];

scripts/helpers/types.d.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
import type { TSESLint } from '@typescript-eslint/utils';
22
import type { Linter, Rule } from 'eslint';
33

4+
export type ConfigName =
5+
| 'javascript'
6+
| 'typescript'
7+
| 'node'
8+
| 'angular'
9+
| 'ngrx'
10+
| 'react'
11+
| 'graphql'
12+
| 'jest'
13+
| 'vitest'
14+
| 'cypress'
15+
| 'playwright'
16+
| 'storybook'
17+
| 'react-testing-library';
18+
419
export type ExportedConfig = {
5-
name: string;
20+
name: ConfigName;
621
flatConfig: TSESLint.FlatConfig.ConfigArray;
722
};
823

src/configs/graphql.js

-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ export default tseslint.config(...node, {
1313
'@graphql-eslint': graphqlEslint,
1414
},
1515
extends: [
16-
// @ts-expect-error rule severity inferred as string from .js
1716
graphqlEslint.flatConfigs['schema-recommended'],
18-
// @ts-expect-error rule severity inferred as string from .js
1917
graphqlEslint.flatConfigs['relay'],
2018
{
2119
name: 'code-pushup/graphql/customized',

src/configs/javascript.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default tseslint.config(
2626
extends: [
2727
eslint.configs.recommended,
2828
...tseslint.configs.recommended,
29-
importPlugin.flatConfigs?.recommended,
29+
importPlugin.flatConfigs.recommended,
3030
sonarjs.configs.recommended,
3131
promise.configs['flat/recommended'],
3232
{

src/configs/react.js

-2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@ export default tseslint.config(...javascript, {
1414
globals: globals.browser,
1515
},
1616
extends: [
17-
// @ts-expect-error types inferred as possibly undefined
1817
react.configs.flat.recommended,
1918
jsxA11y.flatConfigs.recommended,
2019
{
2120
name: 'code-pushup/react/react-hooks',
2221
plugins: {
23-
// @ts-expect-error inferred index signature mismatch
2422
'react-hooks': reactHooks,
2523
},
2624
rules: {

0 commit comments

Comments
 (0)