1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-27 17:14:35 +02:00

Merge branch 'develop' into room

This commit is contained in:
syuilo
2026-07-27 12:58:26 +09:00
482 changed files with 24972 additions and 13601 deletions

View File

@@ -6,7 +6,7 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as yaml from 'js-yaml';
import { load as loadYaml } from 'js-yaml';
import { buildTarball } from './tarball.mjs';
const configDir = fileURLToPath(new URL('../.config', import.meta.url));
@@ -17,16 +17,11 @@ const configPath = process.env.MISSKEY_CONFIG_YML
: path.resolve(configDir, 'default.yml');
async function loadConfig() {
return fs.readFile(configPath, 'utf-8').then(data => yaml.load(data)).catch(() => null);
}
async function copyFrontendFonts() {
await fs.cp('./packages/frontend/node_modules/three/examples/fonts', './built/_frontend_dist_/fonts', { dereference: true, recursive: true });
return fs.readFile(configPath, 'utf-8').then(data => loadYaml(data)).catch(() => null);
}
async function build() {
await Promise.all([
copyFrontendFonts(),
loadConfig().then(config => config?.publishTarballInsteadOfProvideRepositoryUrl && buildTarball()),
]);
}

View File

@@ -1,3 +0,0 @@
node_modules
coverage
.idea

View File

@@ -1,17 +0,0 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../../packages/shared/eslint.config.js';
export default [
...sharedConfig,
{
files: ['src/**/*.ts', 'src/**/*.tsx'],
languageOptions: {
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
{
"name": "changelog-checker",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"run": "vite-node src/index.ts",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@types/mdast": "4.0.4",
"@types/node": "26.0.0",
"@vitest/coverage-v8": "4.1.9",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "5.9.3",
"unified": "11.0.5",
"vite": "8.1.0",
"vite-node": "6.0.0",
"vitest": "4.1.9"
}
}

View File

@@ -1,92 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Release } from './parser.js';
export class Result {
public readonly success: boolean;
public readonly message?: string;
private constructor(success: boolean, message?: string) {
this.success = success;
this.message = message;
}
static ofSuccess(): Result {
return new Result(true);
}
static ofFailed(message?: string): Result {
return new Result(false, message);
}
}
/**
* develop -> masterまたはrelease -> masterを想定したパターン。
* base側の先頭とhead側で追加された分のリリースより1つ前のバージョンが等価であるかチェックする。
*/
export function checkNewRelease(base: Release[], head: Release[]): Result {
const releaseCountDiff = head.length - base.length;
if (releaseCountDiff <= 0) {
return Result.ofFailed('Invalid release count.');
}
const baseLatest = base[0];
const headPrevious = head[releaseCountDiff];
if (baseLatest.releaseName !== headPrevious.releaseName) {
return Result.ofFailed('Contains unexpected releases.');
}
return Result.ofSuccess();
}
/**
* topic -> developまたはtopic -> masterを想定したパターン。
* head側の最新リリース配下に書き加えられているかをチェックする。
*/
export function checkNewTopic(base: Release[], head: Release[]): Result {
if (head.length !== base.length) {
return Result.ofFailed('Invalid release count.');
}
const headLatest = head[0];
for (let relIdx = 0; relIdx < base.length; relIdx++) {
const baseItem = base[relIdx];
const headItem = head[relIdx];
if (baseItem.releaseName !== headItem.releaseName) {
// リリースの順番が変わってると成立しないのでエラーにする
return Result.ofFailed(`Release is different. base:${baseItem.releaseName}, head:${headItem.releaseName}`);
}
if (baseItem.categories.length !== headItem.categories.length) {
// カテゴリごと書き加えられたパターン
if (headLatest.releaseName !== headItem.releaseName) {
// 最新リリース以外に追記されていた場合
return Result.ofFailed(`There is an error in the update history. expected additions:${headLatest.releaseName}, actual additions:${headItem.releaseName}`);
}
} else {
// カテゴリ数の変動はないのでリスト項目の数をチェック
for (let catIdx = 0; catIdx < baseItem.categories.length; catIdx++) {
const baseCategory = baseItem.categories[catIdx];
const headCategory = headItem.categories[catIdx];
if (baseCategory.categoryName !== headCategory.categoryName) {
// カテゴリの順番が変わっていると成立しないのでエラーにする
return Result.ofFailed(`Category is different. base:${baseCategory.categoryName}, head:${headCategory.categoryName}`);
}
if (baseCategory.items.length !== headCategory.items.length) {
if (headLatest.releaseName !== headItem.releaseName) {
// 最新リリース以外に追記されていた場合
return Result.ofFailed(`There is an error in the update history. expected additions:${headLatest.releaseName}, actual additions:${headItem.releaseName}`);
}
}
}
}
}
return Result.ofSuccess();
}

View File

@@ -1,38 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as process from 'process';
import * as fs from 'fs';
import { parseChangeLog } from './parser.js';
import { checkNewRelease, checkNewTopic } from './checker.js';
function abort(message?: string) {
if (message) {
console.error(message);
}
process.exit(1);
}
function main() {
if (!fs.existsSync('./CHANGELOG-base.md') || !fs.existsSync('./CHANGELOG-head.md')) {
console.error('CHANGELOG-base.md or CHANGELOG-head.md is missing.');
return;
}
const base = parseChangeLog('./CHANGELOG-base.md');
const head = parseChangeLog('./CHANGELOG-head.md');
const result = (base.length < head.length)
? checkNewRelease(base, head)
: checkNewTopic(base, head);
if (!result.success) {
abort(result.message);
return;
}
}
main();

View File

@@ -1,67 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import { Heading, List, Node } from 'mdast';
import { toString } from 'mdast-util-to-string';
export class Release {
public readonly releaseName: string;
public readonly categories: ReleaseCategory[];
constructor(releaseName: string, categories: ReleaseCategory[] = []) {
this.releaseName = releaseName;
this.categories = [...categories];
}
}
export class ReleaseCategory {
public readonly categoryName: string;
public readonly items: string[];
constructor(categoryName: string, items: string[] = []) {
this.categoryName = categoryName;
this.items = [...items];
}
}
function isHeading(node: Node): node is Heading {
return node.type === 'heading';
}
function isList(node: Node): node is List {
return node.type === 'list';
}
export function parseChangeLog(path: string): Release[] {
const input = fs.readFileSync(path, { encoding: 'utf8' });
const processor = unified().use(remarkParse);
const releases: Release[] = [];
const root = processor.parse(input);
let release: Release | null = null;
let category: ReleaseCategory | null = null;
for (const it of root.children) {
if (isHeading(it) && it.depth === 2) {
// リリース
release = new Release(toString(it));
releases.push(release);
} else if (isHeading(it) && it.depth === 3 && release) {
// リリース配下のカテゴリ
category = new ReleaseCategory(toString(it));
release.categories.push(category);
} else if (isList(it) && category) {
for (const listItem of it.children) {
// カテゴリ配下のリスト項目
category.items.push(toString(listItem));
}
}
}
return releases;
}

View File

@@ -1,419 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {expect, suite, test} from "vitest";
import {Release, ReleaseCategory} from "../src/parser";
import {checkNewRelease, checkNewTopic} from "../src/checker";
suite('checkNewRelease', () => {
test('headに新しいリリースがある1', () => {
const base = [new Release('2024.12.0')]
const head = [new Release('2024.12.1'), new Release('2024.12.0')]
const result = checkNewRelease(base, head)
expect(result.success).toBe(true)
})
test('headに新しいリリースがある2', () => {
const base = [new Release('2024.12.0')]
const head = [new Release('2024.12.2'), new Release('2024.12.1'), new Release('2024.12.0')]
const result = checkNewRelease(base, head)
expect(result.success).toBe(true)
})
test('リリースの数が同じ', () => {
const base = [new Release('2024.12.0')]
const head = [new Release('2024.12.0')]
const result = checkNewRelease(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
test('baseにあるリリースがheadにない', () => {
const base = [new Release('2024.12.0')]
const head = [new Release('2024.12.2'), new Release('2024.12.1')]
const result = checkNewRelease(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
})
suite('checkNewTopic', () => {
test('追記なし', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const result = checkNewTopic(base, head)
expect(result.success).toBe(true)
})
test('最新バージョンにカテゴリを追加したときはエラーにならない', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const result = checkNewTopic(base, head)
expect(result.success).toBe(true)
})
test('最新バージョンからカテゴリを削除したときはエラーにならない', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const result = checkNewTopic(base, head)
expect(result.success).toBe(true)
})
test('最新バージョンに追記したときはエラーにならない', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
'feat3',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const result = checkNewTopic(base, head)
expect(result.success).toBe(true)
})
test('最新バージョンから削除したときはエラーにならない', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const result = checkNewTopic(base, head)
expect(result.success).toBe(true)
})
test('古いバージョンにカテゴリを追加したときはエラーになる', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat1',
'feat2',
]),
])
]
const result = checkNewTopic(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
test('古いバージョンからカテゴリを削除したときはエラーになる', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
])
]
const result = checkNewTopic(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
test('古いバージョンに追記したときはエラーになる', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
'feat3',
]),
])
]
const result = checkNewTopic(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
test('古いバージョンから削除したときはエラーになる', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
]),
])
]
const result = checkNewTopic(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
})

View File

@@ -1,31 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"strict": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"esModuleInterop": true,
"typeRoots": [
"./node_modules/@types"
],
"lib": [
"esnext"
]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"test/**/*"
]
}

View File

@@ -1,6 +0,0 @@
import {defineConfig} from 'vite';
const config = defineConfig({});
export default config;

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);
});

View File

@@ -10,104 +10,268 @@ import { execa } from 'execa';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
await execa('pnpm', ['clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
/** @type {Set<import('execa').ResultPromise>} */
const childProcesses = new Set();
/** @type {Set<import('execa').ResultPromise>} */
const persistentChildProcesses = new Set();
let shuttingDown = false;
let persistentChildProcessesStarted = false;
let persistentChildProcessFailed = false;
/** @type {Promise<void> | null} */
let shutdownPromise = null;
/**
* 開発用コマンドを起動し、終了時にまとめて停止できるよう追跡する。
* Windows では Ctrl+C の配信先を分離し、出力を親コンソールへ中継する。
*
* @param {string} command - 実行するコマンド。
* @param {string[]} args - コマンドへ渡す引数。
* @param {import('execa').Options} options - execa の起動オプション。
* @returns {import('execa').ResultPromise} 起動した子プロセス。
*/
function spawnChildProcess(command, args, options) {
const isWindows = process.platform === 'win32';
const shouldForceColor = (process.stdout.isTTY || process.stderr.isTTY)
&& process.env.FORCE_COLOR == null
&& process.env.NO_COLOR == null;
const pnpmPath = _dirname + '/../node_modules/pnpm/bin/pnpm.mjs';
const windowsCommand = [process.execPath, pnpmPath, ...args]
.map(argument => `"${argument}"`)
.join(' ');
const executable = isWindows && command === 'pnpm' ? process.env.ComSpec ?? 'cmd.exe' : command;
const executableArgs = isWindows && command === 'pnpm'
? ['/d', '/s', '/c', `start "" /b /wait ${windowsCommand}`]
: args;
const childProcess = execa(executable, executableArgs, isWindows ? {
...options,
// `start /b` keeps the process in the current console without forwarding
// Ctrl+C, allowing only this supervisor to coordinate the shutdown.
windowsVerbatimArguments: true,
windowsHide: false,
env: shouldForceColor ? { FORCE_COLOR: '1' } : undefined,
stdout: 'pipe',
stderr: 'pipe',
buffer: false,
} : options);
if (isWindows) {
if (options.stdout != null) childProcess.stdout?.pipe(options.stdout, { end: false });
if (options.stderr != null) childProcess.stderr?.pipe(options.stderr, { end: false });
}
childProcesses.add(childProcess);
return childProcess;
}
/**
* 子プロセスの終了を待機し、追跡対象から取り除く。
*
* @param {string} command - 実行するコマンド。
* @param {string[]} args - コマンドへ渡す引数。
* @param {import('execa').Options} options - execa の起動オプション。
* @returns {Promise<import('execa').Result>} 子プロセスの実行結果。
*/
async function runChildProcess(command, args, options) {
const childProcess = spawnChildProcess(command, args, options);
try {
return await childProcess;
} finally {
childProcesses.delete(childProcess);
}
}
/**
* 常駐する子プロセスがすべて終了していれば、終了結果を引き継いで親も終了する。
*
* @returns {void}
*/
function shutdownIfAllPersistentChildProcessesStopped() {
if (shuttingDown || !persistentChildProcessesStarted || persistentChildProcesses.size > 0) return;
void shutdown(persistentChildProcessFailed ? 1 : 0);
}
/**
* 常駐する子プロセスを起動し、終了時の追跡解除・エラー出力・親の終了判定を設定する。
*
* @param {string} command - 実行するコマンド。
* @param {string[]} args - コマンドへ渡す引数。
* @param {import('execa').Options} options - execa の起動オプション。
* @returns {void}
*/
function startChildProcess(command, args, options) {
const childProcess = spawnChildProcess(command, args, options);
persistentChildProcesses.add(childProcess);
void childProcess.then(() => {
childProcesses.delete(childProcess);
persistentChildProcesses.delete(childProcess);
shutdownIfAllPersistentChildProcessesStopped();
}, error => {
childProcesses.delete(childProcess);
persistentChildProcesses.delete(childProcess);
if (!shuttingDown) {
persistentChildProcessFailed = true;
console.error(error);
shutdownIfAllPersistentChildProcessesStopped();
}
});
}
/**
* 子プロセスとその配下のプロセスを停止し、終了を待機する。
*
* @param {import('execa').ResultPromise} childProcess - 停止する子プロセス。
* @returns {Promise<void>}
*/
async function stopChildProcess(childProcess) {
if (process.platform === 'win32' && childProcess.pid != null) {
const result = await execa('taskkill', ['/pid', childProcess.pid.toString(), '/t', '/f'], {
reject: false,
});
if (result.failed) childProcess.kill();
} else {
childProcess.kill();
}
await childProcess.catch(() => {});
}
/**
* 追跡中の子プロセスを一度だけ停止して、指定した終了コードで終了する。
*
* @param {number} exitCode - 親プロセスに返す終了コード。
* @returns {Promise<void>} 実行中または完了した停止処理。
*/
function shutdown(exitCode) {
if (shutdownPromise != null) return shutdownPromise;
shuttingDown = true;
shutdownPromise = (async () => {
await Promise.allSettled([...childProcesses].map(stopChildProcess));
process.exit(exitCode);
})();
return shutdownPromise;
}
process.on('SIGINT', () => {
void shutdown(0);
});
// アセットのビルドで依存しているので一番最初に必要
await execa('pnpm', ['--filter', 'i18n', 'build'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
process.on('SIGTERM', () => {
void shutdown(0);
});
await Promise.all([
execa('pnpm', ['build-pre'], {
try {
await runChildProcess('pnpm', ['clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
}),
execa('pnpm', ['build-assets'], {
});
// アセットのビルドで依存しているので一番最初に必要
await runChildProcess('pnpm', ['--filter', 'i18n', 'build'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
}),
execa('pnpm', ['--filter', 'backend...', '--filter=!backend', 'build'], {
});
await Promise.all([
runChildProcess('pnpm', ['build-pre'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
}),
runChildProcess('pnpm', ['build-assets'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
}),
runChildProcess('pnpm', ['--filter', 'backend...', '--filter=!backend', 'build'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
}),
// icons-subsetterは開発段階では使用されないが、型エラーを抑制するためにはじめの一度だけビルドする
runChildProcess('pnpm', ['--filter', 'icons-subsetter', 'build'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
}),
runChildProcess('pnpm', ['--filter', 'misskey-js', 'build'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
}),
]);
startChildProcess('pnpm', ['build-pre', '--watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
}),
// icons-subsetterは開発段階では使用されないが、型エラーを抑制するためにはじめの一度だけビルドする
execa('pnpm', ['--filter', 'icons-subsetter', 'build'], {
});
startChildProcess('pnpm', ['build-assets', '--watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
}),
execa('pnpm', ['--filter', 'misskey-js', 'build'], {
});
startChildProcess('pnpm', ['--filter', 'backend', 'dev'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
}),
]);
});
execa('pnpm', ['build-pre', '--watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
startChildProcess('pnpm', ['--filter', 'frontend', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
execa('pnpm', ['build-assets', '--watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
startChildProcess('pnpm', ['--filter', 'frontend-embed', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
execa('pnpm', ['--filter', 'backend', 'dev'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
startChildProcess('pnpm', ['--filter', 'sw', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
execa('pnpm', ['--filter', 'frontend', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
startChildProcess('pnpm', ['--filter', 'misskey-js', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
execa('pnpm', ['--filter', 'frontend-embed', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
startChildProcess('pnpm', ['--filter', 'i18n', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
execa('pnpm', ['--filter', 'sw', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
startChildProcess('pnpm', ['--filter', 'misskey-reversi', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
execa('pnpm', ['--filter', 'misskey-js', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
startChildProcess('pnpm', ['--filter', 'misskey-bubble-game', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
execa('pnpm', ['--filter', 'i18n', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
execa('pnpm', ['--filter', 'misskey-reversi', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
execa('pnpm', ['--filter', 'misskey-bubble-game', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
persistentChildProcessesStarted = true;
shutdownIfAllPersistentChildProcessesStopped();
} catch (error) {
if (!shuttingDown) {
console.error(error);
await shutdown(1);
}
}