diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d80c56f032..33a1ccbc76 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -111,10 +111,5 @@ jobs: node-version-file: '.node-version' cache: 'pnpm' - run: pnpm i --frozen-lockfile - - run: pnpm --filter misskey-js run build - if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'frontend' || matrix.workspace == 'sw' }} - - run: pnpm --filter misskey-reversi run build - if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'frontend' }} - - run: pnpm --filter misskey-bubble-game run build - if: ${{ matrix.workspace == 'frontend' }} + - run: pnpm --filter "${{ matrix.workspace }}^..." run build - run: pnpm --filter ${{ matrix.workspace }} run typecheck diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml index e63d83997b..d75335f38f 100644 --- a/.github/workflows/locale.yml +++ b/.github/workflows/locale.yml @@ -3,10 +3,12 @@ name: Lint on: push: paths: + - packages/i18n/** - locales/** - .github/workflows/locale.yml pull_request: paths: + - packages/i18n/** - locales/** - .github/workflows/locale.yml jobs: @@ -14,15 +16,18 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v4.3.0 - with: - fetch-depth: 0 - submodules: true - - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 - - uses: actions/setup-node@v4.4.0 - with: - node-version-file: '.node-version' - cache: 'pnpm' - - run: pnpm i --frozen-lockfile - - run: cd locales && node verify.js + - uses: actions/checkout@v4.3.0 + with: + fetch-depth: 0 + submodules: true + - name: Setup pnpm + uses: pnpm/action-setup@v4.2.0 + - uses: actions/setup-node@v4.4.0 + with: + node-version-file: ".node-version" + cache: "pnpm" + - run: pnpm i --frozen-lockfile + - run: pnpm --filter i18n build + - name: Verify Locales + working-directory: ./packages/i18n + run: pnpm run verify diff --git a/Dockerfile b/Dockerfile index 20e24d1dc2..a071970927 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-share COPY --link ["packages/frontend/package.json", "./packages/frontend/"] COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"] COPY --link ["packages/frontend-builder/package.json", "./packages/frontend-builder/"] +COPY --link ["packages/i18n/package.json", "./packages/i18n/"] COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"] COPY --link ["packages/sw/package.json", "./packages/sw/"] COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] @@ -101,6 +102,7 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built +COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis COPY --chown=misskey:misskey . ./ diff --git a/locales/generateDTS.js b/locales/generateDTS.js deleted file mode 100644 index ab0613cc82..0000000000 --- a/locales/generateDTS.js +++ /dev/null @@ -1,232 +0,0 @@ -import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import * as yaml from 'js-yaml'; -import ts from 'typescript'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const parameterRegExp = /\{(\w+)\}/g; - -function createMemberType(item) { - if (typeof item !== 'string') { - return ts.factory.createTypeLiteralNode(createMembers(item)); - } - const parameters = Array.from( - item.matchAll(parameterRegExp), - ([, parameter]) => parameter, - ); - return parameters.length - ? ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('ParameterizedString'), - [ - ts.factory.createUnionTypeNode( - parameters.map((parameter) => - ts.factory.createStringLiteral(parameter), - ), - ), - ], - ) - : ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); -} - -function createMembers(record) { - return Object.entries(record).map(([k, v]) => { - const node = ts.factory.createPropertySignature( - undefined, - ts.factory.createStringLiteral(k), - undefined, - createMemberType(v), - ); - if (typeof v === 'string') { - ts.addSyntheticLeadingComment( - node, - ts.SyntaxKind.MultiLineCommentTrivia, - `* - * ${v.replace(/\n/g, '\n * ')} - `, - true, - ); - } - return node; - }); -} - -export default function generateDTS() { - const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); - const members = createMembers(locale); - const elements = [ - ts.factory.createVariableStatement( - [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier('kParameters'), - undefined, - ts.factory.createTypeOperatorNode( - ts.SyntaxKind.UniqueKeyword, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword), - ), - undefined, - ), - ], - ts.NodeFlags.Const, - ), - ), - ts.factory.createTypeAliasDeclaration( - [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier('ParameterizedString'), - [ - ts.factory.createTypeParameterDeclaration( - undefined, - ts.factory.createIdentifier('T'), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ), - ], - ts.factory.createIntersectionTypeNode([ - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ts.factory.createTypeLiteralNode([ - ts.factory.createPropertySignature( - undefined, - ts.factory.createComputedPropertyName( - ts.factory.createIdentifier('kParameters'), - ), - undefined, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('T'), - undefined, - ), - ), - ]) - ]), - ), - ts.factory.createInterfaceDeclaration( - [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier('ILocale'), - undefined, - undefined, - [ - ts.factory.createIndexSignature( - undefined, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier('_'), - undefined, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - undefined, - ), - ], - ts.factory.createUnionTypeNode([ - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('ParameterizedString'), - ), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('ILocale'), - undefined, - ), - ]), - ), - ], - ), - ts.factory.createInterfaceDeclaration( - [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier('Locale'), - undefined, - [ - ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ - ts.factory.createExpressionWithTypeArguments( - ts.factory.createIdentifier('ILocale'), - undefined, - ), - ]), - ], - members, - ), - ts.factory.createVariableStatement( - [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier('locales'), - undefined, - ts.factory.createTypeLiteralNode([ - ts.factory.createIndexSignature( - undefined, - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier('lang'), - undefined, - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.StringKeyword, - ), - undefined, - ), - ], - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('Locale'), - undefined, - ), - ), - ]), - undefined, - ), - ], - ts.NodeFlags.Const, - ), - ), - ts.factory.createFunctionDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - undefined, - ts.factory.createIdentifier('build'), - undefined, - [], - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('Locale'), - undefined, - ), - undefined, - ), - ts.factory.createExportDefault(ts.factory.createIdentifier('locales')), - ]; - ts.addSyntheticLeadingComment( - elements[0], - ts.SyntaxKind.MultiLineCommentTrivia, - ' eslint-disable ', - true, - ); - ts.addSyntheticLeadingComment( - elements[0], - ts.SyntaxKind.SingleLineCommentTrivia, - ' This file is generated by locales/generateDTS.js', - true, - ); - ts.addSyntheticLeadingComment( - elements[0], - ts.SyntaxKind.SingleLineCommentTrivia, - ' Do not edit this file directly.', - true, - ); - const printed = ts - .createPrinter({ - newLine: ts.NewLineKind.LineFeed, - }) - .printList( - ts.ListFormat.MultiLine, - ts.factory.createNodeArray(elements), - ts.createSourceFile( - 'index.d.ts', - '', - ts.ScriptTarget.ESNext, - true, - ts.ScriptKind.TS, - ), - ); - - fs.writeFileSync(`${__dirname}/index.d.ts`, printed, 'utf-8'); -} diff --git a/locales/index.js b/locales/index.js deleted file mode 100644 index 6d9cf4796b..0000000000 --- a/locales/index.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Languages Loader - */ - -import * as fs from 'node:fs'; -import * as yaml from 'js-yaml'; - -const merge = (...args) => args.reduce((a, c) => ({ - ...a, - ...c, - ...Object.entries(a) - .filter(([k]) => c && typeof c[k] === 'object') - .reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {}) -}), {}); - -const languages = [ - 'ar-SA', - 'ca-ES', - 'cs-CZ', - 'da-DK', - 'de-DE', - 'en-US', - 'es-ES', - 'fr-FR', - 'id-ID', - 'it-IT', - 'ja-JP', - 'ja-KS', - 'kab-KAB', - 'kn-IN', - 'ko-KR', - 'nl-NL', - 'no-NO', - 'pl-PL', - 'pt-PT', - 'ru-RU', - 'sk-SK', - 'th-TH', - 'tr-TR', - 'ug-CN', - 'uk-UA', - 'vi-VN', - 'zh-CN', - 'zh-TW', -]; - -const primaries = { - 'en': 'US', - 'ja': 'JP', - 'zh': 'CN', -}; - -// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く -const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); - -export function build() { - // vitestの挙動を調整するため、一度ローカル変数化する必要がある - // https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577 - // https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785 - const metaUrl = import.meta.url; - const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {}); - - // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す - const removeEmpty = (obj) => { - for (const [k, v] of Object.entries(obj)) { - if (v === '') { - delete obj[k]; - } else if (typeof v === 'object') { - removeEmpty(v); - } - } - return obj; - }; - removeEmpty(locales); - - return Object.entries(locales) - .reduce((a, [k, v]) => (a[k] = (() => { - const [lang] = k.split('-'); - switch (k) { - case 'ja-JP': return v; - case 'ja-KS': - case 'en-US': return merge(locales['ja-JP'], v); - default: return merge( - locales['ja-JP'], - locales['en-US'], - locales[`${lang}-${primaries[lang]}`] ?? {}, - v - ); - } - })(), a), {}); -} - -export default build(); diff --git a/locales/package.json b/locales/package.json deleted file mode 100644 index bedb411a91..0000000000 --- a/locales/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/locales/verify.js b/locales/verify.js deleted file mode 100644 index a8e9875d6e..0000000000 --- a/locales/verify.js +++ /dev/null @@ -1,53 +0,0 @@ -import locales from './index.js'; - -let valid = true; - -function writeError(type, lang, tree, data) { - process.stderr.write(JSON.stringify({ type, lang, tree, data })); - process.stderr.write('\n'); - valid = false; -} - -function verify(expected, actual, lang, trace) { - for (let key in expected) { - if (!Object.prototype.hasOwnProperty.call(actual, key)) { - continue; - } - if (typeof expected[key] === 'object') { - if (typeof actual[key] !== 'object') { - writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'object', actual: typeof actual[key] }); - continue; - } - verify(expected[key], actual[key], lang, trace ? `${trace}.${key}` : key); - } else if (typeof expected[key] === 'string') { - switch (typeof actual[key]) { - case 'object': - writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'string', actual: 'object' }); - break; - case 'undefined': - continue; - case 'string': - const expectedParameters = new Set(expected[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1))); - const actualParameters = new Set(actual[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1))); - for (let parameter of expectedParameters) { - if (!actualParameters.has(parameter)) { - writeError('missing_parameter', lang, trace ? `${trace}.${key}` : key, { parameter }); - } - } - } - } - } -} - -const { ['ja-JP']: original, ...verifiees } = locales; - -for (let lang in verifiees) { - if (!Object.prototype.hasOwnProperty.call(locales, lang)) { - continue; - } - verify(original, verifiees[lang], lang); -} - -if (!valid) { - process.exit(1); -} diff --git a/package.json b/package.json index 9649d7fa6d..394fbf6101 100644 --- a/package.json +++ b/package.json @@ -8,15 +8,17 @@ }, "packageManager": "pnpm@10.22.0", "workspaces": [ - "packages/frontend-shared", - "packages/frontend", - "packages/frontend-embed", - "packages/icons-subsetter", - "packages/backend", - "packages/sw", "packages/misskey-js", + "packages/i18n", "packages/misskey-reversi", - "packages/misskey-bubble-game" + "packages/misskey-bubble-game", + "packages/icons-subsetter", + "packages/frontend-shared", + "packages/frontend-builder", + "packages/sw", + "packages/backend", + "packages/frontend", + "packages/frontend-embed" ], "private": true, "scripts": { @@ -69,6 +71,7 @@ }, "devDependencies": { "@eslint/js": "9.39.1", + "i18n": "workspace:*", "@misskey-dev/eslint-plugin": "2.2.0", "@types/js-yaml": "4.0.9", "@types/node": "24.10.1", diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 5ec362fb34..4776d0d412 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -10,8 +10,6 @@ import * as os from 'node:os'; import cluster from 'node:cluster'; import chalk from 'chalk'; import chalkTemplate from 'chalk-template'; -import * as Sentry from '@sentry/node'; -import { nodeProfilingIntegration } from '@sentry/profiling-node'; import Logger from '@/logger.js'; import { loadConfig } from '@/config.js'; import type { Config } from '@/config.js'; @@ -74,6 +72,9 @@ export async function masterMain() { bootLogger.succ('Misskey initialized'); if (config.sentryForBackend) { + const Sentry = await import('@sentry/node'); + const { nodeProfilingIntegration } = await import('@sentry/profiling-node'); + Sentry.init({ integrations: [ ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []), diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts index 5d4a15b29f..3feb6fd199 100644 --- a/packages/backend/src/boot/worker.ts +++ b/packages/backend/src/boot/worker.ts @@ -4,8 +4,6 @@ */ import cluster from 'node:cluster'; -import * as Sentry from '@sentry/node'; -import { nodeProfilingIntegration } from '@sentry/profiling-node'; import { envOption } from '@/env.js'; import { loadConfig } from '@/config.js'; import { jobQueue, server } from './common.js'; @@ -17,6 +15,9 @@ export async function workerMain() { const config = loadConfig(); if (config.sentryForBackend) { + const Sentry = await import('@sentry/node'); + const { nodeProfilingIntegration } = await import('@sentry/profiling-node'); + Sentry.init({ integrations: [ ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []), diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index eeade4569b..310ffec7ce 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -202,7 +202,7 @@ export class NotificationService implements OnApplicationShutdown { } // TODO - //const locales = await import('../../../../locales/index.js'); + //const locales = await import('i18n'); // TODO: locale ファイルをクライアント用とサーバー用で分けたい @@ -271,7 +271,7 @@ export class NotificationService implements OnApplicationShutdown { let untilTime = untilId ? this.toXListId(untilId) : null; let notifications: MiNotification[]; - for (;;) { + for (; ;) { let notificationsRes: [id: string, fields: string[]][]; // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照 diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 642d3fc8ad..306fdb41f6 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -5,7 +5,6 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; -import * as Sentry from '@sentry/node'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; @@ -157,6 +156,13 @@ export class QueueProcessorService implements OnApplicationShutdown { }; } + let Sentry: typeof import('@sentry/node') | undefined; + if (Sentry != null) { + import('@sentry/node').then((mod) => { + Sentry = mod; + }); + } + //#region system { const processer = (job: Bull.Job) => { @@ -175,7 +181,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }; this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { - if (this.config.sentryForBackend) { + if (Sentry != null) { return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job)); } else { return processer(job); @@ -192,7 +198,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err: Error) => { logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (config.sentryForBackend) { + if (Sentry != null) { Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, @@ -232,7 +238,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }; this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { - if (this.config.sentryForBackend) { + if (Sentry != null) { return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job)); } else { return processer(job); @@ -249,7 +255,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (config.sentryForBackend) { + if (Sentry != null) { Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, @@ -264,7 +270,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region deliver { this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => { - if (this.config.sentryForBackend) { + if (Sentry != null) { return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job)); } else { return this.deliverProcessorService.process(job); @@ -289,7 +295,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (config.sentryForBackend) { + if (Sentry != null) { Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, @@ -304,7 +310,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region inbox { this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => { - if (this.config.sentryForBackend) { + if (Sentry != null) { return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job)); } else { return this.inboxProcessorService.process(job); @@ -329,7 +335,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) }); - if (config.sentryForBackend) { + if (Sentry != null) { Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, @@ -344,7 +350,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region user-webhook deliver { this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => { - if (this.config.sentryForBackend) { + if (Sentry != null) { return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job)); } else { return this.userWebhookDeliverProcessorService.process(job); @@ -369,7 +375,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (config.sentryForBackend) { + if (Sentry != null) { Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, @@ -384,7 +390,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region system-webhook deliver { this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => { - if (this.config.sentryForBackend) { + if (Sentry != null) { return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job)); } else { return this.systemWebhookDeliverProcessorService.process(job); @@ -409,7 +415,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); - if (config.sentryForBackend) { + if (Sentry != null) { Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, @@ -434,7 +440,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }; this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { - if (this.config.sentryForBackend) { + if (Sentry != null) { return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job)); } else { return processer(job); @@ -456,7 +462,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (config.sentryForBackend) { + if (Sentry != null) { Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, @@ -479,7 +485,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }; this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { - if (this.config.sentryForBackend) { + if (Sentry != null) { return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job)); } else { return processer(job); @@ -497,7 +503,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); - if (config.sentryForBackend) { + if (Sentry != null) { Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, @@ -512,7 +518,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region ended poll notification { this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => { - if (this.config.sentryForBackend) { + if (Sentry != null) { return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job)); } else { return this.endedPollNotificationProcessorService.process(job); @@ -527,7 +533,7 @@ export class QueueProcessorService implements OnApplicationShutdown { //#region post scheduled note { this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => { - if (this.config.sentryForBackend) { + if (Sentry != null) { return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job)); } else { return this.postScheduledNoteProcessorService.process(job); diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 7a4af407a3..27c79ab438 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -7,7 +7,6 @@ import { randomUUID } from 'node:crypto'; import * as fs from 'node:fs'; import * as stream from 'node:stream/promises'; import { Inject, Injectable } from '@nestjs/common'; -import * as Sentry from '@sentry/node'; import { DI } from '@/di-symbols.js'; import { getIpHash } from '@/misc/get-ip-hash.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; @@ -37,6 +36,7 @@ export class ApiCallService implements OnApplicationShutdown { private logger: Logger; private userIpHistories: Map>; private userIpHistoriesClearIntervalId: NodeJS.Timeout; + private Sentry: typeof import('@sentry/node') | null = null; constructor( @Inject(DI.meta) @@ -59,6 +59,12 @@ export class ApiCallService implements OnApplicationShutdown { this.userIpHistoriesClearIntervalId = setInterval(() => { this.userIpHistories.clear(); }, 1000 * 60 * 60); + + if (this.config.sentryForBackend) { + import('@sentry/node').then((Sentry) => { + this.Sentry = Sentry; + }); + } } #sendApiError(reply: FastifyReply, err: ApiError): void { @@ -120,8 +126,8 @@ export class ApiCallService implements OnApplicationShutdown { }, }); - if (this.config.sentryForBackend) { - Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, { + if (this.Sentry != null) { + this.Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, { level: 'error', user: { id: userId, @@ -432,8 +438,8 @@ export class ApiCallService implements OnApplicationShutdown { } // API invoking - if (this.config.sentryForBackend) { - return await Sentry.startSpan({ + if (this.Sentry != null) { + return await this.Sentry.startSpan({ name: 'API: ' + ep.name, }, () => ep.exec(data, user, token, file, request.ip, request.headers) .catch((err: Error) => this.#onExecError(ep, data, err, user?.id))); diff --git a/packages/frontend-builder/locale-inliner.ts b/packages/frontend-builder/locale-inliner.ts index 9bef465eeb..191d7250a6 100644 --- a/packages/frontend-builder/locale-inliner.ts +++ b/packages/frontend-builder/locale-inliner.ts @@ -10,7 +10,7 @@ import { collectModifications } from './locale-inliner/collect-modifications.js' import { applyWithLocale } from './locale-inliner/apply-with-locale.js'; import { blankLogger } from './logger.js'; import type { Logger } from './logger.js'; -import type { Locale } from '../../locales/index.js'; +import type { Locale } from 'i18n'; import type { Manifest as ViteManifest } from 'vite'; export class LocaleInliner { diff --git a/packages/frontend-builder/locale-inliner/apply-with-locale.ts b/packages/frontend-builder/locale-inliner/apply-with-locale.ts index 5e601cdf12..78851d3029 100644 --- a/packages/frontend-builder/locale-inliner/apply-with-locale.ts +++ b/packages/frontend-builder/locale-inliner/apply-with-locale.ts @@ -5,7 +5,7 @@ import MagicString from 'magic-string'; import { assertNever } from '../utils.js'; -import type { Locale, ILocale } from '../../../locales/index.js'; +import type { ILocale, Locale } from 'i18n'; import type { TextModification } from '../locale-inliner.js'; import type { Logger } from '../logger.js'; diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json index d01e4c86ed..37dd133fe6 100644 --- a/packages/frontend-builder/package.json +++ b/packages/frontend-builder/package.json @@ -18,6 +18,7 @@ "typescript": "5.9.3" }, "dependencies": { + "i18n": "workspace:*", "estree-walker": "3.0.3", "magic-string": "0.30.21", "vite": "7.2.4" diff --git a/packages/frontend-embed/build.ts b/packages/frontend-embed/build.ts index 737233a4d0..4e1f588802 100644 --- a/packages/frontend-embed/build.ts +++ b/packages/frontend-embed/build.ts @@ -2,7 +2,7 @@ import * as fs from 'fs/promises'; import url from 'node:url'; import path from 'node:path'; import { execa } from 'execa'; -import locales from '../../locales/index.js'; +import locales from 'i18n'; import { LocaleInliner } from '../frontend-builder/locale-inliner.js' import { createLogger } from '../frontend-builder/logger'; diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 85e25c8faa..c27583cf86 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@discordapp/twemoji": "16.0.1", + "i18n": "workspace:*", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.3", "@rollup/pluginutils": "5.3.0", diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue index b621110ec9..9866e50958 100644 --- a/packages/frontend-embed/src/components/I18n.vue +++ b/packages/frontend-embed/src/components/I18n.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only