From adb3ad6b7f2e4705d563ecdea65879db929a1971 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Fri, 8 Aug 2025 18:47:35 +0900 Subject: [PATCH] Follow up per locale bundle (#16381) * fix docker build * enable check spdx license id in frontend-builder * fix eslint config * run eslint for frontend-builder in ci * fix eslint * add license headers * fix unnecessary comments * update changelog * fix generateDts * fix tsx --- .github/workflows/check-spdx-license-id.yml | 1 + .github/workflows/lint.yml | 3 + CHANGELOG.md | 1 + Dockerfile | 1 + locales/generateDTS.js | 28 +++--- locales/index.d.ts | 4 +- packages/frontend-builder/README.txt | 2 - packages/frontend-builder/eslint.config.js | 48 +--------- packages/frontend-builder/locale-inliner.ts | 14 ++- .../locale-inliner/apply-with-locale.ts | 41 ++++---- .../locale-inliner/collect-modifications.ts | 96 ++++++++++--------- packages/frontend-builder/logger.ts | 13 ++- .../rollup-plugin-remove-unref-i18n.ts | 12 +-- packages/frontend-builder/utils.ts | 5 + packages/frontend-embed/vite.config.ts | 2 +- packages/frontend-shared/js/i18n.ts | 3 +- packages/frontend/vite.config.ts | 2 +- 17 files changed, 133 insertions(+), 143 deletions(-) diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml index e40a4557df..cf1fd6007d 100644 --- a/.github/workflows/check-spdx-license-id.yml +++ b/.github/workflows/check-spdx-license-id.yml @@ -50,6 +50,7 @@ jobs: "packages/backend/test" "packages/frontend-shared/@types" "packages/frontend-shared/js" + "packages/frontend-builder" "packages/frontend/.storybook" "packages/frontend/@types" "packages/frontend/lib" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 235faeb807..550438e308 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,6 +9,7 @@ on: - packages/backend/** - packages/frontend/** - packages/frontend-shared/** + - packages/frontend-builder/** - packages/frontend-embed/** - packages/icons-subsetter/** - packages/sw/** @@ -22,6 +23,7 @@ on: - packages/backend/** - packages/frontend/** - packages/frontend-shared/** + - packages/frontend-builder/** - packages/frontend-embed/** - packages/icons-subsetter/** - packages/sw/** @@ -56,6 +58,7 @@ jobs: - backend - frontend - frontend-shared + - frontend-builder - frontend-embed - icons-subsetter - sw diff --git a/CHANGELOG.md b/CHANGELOG.md index 4734033c54..a1dc3e2fe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Feat: ページのタブバーを下部に表示できるように - Enhance: コントロールパネルを検索できるように - Enhance: トルコ語 (tr-TR) に対応 +- Enhance: 言語別のスクリプトバンドルを生成するように - Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正 - Fix: 一部の設定検索結果が存在しないパスになる問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171) diff --git a/Dockerfile b/Dockerfile index 62b737c084..370bed5751 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"] COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-shared/"] 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/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/"] diff --git a/locales/generateDTS.js b/locales/generateDTS.js index 49807144ec..ab0613cc82 100644 --- a/locales/generateDTS.js +++ b/locales/generateDTS.js @@ -73,7 +73,7 @@ export default function generateDTS() { ts.NodeFlags.Const, ), ), - ts.factory.createInterfaceDeclaration( + ts.factory.createTypeAliasDeclaration( [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier('ParameterizedString'), [ @@ -84,20 +84,22 @@ export default function generateDTS() { ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), ), ], - undefined, - [ - ts.factory.createPropertySignature( - undefined, - ts.factory.createComputedPropertyName( - ts.factory.createIdentifier('kParameters'), - ), - undefined, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier('T'), + 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)], diff --git a/locales/index.d.ts b/locales/index.d.ts index cafa9012b9..b0a15e0ad1 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2,9 +2,9 @@ // This file is generated by locales/generateDTS.js // Do not edit this file directly. declare const kParameters: unique symbol; -export interface ParameterizedString { +export type ParameterizedString = string & { [kParameters]: T; -} +}; export interface ILocale { [_: string]: string | ParameterizedString | ILocale; } diff --git a/packages/frontend-builder/README.txt b/packages/frontend-builder/README.txt index db878ffa83..428166e792 100644 --- a/packages/frontend-builder/README.txt +++ b/packages/frontend-builder/README.txt @@ -1,3 +1 @@ This package contains the common scripts that are used to build the frontend and frontend-embed packages. - - diff --git a/packages/frontend-builder/eslint.config.js b/packages/frontend-builder/eslint.config.js index 5805c9924d..a13490c97f 100644 --- a/packages/frontend-builder/eslint.config.js +++ b/packages/frontend-builder/eslint.config.js @@ -1,20 +1,13 @@ import globals from 'globals'; import tsParser from '@typescript-eslint/parser'; -import pluginMisskey from '@misskey-dev/eslint-plugin'; import sharedConfig from '../shared/eslint.config.js'; // eslint-disable-next-line import/no-default-export export default [ ...sharedConfig, - { - files: ['**/*.vue'], - ...pluginMisskey.configs.typescript, - }, { files: [ - '@types/**/*.ts', - 'js/**/*.ts', - '**/*.vue', + '**/*.ts', ], languageOptions: { globals: { @@ -50,45 +43,6 @@ export default [ // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため 'id-denylist': ['error', 'window', 'e'], 'no-shadow': ['warn'], - 'vue/attributes-order': ['error', { - alphabetical: false, - }], - 'vue/no-use-v-if-with-v-for': ['error', { - allowUsingIterationVar: false, - }], - 'vue/no-ref-as-operand': 'error', - 'vue/no-multi-spaces': ['error', { - ignoreProperties: false, - }], - 'vue/no-v-html': 'warn', - 'vue/order-in-components': 'error', - 'vue/html-indent': ['warn', 'tab', { - attribute: 1, - baseIndent: 0, - closeBracket: 0, - alignAttributesVertically: true, - ignores: [], - }], - 'vue/html-closing-bracket-spacing': ['warn', { - startTag: 'never', - endTag: 'never', - selfClosingTag: 'never', - }], - 'vue/multi-word-component-names': 'warn', - 'vue/require-v-for-key': 'warn', - 'vue/no-unused-components': 'warn', - 'vue/no-unused-vars': 'warn', - 'vue/no-dupe-keys': 'warn', - 'vue/valid-v-for': 'warn', - 'vue/return-in-computed-property': 'warn', - 'vue/no-setup-props-reactivity-loss': 'warn', - 'vue/max-attributes-per-line': 'off', - 'vue/html-self-closing': 'off', - 'vue/singleline-html-element-content-newline': 'off', - 'vue/v-on-event-hyphenation': ['error', 'never', { - autofix: true, - }], - 'vue/attribute-hyphenation': ['error', 'never'], }, }, { diff --git a/packages/frontend-builder/locale-inliner.ts b/packages/frontend-builder/locale-inliner.ts index ce3f59a81c..75bcdc5b3f 100644 --- a/packages/frontend-builder/locale-inliner.ts +++ b/packages/frontend-builder/locale-inliner.ts @@ -1,11 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as fs from 'fs/promises'; import * as path from 'node:path'; -import { type Locale } from '../../locales/index.js'; -import type { Manifest as ViteManifest } from 'vite'; import MagicString from 'magic-string'; import { collectModifications } from './locale-inliner/collect-modifications.js'; import { applyWithLocale } from './locale-inliner/apply-with-locale.js'; -import { blankLogger, type Logger } from './logger.js'; +import { blankLogger } from './logger.js'; +import type { Logger } from './logger.js'; +import type { Locale } from '../../locales/index.js'; +import type { Manifest as ViteManifest } from 'vite'; export class LocaleInliner { outputDir: string; @@ -70,7 +76,7 @@ export class LocaleInliner { async saveLocale(localeName: string, localeJson: Locale) { // create directory await fs.mkdir(path.join(this.outputDir, localeName), { recursive: true }); - const localeLogger = localeName == 'ja-JP' ? this.logger : blankLogger; // we want to log for single locale only + const localeLogger = localeName === 'ja-JP' ? this.logger : blankLogger; // we want to log for single locale only for (const chunk of this.chunks) { if (!chunk.sourceCode || !chunk.modifications) { throw new Error(`Source code or modifications for ${chunk.fileName} is not available.`); diff --git a/packages/frontend-builder/locale-inliner/apply-with-locale.ts b/packages/frontend-builder/locale-inliner/apply-with-locale.ts index a79ebb4253..5e601cdf12 100644 --- a/packages/frontend-builder/locale-inliner/apply-with-locale.ts +++ b/packages/frontend-builder/locale-inliner/apply-with-locale.ts @@ -1,6 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + import MagicString from 'magic-string'; -import type { Locale } from '../../../locales/index.js'; import { assertNever } from '../utils.js'; +import type { Locale, ILocale } from '../../../locales/index.js'; import type { TextModification } from '../locale-inliner.js'; import type { Logger } from '../logger.js'; @@ -13,16 +18,16 @@ export function applyWithLocale( ) { for (const modification of modifications) { switch (modification.type) { - case "delete": + case 'delete': sourceCode.remove(modification.begin, modification.end); break; - case "insert": + case 'insert': sourceCode.appendRight(modification.begin, modification.text); break; - case "replace": + case 'replace': sourceCode.update(modification.begin, modification.end, modification.text); break; - case "localized": { + case 'localized': { const accessed = getPropertyByPath(localeJson, modification.localizationKey); if (accessed == null) { fileLogger.warn(`Cannot find localization key ${modification.localizationKey.join('.')}`); @@ -30,7 +35,7 @@ export function applyWithLocale( sourceCode.update(modification.begin, modification.end, JSON.stringify(accessed)); break; } - case "parameterized-function": { + case 'parameterized-function': { const accessed = getPropertyByPath(localeJson, modification.localizationKey); let replacement: string; if (typeof accessed === 'string') { @@ -44,33 +49,33 @@ export function applyWithLocale( sourceCode.update(modification.begin, modification.end, replacement); break; - function formatFunction(accessed: string): string { + function formatFunction(format: string): string { const params = new Set(); const components: string[] = []; let lastIndex = 0; - for (const match of accessed.matchAll(/\{(.+?)}/g)) { + for (const match of format.matchAll(/\{(.+?)}/g)) { const [fullMatch, paramName] = match; if (lastIndex < match.index) { - components.push(JSON.stringify(accessed.slice(lastIndex, match.index))); + components.push(JSON.stringify(format.slice(lastIndex, match.index))); } params.add(paramName); components.push(paramName); lastIndex = match.index + fullMatch.length; } - components.push(JSON.stringify(accessed.slice(lastIndex))); + components.push(JSON.stringify(format.slice(lastIndex))); // we replace with `(({name,count})=>(name+count+"some"))` const paramList = Array.from(params).join(','); - let body = components.filter(x => x != '""').join('+'); - if (body == '') body = '""'; // if the body is empty, we return empty string + let body = components.filter(x => x !== '""').join('+'); + if (body === '') body = '""'; // if the body is empty, we return empty string return `(({${paramList}})=>(${body}))`; } } - case "locale-name": { + case 'locale-name': { sourceCode.update(modification.begin, modification.end, modification.literal ? JSON.stringify(localeName) : localeName); break; } - case "locale-json": { + case 'locale-json': { // locale-json is inlined to place where initialize module-level variable which is executed only once. // In such case we can use JSON.parse to speed up the parsing script. // https://v8.dev/blog/cost-of-javascript-2019#json @@ -84,14 +89,14 @@ export function applyWithLocale( } } -function getPropertyByPath(localeJson: any, localizationKey: string[]): string | object | null { +function getPropertyByPath(localeJson: ILocale, localizationKey: string[]): string | object | null { if (localizationKey.length === 0) return localeJson; - let current: any = localeJson; + let current: ILocale | string = localeJson; for (const key of localizationKey) { - if (typeof current !== 'object' || current === null || !(key in current)) { + if (typeof current !== 'object' || !(key in current)) { return null; // Key not found } current = current[key]; } - return current ?? null; + return current; } diff --git a/packages/frontend-builder/locale-inliner/collect-modifications.ts b/packages/frontend-builder/locale-inliner/collect-modifications.ts index 0cbf6a504e..59e5d96517 100644 --- a/packages/frontend-builder/locale-inliner/collect-modifications.ts +++ b/packages/frontend-builder/locale-inliner/collect-modifications.ts @@ -1,10 +1,15 @@ -import type { AstNode, ProgramNode } from 'rollup'; +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { parseAst } from 'vite'; import * as estreeWalker from 'estree-walker'; +import { assertNever, assertType } from '../utils.js'; +import type { AstNode, ProgramNode } from 'rollup'; import type * as estree from 'estree'; import type { LocaleInliner, TextModification } from '../locale-inliner.js'; -import type { Logger } from '../logger.js' -import { assertNever, assertType } from '../utils.js'; +import type { Logger } from '../logger.js'; // WalkerContext is not exported from estree-walker, so we define it here interface WalkerContext { @@ -15,12 +20,12 @@ export function collectModifications(sourceCode: string, fileName: string, fileL let programNode: ProgramNode; try { programNode = parseAst(sourceCode); - } catch (e) { - fileLogger.error(`Failed to parse source code: ${e}`); + } catch (err) { + fileLogger.error(`Failed to parse source code: ${err}`); return []; } if (programNode.sourceType !== 'module') { - fileLogger.error(`Source code is not a module.`); + fileLogger.error('Source code is not a module.'); return []; } @@ -32,7 +37,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL // 3) replace all `await window.fetch(`/assets/locales/${d}.${x}.json`).then(u=>u.json())` with `localeJson` variable estreeWalker.walk(programNode, { enter(this: WalkerContext, node: Node) { - assertType(node) + assertType(node); if (node.type === 'Literal' && typeof node.value === 'string' && node.raw) { if (node.raw.substring(1).startsWith(inliner.scriptsDir)) { @@ -46,7 +51,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL localizedOnly: true, }); } - if (node.raw.substring(1, node.raw.length - 1) == `${inliner.scriptsDir}/${inliner.i18nFileName}`) { + if (node.raw.substring(1, node.raw.length - 1) === `${inliner.scriptsDir}/${inliner.i18nFileName}`) { // we find `scripts/i18n.ts` literal. // This is tipically in depmap and replace with this file name to avoid unnecessary loading i18n script fileLogger.debug(`${lineCol(sourceCode, node)}: found ${inliner.i18nFileName} path literal ${node.raw}`); @@ -81,17 +86,17 @@ export function collectModifications(sourceCode: string, fileName: string, fileL localizedOnly: true, }); } - } - }) + }, + }); const importSpecifierResult = findImportSpecifier(programNode, inliner.i18nFileName, 'i18n'); switch (importSpecifierResult.type) { case 'no-import': - fileLogger.debug(`No import of i18n found, skipping inlining.`); + fileLogger.debug('No import of i18n found, skipping inlining.'); return modifications; case 'no-specifiers': - fileLogger.debug(`Importing i18n without specifiers, removing the import.`); + fileLogger.debug('Importing i18n without specifiers, removing the import.'); modifications.push({ type: 'delete', begin: importSpecifierResult.importNode.start, @@ -115,17 +120,18 @@ export function collectModifications(sourceCode: string, fileName: string, fileL let isSupported = true; estreeWalker.walk(programNode, { enter(node) { - if (node.type == 'VariableDeclaration') { + if (node.type === 'VariableDeclaration') { assertType(node); - for (let id of node.declarations.flatMap(x => declsOfPattern(x.id))) { - if (id == localI18nIdentifier) { + for (const id of node.declarations.flatMap(x => declsOfPattern(x.id))) { + if (id === localI18nIdentifier) { isSupported = false; } } } - } - }) + }, + }); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!isSupported) { fileLogger.error(`Duplicated identifier "${localI18nIdentifier}" in variable declaration. Skipping inlining.`); return modifications; @@ -141,8 +147,8 @@ export function collectModifications(sourceCode: string, fileName: string, fileL toSkip.add(i18nImport); estreeWalker.walk(programNode, { enter(this: WalkerContext, node, parent, property) { - assertType(node) - assertType(parent) + assertType(node); + assertType(parent); if (toSkip.has(node)) { // This is the import specifier, skip processing it this.skip(); @@ -150,23 +156,23 @@ export function collectModifications(sourceCode: string, fileName: string, fileL } // We don't care original name part of the import declaration - if (node.type == 'ImportDeclaration') this.skip(); + if (node.type === 'ImportDeclaration') this.skip(); if (node.type === 'Identifier') { - assertType(node) - assertType(parent) - if (parent.type === 'Property' && !parent.computed && property == 'key') return; // we don't care 'id' part of { id: expr } - if (parent.type === 'MemberExpression' && !parent.computed && property == 'property') return; // we don't care 'id' part of { id: expr } - if (parent.type === 'ExportSpecifier' && property == 'exported') return; // we don't care 'id' part of { id: expr } - if (node.name == localI18nIdentifier) { + assertType(node); + assertType(parent); + if (parent.type === 'Property' && !parent.computed && property === 'key') return; // we don't care 'id' part of { id: expr } + if (parent.type === 'MemberExpression' && !parent.computed && property === 'property') return; // we don't care 'id' part of { id: expr } + if (parent.type === 'ExportSpecifier' && property === 'exported') return; // we don't care 'id' part of { id: expr } + if (node.name === localI18nIdentifier) { fileLogger.error(`${lineCol(sourceCode, node)}: Using i18n identifier "${localI18nIdentifier}" directly. Skipping inlining.`); preserveI18nImport = true; } } else if (node.type === 'MemberExpression') { assertType(node); const i18nPath = parseI18nPropertyAccess(node); - if (i18nPath != null && i18nPath.length >= 2 && i18nPath[0] == 'ts') { - if (parent.type === 'CallExpression' && property == 'callee') return; // we don't want to process `i18n.ts.property.stringBuiltinMethod()` + if (i18nPath != null && i18nPath.length >= 2 && i18nPath[0] === 'ts') { + if (parent.type === 'CallExpression' && property === 'callee') return; // we don't want to process `i18n.ts.property.stringBuiltinMethod()` if (i18nPath.at(-1)?.startsWith('_')) fileLogger.debug(`found i18n grouped property access ${i18nPath.join('.')}`); else fileLogger.debug(`${lineCol(sourceCode, node)}: found i18n property access ${i18nPath.join('.')}`); // it's i18n.ts.propertyAccess @@ -179,7 +185,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL localizedOnly: true, }); this.skip(); - } else if (i18nPath != null && i18nPath.length >= 2 && i18nPath[0] == 'tsx') { + } else if (i18nPath != null && i18nPath.length >= 2 && i18nPath[0] === 'tsx') { // it's parameterized locale substitution (`i18n.tsx.property(parameters)`) // we expect the parameter to be an object literal fileLogger.debug(`${lineCol(sourceCode, node)}: found i18n function access (object) ${i18nPath.join('.')}`); @@ -197,11 +203,12 @@ export function collectModifications(sourceCode: string, fileName: string, fileL // If there is 'i18n' in the parameters, we care interior of the function if (node.params.flatMap(param => declsOfPattern(param)).includes(localI18nIdentifier)) this.skip(); } - } - }) + }, + }); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!preserveI18nImport) { - fileLogger.debug(`removing i18n import statement`); + fileLogger.debug('removing i18n import statement'); modifications.push({ type: 'delete', begin: i18nImport.start, @@ -211,7 +218,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL } function parseI18nPropertyAccess(node: estree.Expression | estree.Super): string[] | null { - if (node.type === 'Identifier' && node.name == localI18nIdentifier) return []; // i18n itself + if (node.type === 'Identifier' && node.name === localI18nIdentifier) return []; // i18n itself if (node.type !== 'MemberExpression') return null; // super.* if (node.object.type === 'Super') return null; @@ -219,7 +226,6 @@ export function collectModifications(sourceCode: string, fileName: string, fileL // i18n?.property is not supported if (node.optional) return null; - let id: string | null = null; if (node.computed) { if (node.property.type === 'Literal' && typeof node.property.value === 'string') { @@ -243,10 +249,10 @@ export function collectModifications(sourceCode: string, fileName: string, fileL function declsOfPattern(pattern: estree.Pattern | null): string[] { if (pattern == null) return []; - switch (pattern?.type) { - case "Identifier": + switch (pattern.type) { + case 'Identifier': return [pattern.name]; - case "ObjectPattern": + case 'ObjectPattern': return pattern.properties.flatMap(prop => { switch (prop.type) { case 'Property': @@ -254,16 +260,16 @@ function declsOfPattern(pattern: estree.Pattern | null): string[] { case 'RestElement': return declsOfPattern(prop.argument); default: - assertNever(prop) + assertNever(prop); } }); - case "ArrayPattern": + case 'ArrayPattern': return pattern.elements.flatMap(p => declsOfPattern(p)); - case "RestElement": + case 'RestElement': return declsOfPattern(pattern.argument); - case "AssignmentPattern": + case 'AssignmentPattern': return declsOfPattern(pattern.left); - case "MemberExpression": + case 'MemberExpression': // assignment pattern so no new variable is declared return []; default: @@ -375,15 +381,15 @@ type SpecifierResult = function findImportSpecifier(programNode: ProgramNode, i18nFileName: string, i18nSymbol: string): SpecifierResult { const imports = programNode.body.filter(x => x.type === 'ImportDeclaration'); - const importNode = imports.find(x => x.source.value === `./${i18nFileName}`) as estree.ImportDeclaration; + const importNode = imports.find(x => x.source.value === `./${i18nFileName}`) as estree.ImportDeclaration | undefined; if (!importNode) return { type: 'no-import' }; assertType(importNode); - if (importNode.specifiers.length == 0) { + if (importNode.specifiers.length === 0) { return { type: 'no-specifiers', importNode }; } - if (importNode.specifiers.length != 1) { + if (importNode.specifiers.length !== 1) { return { type: 'unexpected-specifiers', importNode }; } const i18nImportSpecifier = importNode.specifiers[0]; diff --git a/packages/frontend-builder/logger.ts b/packages/frontend-builder/logger.ts index a3f66730e2..c619882380 100644 --- a/packages/frontend-builder/logger.ts +++ b/packages/frontend-builder/logger.ts @@ -1,4 +1,11 @@ -const debug = false; +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as process from 'node:process'; + +const debug = process.env.BUILDER_DEBUG !== undefined && process.env.BUILDER_DEBUG !== '0'; export interface Logger { debug(message: string): void; @@ -27,7 +34,7 @@ export function createLogger(): RootLogger { type LogContext = { warningCount: number; errorCount: number; -} +}; function loggerFactory(prefix: string, context: LogContext): RootLogger { return { @@ -63,4 +70,4 @@ export const blankLogger: Logger = { error: () => void 0, info: () => void 0, prefixed: () => blankLogger, -} +}; diff --git a/packages/frontend-builder/rollup-plugin-remove-unref-i18n.ts b/packages/frontend-builder/rollup-plugin-remove-unref-i18n.ts index 9010e6910c..4a2bfa67d9 100644 --- a/packages/frontend-builder/rollup-plugin-remove-unref-i18n.ts +++ b/packages/frontend-builder/rollup-plugin-remove-unref-i18n.ts @@ -4,18 +4,18 @@ */ import * as estreeWalker from 'estree-walker'; -import type { Plugin } from 'vite'; -import type { CallExpression, Expression, Program, } from 'estree'; import MagicString from 'magic-string'; -import type { AstNode } from 'rollup'; import { assertType } from './utils.js'; +import type { Plugin } from 'vite'; +import type { CallExpression, Expression, Program } from 'estree'; +import type { AstNode } from 'rollup'; // This plugin transforms `unref(i18n)` to `i18n` in the code, which is useful for removing unnecessary unref calls // and helps locale inliner runs after vite build to inline the locale data into the final build. // // locale inliner cannot know minifiedSymbol(i18n) is 'unref(i18n)' or 'otherFunctionsWithEffect(i18n)' so // it is necessary to remove unref calls before minification. -export default function pluginRemoveUnrefI18n( +export function pluginRemoveUnrefI18n( { i18nSymbolName = 'i18n', }: { @@ -42,12 +42,12 @@ export default function pluginRemoveUnrefI18n( magicString.remove(arg.end, node.end); } } - } + }, }); return { code: magicString.toString(), map: magicString.generateMap({ hires: true }), - } + }; }, }; } diff --git a/packages/frontend-builder/utils.ts b/packages/frontend-builder/utils.ts index 1ce9f7fc76..71ffebe03e 100644 --- a/packages/frontend-builder/utils.ts +++ b/packages/frontend-builder/utils.ts @@ -1,5 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ export function assertNever(x: never): never { + // eslint-disable-next-line @typescript-eslint/no-explicit-any throw new Error(`Unexpected type: ${(x as any)?.type ?? x}`); } diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts index eb57db9774..3ddee9b8a9 100644 --- a/packages/frontend-embed/vite.config.ts +++ b/packages/frontend-embed/vite.config.ts @@ -8,7 +8,7 @@ import locales from '../../locales/index.js'; import meta from '../../package.json'; import packageInfo from './package.json' with { type: 'json' }; import pluginJson5 from './vite.json5.js'; -import pluginRemoveUnrefI18n from '../frontend-builder/rollup-plugin-remove-unref-i18n'; +import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n'; const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null; const host = url ? (new URL(url)).hostname : undefined; diff --git a/packages/frontend-shared/js/i18n.ts b/packages/frontend-shared/js/i18n.ts index bd9ba0922a..3b103c4714 100644 --- a/packages/frontend-shared/js/i18n.ts +++ b/packages/frontend-shared/js/i18n.ts @@ -28,7 +28,8 @@ type ParametersOf = { - readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString + // `string extends T[K] ? never : K` part removes non-parameterized string keys from Tsx type. + readonly [K in keyof T as string extends T[K] ? never : K]: T[K] extends ParameterizedString ? (arg: { readonly [_ in P]: string | number }) => string // @ts-expect-error -- 証明省略 : Tsx; diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index e9707d13d1..9b54014b54 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -14,7 +14,7 @@ import pluginJson5 from './vite.json5.js'; import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js'; import type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js'; import pluginWatchLocales from './lib/vite-plugin-watch-locales.js'; -import pluginRemoveUnrefI18n from '../frontend-builder/rollup-plugin-remove-unref-i18n.js'; +import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n.js'; const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null; const host = url ? (new URL(url)).hostname : undefined;