1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-28 18:44:35 +02:00

fix: tsconfig.jsonにskipLibCheck: trueを追加する (#17651)

* fix: tsconfig.jsonにskipLibCheck: trueを追加する

* skipLibCheckで抜けたチェックを自前でやる
This commit is contained in:
おさむのひと
2026-07-03 19:22:23 +09:00
committed by GitHub
parent 5432984af8
commit 9f614517c0
15 changed files with 260 additions and 6 deletions

View File

@@ -17,7 +17,9 @@ on:
- packages/misskey-bubble-game/**
- packages/misskey-reversi/**
- packages/shared/eslint.config.js
- scripts/check-dts*.mjs
- .github/workflows/lint.yml
- package.json
pull_request:
paths:
- packages/backend/**
@@ -31,7 +33,9 @@ on:
- packages/misskey-bubble-game/**
- packages/misskey-reversi/**
- packages/shared/eslint.config.js
- scripts/check-dts*.mjs
- .github/workflows/lint.yml
- package.json
jobs:
pnpm_install:
runs-on: ubuntu-latest
@@ -113,3 +117,22 @@ jobs:
- run: pnpm i --frozen-lockfile
- run: pnpm --filter "${{ matrix.workspace }}^..." run build
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
check-dts:
needs: [pnpm_install]
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.3
- uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- run: node --test scripts/check-dts.test.mjs
- run: pnpm check-dts

View File

@@ -39,7 +39,8 @@
"migrateandstart": "pnpm migrate && pnpm start",
"watch": "pnpm dev",
"dev": "node scripts/dev.mjs",
"lint": "pnpm --no-bail -r lint",
"check-dts": "node scripts/check-dts.mjs",
"lint": "pnpm --no-bail -r lint && pnpm check-dts",
"cy:open": "pnpm cypress open --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run",
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",

View File

@@ -26,7 +26,6 @@ declare module 'probe-image-size' {
function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void;
function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void;
namespace probeImageSize {} // Hack
export = probeImageSize;
// eslint-disable-next-line import/no-default-export
export { probeImageSize as default };
}

View File

@@ -9,6 +9,7 @@
"sourceMap": false,
"noEmit": true,
"removeComments": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"strict": true,
"strictFunctionTypes": true,

View File

@@ -4,7 +4,7 @@
*/
declare module '@@/themes/*.json5' {
import { Theme } from '@/theme.js';
import { Theme } from '@@/js/theme.js';
const theme: Theme;

View File

@@ -13,6 +13,7 @@
"module": "ES2022",
"moduleResolution": "Bundler",
"removeComments": false,
"skipLibCheck": true,
"noLib": false,
"strict": true,
"strictNullChecks": true,

View File

@@ -9,6 +9,7 @@
"sourceMap": false,
"outDir": "./js-built",
"removeComments": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"strict": true,
"strictFunctionTypes": true,

View File

@@ -4,7 +4,7 @@
*/
declare module '@@/themes/*.json5' {
import { Theme } from '@/theme.js';
import { Theme } from '@@/js/theme.js';
const theme: Theme;

View File

@@ -8,6 +8,7 @@
"strictFunctionTypes": true,
"strictNullChecks": true,
"esModuleInterop": true,
"skipLibCheck": true,
"lib": [
"esnext",
"dom"

View File

@@ -9,6 +9,7 @@
"sourceMap": false,
"outDir": "./built",
"removeComments": true,
"skipLibCheck": true,
"strict": true,
"strictFunctionTypes": true,
"strictNullChecks": true,

View File

@@ -9,6 +9,7 @@
"sourceMap": false,
"outDir": "./built",
"removeComments": true,
"skipLibCheck": true,
"strict": true,
"strictFunctionTypes": true,
"strictNullChecks": true,

View File

@@ -9,6 +9,7 @@
"sourceMap": false,
"outDir": "./built",
"removeComments": true,
"skipLibCheck": true,
"strict": true,
"strictFunctionTypes": true,
"strictNullChecks": true,

View File

@@ -13,6 +13,7 @@
"module": "nodenext",
"moduleResolution": "nodenext",
"removeComments": false,
"skipLibCheck": true,
"noLib": false,
"strict": true,
"strictNullChecks": true,

198
scripts/check-dts.mjs Normal file
View File

@@ -0,0 +1,198 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import ts from 'typescript';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const defaultRootDir = path.resolve(scriptDir, '..');
// パス判定は OS 差分を避けるため、必要なところだけ POSIX 形式へ寄せる。
function toPosixPath(filePath) {
return filePath.split(path.sep).join('/');
}
function isInside(parentDir, filePath) {
const relative = path.relative(parentDir, filePath);
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
}
// skipLibCheck で隠したくない、リポジトリ管理下の手書き .d.ts だけを検査する。
export function isCheckableDeclarationFile(filePath, rootDir = defaultRootDir) {
const absoluteRootDir = path.resolve(rootDir);
const absoluteFilePath = path.resolve(filePath);
if (!isInside(absoluteRootDir, absoluteFilePath)) return false;
if (!absoluteFilePath.endsWith('.d.ts')) return false;
const relativePath = toPosixPath(path.relative(absoluteRootDir, absoluteFilePath));
const segments = relativePath.split('/');
if (segments.includes('node_modules') || segments.includes('.pnpm')) return false;
if (segments.includes('built') || segments.includes('js-built')) return false;
return true;
}
// root package.json の workspaces を信頼して、検査対象 package を列挙する。
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function getWorkspacePackageDirs(rootDir) {
const rootPackageJson = readJson(path.join(rootDir, 'package.json'));
return rootPackageJson.workspaces
.filter((workspace) => !workspace.includes('*'))
.map((workspace) => path.resolve(rootDir, workspace))
.filter((workspaceDir) => fs.existsSync(path.join(workspaceDir, 'package.json')));
}
function getTsconfigPaths(packageDir) {
return ts.sys.readDirectory(
packageDir,
['.json'],
['node_modules'],
['**/tsconfig.json'],
undefined,
);
}
// tsconfig の include から漏れる package 直下の shim も拾えるよう、補助的に探索する。
function discoverDeclarationFiles(packageDir, rootDir) {
return ts.sys.readDirectory(
packageDir,
['.d.ts'],
undefined,
['**/*.d.ts'],
undefined,
).filter((filePath) => isCheckableDeclarationFile(filePath, rootDir));
}
// 各 package の設定をそのまま使い、検査時だけ skipLibCheck を後で上書きする。
function readTsconfig(tsconfigPath) {
if (!fs.existsSync(tsconfigPath)) return undefined;
const config = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
if (config.error != null) {
throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'));
}
return ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(tsconfigPath));
}
function createFormatHost(rootDir) {
return {
getCanonicalFileName: (fileName) => fileName,
getCurrentDirectory: () => rootDir,
getNewLine: () => ts.sys.newLine,
};
}
// 通常の tsconfig は include 対象を使い、root tsconfig だけ package 直下の shim を足す。
function isRootTsconfig(packageDir, tsconfigPath) {
return path.resolve(packageDir, 'tsconfig.json') === path.resolve(tsconfigPath);
}
function getDeclarationFilesForTsconfig(packageDir, tsconfigPath, parsedConfig, rootDir) {
const declarationFiles = parsedConfig.fileNames
.filter((filePath) => isCheckableDeclarationFile(filePath, rootDir));
if (!isRootTsconfig(packageDir, tsconfigPath)) {
return declarationFiles;
}
const extraRootConfigDeclarationFiles = discoverDeclarationFiles(packageDir, rootDir)
.filter((filePath) => {
return path.dirname(filePath) === packageDir;
});
return [...new Set([...declarationFiles, ...extraRootConfigDeclarationFiles])];
}
// node_modules 側の診断は出さず、対象 .d.ts 自身に出た診断だけを失敗扱いにする。
function checkTsconfigDeclarations(packageDir, tsconfigPath, rootDir) {
const parsedConfig = readTsconfig(tsconfigPath);
if (parsedConfig == null) {
return { declarationFiles: [], diagnostics: [] };
}
const declarationFiles = getDeclarationFilesForTsconfig(packageDir, tsconfigPath, parsedConfig, rootDir);
if (declarationFiles.length === 0) {
return { declarationFiles, diagnostics: [] };
}
const program = ts.createProgram({
rootNames: declarationFiles,
options: {
...parsedConfig.options,
noEmit: true,
skipLibCheck: false,
},
});
const diagnostics = ts.getPreEmitDiagnostics(program)
.filter((diagnostic) => diagnostic.file != null && isCheckableDeclarationFile(diagnostic.file.fileName, rootDir));
return { declarationFiles, diagnostics };
}
// 複数 tsconfig で同じ宣言を読むことがあるため、同一診断をまとめる。
function deduplicateDiagnostics(diagnostics) {
const seen = new Set();
return diagnostics.filter((diagnostic) => {
const key = [
diagnostic.file?.fileName ?? '',
diagnostic.start ?? '',
diagnostic.code,
ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
].join('\0');
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function checkPackageDeclarations(packageDir, rootDir) {
const results = getTsconfigPaths(packageDir)
.map((tsconfigPath) => checkTsconfigDeclarations(packageDir, tsconfigPath, rootDir));
return {
declarationFiles: [...new Set(results.flatMap((result) => result.declarationFiles))],
diagnostics: deduplicateDiagnostics(results.flatMap((result) => result.diagnostics)),
};
}
// workspace 全体を走査し、CI が扱いやすい件数と診断リストに集約する。
export function checkDeclarations(rootDir = defaultRootDir) {
const workspacePackageDirs = getWorkspacePackageDirs(rootDir);
const results = workspacePackageDirs.map((packageDir) => ({
packageDir,
...checkPackageDeclarations(packageDir, rootDir),
}));
return {
results,
declarationFileCount: results.reduce((count, result) => count + result.declarationFiles.length, 0),
diagnostics: results.flatMap((result) => result.diagnostics),
};
}
// CLI 実行時は TypeScript 標準の formatter で、通常の tsc に近い形で表示する。
function main() {
const { declarationFileCount, diagnostics } = checkDeclarations(defaultRootDir);
if (diagnostics.length === 0) {
console.log(`Checked ${declarationFileCount} repository declaration files.`);
return;
}
console.error(ts.formatDiagnosticsWithColorAndContext(diagnostics, createFormatHost(defaultRootDir)));
process.exitCode = 1;
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main();
}

View File

@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import assert from 'node:assert/strict';
import test from 'node:test';
import { isCheckableDeclarationFile } from './check-dts.mjs';
const rootDir = '/repo';
test('detects repository-owned declaration files that should be checked', () => {
assert.equal(isCheckableDeclarationFile(`${rootDir}/packages/frontend/@types/theme.d.ts`, rootDir), true);
assert.equal(isCheckableDeclarationFile(`${rootDir}/packages/frontend/src/utility/virtual.d.ts`, rootDir), true);
assert.equal(isCheckableDeclarationFile(`${rootDir}/packages/backend/test/global.d.ts`, rootDir), true);
});
test('ignores declarations outside the repository-owned surface', () => {
assert.equal(isCheckableDeclarationFile(`${rootDir}/node_modules/@types/node/index.d.ts`, rootDir), false);
assert.equal(isCheckableDeclarationFile(`${rootDir}/packages/frontend/node_modules/@types/foo/index.d.ts`, rootDir), false);
assert.equal(isCheckableDeclarationFile(`${rootDir}/node_modules/.pnpm/typescript/lib/lib.dom.d.ts`, rootDir), false);
assert.equal(isCheckableDeclarationFile(`${rootDir}/packages/misskey-js/built/index.d.ts`, rootDir), false);
assert.equal(isCheckableDeclarationFile(`${rootDir}/packages/frontend-shared/js-built/i18n.d.ts`, rootDir), false);
assert.equal(isCheckableDeclarationFile(`${rootDir}/packages/frontend/src/theme.ts`, rootDir), false);
});