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:
23
.github/workflows/lint.yml
vendored
23
.github/workflows/lint.yml
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"sourceMap": false,
|
||||
"noEmit": true,
|
||||
"removeComments": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
|
||||
2
packages/frontend-embed/@types/theme.d.ts
vendored
2
packages/frontend-embed/@types/theme.d.ts
vendored
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
declare module '@@/themes/*.json5' {
|
||||
import { Theme } from '@/theme.js';
|
||||
import { Theme } from '@@/js/theme.js';
|
||||
|
||||
const theme: Theme;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"removeComments": false,
|
||||
"skipLibCheck": true,
|
||||
"noLib": false,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"sourceMap": false,
|
||||
"outDir": "./js-built",
|
||||
"removeComments": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
|
||||
2
packages/frontend/@types/theme.d.ts
vendored
2
packages/frontend/@types/theme.d.ts
vendored
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
declare module '@@/themes/*.json5' {
|
||||
import { Theme } from '@/theme.js';
|
||||
import { Theme } from '@@/js/theme.js';
|
||||
|
||||
const theme: Theme;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"sourceMap": false,
|
||||
"outDir": "./built",
|
||||
"removeComments": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"sourceMap": false,
|
||||
"outDir": "./built",
|
||||
"removeComments": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"sourceMap": false,
|
||||
"outDir": "./built",
|
||||
"removeComments": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
@@ -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
198
scripts/check-dts.mjs
Normal 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();
|
||||
}
|
||||
25
scripts/check-dts.test.mjs
Normal file
25
scripts/check-dts.test.mjs
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user