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
This commit is contained in:
parent
8598f3912e
commit
adb3ad6b7f
|
@ -50,6 +50,7 @@ jobs:
|
||||||
"packages/backend/test"
|
"packages/backend/test"
|
||||||
"packages/frontend-shared/@types"
|
"packages/frontend-shared/@types"
|
||||||
"packages/frontend-shared/js"
|
"packages/frontend-shared/js"
|
||||||
|
"packages/frontend-builder"
|
||||||
"packages/frontend/.storybook"
|
"packages/frontend/.storybook"
|
||||||
"packages/frontend/@types"
|
"packages/frontend/@types"
|
||||||
"packages/frontend/lib"
|
"packages/frontend/lib"
|
||||||
|
|
|
@ -9,6 +9,7 @@ on:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
- packages/frontend/**
|
- packages/frontend/**
|
||||||
- packages/frontend-shared/**
|
- packages/frontend-shared/**
|
||||||
|
- packages/frontend-builder/**
|
||||||
- packages/frontend-embed/**
|
- packages/frontend-embed/**
|
||||||
- packages/icons-subsetter/**
|
- packages/icons-subsetter/**
|
||||||
- packages/sw/**
|
- packages/sw/**
|
||||||
|
@ -22,6 +23,7 @@ on:
|
||||||
- packages/backend/**
|
- packages/backend/**
|
||||||
- packages/frontend/**
|
- packages/frontend/**
|
||||||
- packages/frontend-shared/**
|
- packages/frontend-shared/**
|
||||||
|
- packages/frontend-builder/**
|
||||||
- packages/frontend-embed/**
|
- packages/frontend-embed/**
|
||||||
- packages/icons-subsetter/**
|
- packages/icons-subsetter/**
|
||||||
- packages/sw/**
|
- packages/sw/**
|
||||||
|
@ -56,6 +58,7 @@ jobs:
|
||||||
- backend
|
- backend
|
||||||
- frontend
|
- frontend
|
||||||
- frontend-shared
|
- frontend-shared
|
||||||
|
- frontend-builder
|
||||||
- frontend-embed
|
- frontend-embed
|
||||||
- icons-subsetter
|
- icons-subsetter
|
||||||
- sw
|
- sw
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
- Feat: ページのタブバーを下部に表示できるように
|
- Feat: ページのタブバーを下部に表示できるように
|
||||||
- Enhance: コントロールパネルを検索できるように
|
- Enhance: コントロールパネルを検索できるように
|
||||||
- Enhance: トルコ語 (tr-TR) に対応
|
- Enhance: トルコ語 (tr-TR) に対応
|
||||||
|
- Enhance: 言語別のスクリプトバンドルを生成するように
|
||||||
- Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正
|
- Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正
|
||||||
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
|
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
|
||||||
|
|
|
@ -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-shared/package.json", "./packages/frontend-shared/"]
|
||||||
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
||||||
COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
|
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/icons-subsetter/package.json", "./packages/icons-subsetter/"]
|
||||||
COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default function generateDTS() {
|
||||||
ts.NodeFlags.Const,
|
ts.NodeFlags.Const,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ts.factory.createInterfaceDeclaration(
|
ts.factory.createTypeAliasDeclaration(
|
||||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||||
ts.factory.createIdentifier('ParameterizedString'),
|
ts.factory.createIdentifier('ParameterizedString'),
|
||||||
[
|
[
|
||||||
|
@ -84,8 +84,9 @@ export default function generateDTS() {
|
||||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
undefined,
|
ts.factory.createIntersectionTypeNode([
|
||||||
[
|
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||||
|
ts.factory.createTypeLiteralNode([
|
||||||
ts.factory.createPropertySignature(
|
ts.factory.createPropertySignature(
|
||||||
undefined,
|
undefined,
|
||||||
ts.factory.createComputedPropertyName(
|
ts.factory.createComputedPropertyName(
|
||||||
|
@ -97,7 +98,8 @@ export default function generateDTS() {
|
||||||
undefined,
|
undefined,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
])
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
ts.factory.createInterfaceDeclaration(
|
ts.factory.createInterfaceDeclaration(
|
||||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
// This file is generated by locales/generateDTS.js
|
// This file is generated by locales/generateDTS.js
|
||||||
// Do not edit this file directly.
|
// Do not edit this file directly.
|
||||||
declare const kParameters: unique symbol;
|
declare const kParameters: unique symbol;
|
||||||
export interface ParameterizedString<T extends string = string> {
|
export type ParameterizedString<T extends string = string> = string & {
|
||||||
[kParameters]: T;
|
[kParameters]: T;
|
||||||
}
|
};
|
||||||
export interface ILocale {
|
export interface ILocale {
|
||||||
[_: string]: string | ParameterizedString | ILocale;
|
[_: string]: string | ParameterizedString | ILocale;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
This package contains the common scripts that are used to build the frontend and frontend-embed packages.
|
This package contains the common scripts that are used to build the frontend and frontend-embed packages.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,13 @@
|
||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
import tsParser from '@typescript-eslint/parser';
|
import tsParser from '@typescript-eslint/parser';
|
||||||
import pluginMisskey from '@misskey-dev/eslint-plugin';
|
|
||||||
import sharedConfig from '../shared/eslint.config.js';
|
import sharedConfig from '../shared/eslint.config.js';
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default [
|
export default [
|
||||||
...sharedConfig,
|
...sharedConfig,
|
||||||
{
|
|
||||||
files: ['**/*.vue'],
|
|
||||||
...pluginMisskey.configs.typescript,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
files: [
|
files: [
|
||||||
'@types/**/*.ts',
|
'**/*.ts',
|
||||||
'js/**/*.ts',
|
|
||||||
'**/*.vue',
|
|
||||||
],
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
|
@ -50,45 +43,6 @@ export default [
|
||||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||||
'id-denylist': ['error', 'window', 'e'],
|
'id-denylist': ['error', 'window', 'e'],
|
||||||
'no-shadow': ['warn'],
|
'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'],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 fs from 'fs/promises';
|
||||||
import * as path from 'node:path';
|
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 MagicString from 'magic-string';
|
||||||
import { collectModifications } from './locale-inliner/collect-modifications.js';
|
import { collectModifications } from './locale-inliner/collect-modifications.js';
|
||||||
import { applyWithLocale } from './locale-inliner/apply-with-locale.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 {
|
export class LocaleInliner {
|
||||||
outputDir: string;
|
outputDir: string;
|
||||||
|
@ -70,7 +76,7 @@ export class LocaleInliner {
|
||||||
async saveLocale(localeName: string, localeJson: Locale) {
|
async saveLocale(localeName: string, localeJson: Locale) {
|
||||||
// create directory
|
// create directory
|
||||||
await fs.mkdir(path.join(this.outputDir, localeName), { recursive: true });
|
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) {
|
for (const chunk of this.chunks) {
|
||||||
if (!chunk.sourceCode || !chunk.modifications) {
|
if (!chunk.sourceCode || !chunk.modifications) {
|
||||||
throw new Error(`Source code or modifications for ${chunk.fileName} is not available.`);
|
throw new Error(`Source code or modifications for ${chunk.fileName} is not available.`);
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
import MagicString from 'magic-string';
|
import MagicString from 'magic-string';
|
||||||
import type { Locale } from '../../../locales/index.js';
|
|
||||||
import { assertNever } from '../utils.js';
|
import { assertNever } from '../utils.js';
|
||||||
|
import type { Locale, ILocale } from '../../../locales/index.js';
|
||||||
import type { TextModification } from '../locale-inliner.js';
|
import type { TextModification } from '../locale-inliner.js';
|
||||||
import type { Logger } from '../logger.js';
|
import type { Logger } from '../logger.js';
|
||||||
|
|
||||||
|
@ -13,16 +18,16 @@ export function applyWithLocale(
|
||||||
) {
|
) {
|
||||||
for (const modification of modifications) {
|
for (const modification of modifications) {
|
||||||
switch (modification.type) {
|
switch (modification.type) {
|
||||||
case "delete":
|
case 'delete':
|
||||||
sourceCode.remove(modification.begin, modification.end);
|
sourceCode.remove(modification.begin, modification.end);
|
||||||
break;
|
break;
|
||||||
case "insert":
|
case 'insert':
|
||||||
sourceCode.appendRight(modification.begin, modification.text);
|
sourceCode.appendRight(modification.begin, modification.text);
|
||||||
break;
|
break;
|
||||||
case "replace":
|
case 'replace':
|
||||||
sourceCode.update(modification.begin, modification.end, modification.text);
|
sourceCode.update(modification.begin, modification.end, modification.text);
|
||||||
break;
|
break;
|
||||||
case "localized": {
|
case 'localized': {
|
||||||
const accessed = getPropertyByPath(localeJson, modification.localizationKey);
|
const accessed = getPropertyByPath(localeJson, modification.localizationKey);
|
||||||
if (accessed == null) {
|
if (accessed == null) {
|
||||||
fileLogger.warn(`Cannot find localization key ${modification.localizationKey.join('.')}`);
|
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));
|
sourceCode.update(modification.begin, modification.end, JSON.stringify(accessed));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "parameterized-function": {
|
case 'parameterized-function': {
|
||||||
const accessed = getPropertyByPath(localeJson, modification.localizationKey);
|
const accessed = getPropertyByPath(localeJson, modification.localizationKey);
|
||||||
let replacement: string;
|
let replacement: string;
|
||||||
if (typeof accessed === 'string') {
|
if (typeof accessed === 'string') {
|
||||||
|
@ -44,33 +49,33 @@ export function applyWithLocale(
|
||||||
sourceCode.update(modification.begin, modification.end, replacement);
|
sourceCode.update(modification.begin, modification.end, replacement);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
function formatFunction(accessed: string): string {
|
function formatFunction(format: string): string {
|
||||||
const params = new Set<string>();
|
const params = new Set<string>();
|
||||||
const components: string[] = [];
|
const components: string[] = [];
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
for (const match of accessed.matchAll(/\{(.+?)}/g)) {
|
for (const match of format.matchAll(/\{(.+?)}/g)) {
|
||||||
const [fullMatch, paramName] = match;
|
const [fullMatch, paramName] = match;
|
||||||
if (lastIndex < match.index) {
|
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);
|
params.add(paramName);
|
||||||
components.push(paramName);
|
components.push(paramName);
|
||||||
lastIndex = match.index + fullMatch.length;
|
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"))`
|
// we replace with `(({name,count})=>(name+count+"some"))`
|
||||||
const paramList = Array.from(params).join(',');
|
const paramList = Array.from(params).join(',');
|
||||||
let body = components.filter(x => x != '""').join('+');
|
let body = components.filter(x => x !== '""').join('+');
|
||||||
if (body == '') body = '""'; // if the body is empty, we return empty string
|
if (body === '') body = '""'; // if the body is empty, we return empty string
|
||||||
return `(({${paramList}})=>(${body}))`;
|
return `(({${paramList}})=>(${body}))`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "locale-name": {
|
case 'locale-name': {
|
||||||
sourceCode.update(modification.begin, modification.end, modification.literal ? JSON.stringify(localeName) : localeName);
|
sourceCode.update(modification.begin, modification.end, modification.literal ? JSON.stringify(localeName) : localeName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "locale-json": {
|
case 'locale-json': {
|
||||||
// locale-json is inlined to place where initialize module-level variable which is executed only once.
|
// 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.
|
// In such case we can use JSON.parse to speed up the parsing script.
|
||||||
// https://v8.dev/blog/cost-of-javascript-2019#json
|
// 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;
|
if (localizationKey.length === 0) return localeJson;
|
||||||
let current: any = localeJson;
|
let current: ILocale | string = localeJson;
|
||||||
for (const key of localizationKey) {
|
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
|
return null; // Key not found
|
||||||
}
|
}
|
||||||
current = current[key];
|
current = current[key];
|
||||||
}
|
}
|
||||||
return current ?? null;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { parseAst } from 'vite';
|
||||||
import * as estreeWalker from 'estree-walker';
|
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 * as estree from 'estree';
|
||||||
import type { LocaleInliner, TextModification } from '../locale-inliner.js';
|
import type { LocaleInliner, TextModification } from '../locale-inliner.js';
|
||||||
import type { Logger } from '../logger.js'
|
import type { Logger } from '../logger.js';
|
||||||
import { assertNever, assertType } from '../utils.js';
|
|
||||||
|
|
||||||
// WalkerContext is not exported from estree-walker, so we define it here
|
// WalkerContext is not exported from estree-walker, so we define it here
|
||||||
interface WalkerContext {
|
interface WalkerContext {
|
||||||
|
@ -15,12 +20,12 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
|
||||||
let programNode: ProgramNode;
|
let programNode: ProgramNode;
|
||||||
try {
|
try {
|
||||||
programNode = parseAst(sourceCode);
|
programNode = parseAst(sourceCode);
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
fileLogger.error(`Failed to parse source code: ${e}`);
|
fileLogger.error(`Failed to parse source code: ${err}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (programNode.sourceType !== 'module') {
|
if (programNode.sourceType !== 'module') {
|
||||||
fileLogger.error(`Source code is not a module.`);
|
fileLogger.error('Source code is not a module.');
|
||||||
return [];
|
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
|
// 3) replace all `await window.fetch(`/assets/locales/${d}.${x}.json`).then(u=>u.json())` with `localeJson` variable
|
||||||
estreeWalker.walk(programNode, {
|
estreeWalker.walk(programNode, {
|
||||||
enter(this: WalkerContext, node: Node) {
|
enter(this: WalkerContext, node: Node) {
|
||||||
assertType<AstNode>(node)
|
assertType<AstNode>(node);
|
||||||
|
|
||||||
if (node.type === 'Literal' && typeof node.value === 'string' && node.raw) {
|
if (node.type === 'Literal' && typeof node.value === 'string' && node.raw) {
|
||||||
if (node.raw.substring(1).startsWith(inliner.scriptsDir)) {
|
if (node.raw.substring(1).startsWith(inliner.scriptsDir)) {
|
||||||
|
@ -46,7 +51,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
|
||||||
localizedOnly: true,
|
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.
|
// we find `scripts/i18n.ts` literal.
|
||||||
// This is tipically in depmap and replace with this file name to avoid unnecessary loading i18n script
|
// 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}`);
|
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,
|
localizedOnly: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const importSpecifierResult = findImportSpecifier(programNode, inliner.i18nFileName, 'i18n');
|
const importSpecifierResult = findImportSpecifier(programNode, inliner.i18nFileName, 'i18n');
|
||||||
|
|
||||||
switch (importSpecifierResult.type) {
|
switch (importSpecifierResult.type) {
|
||||||
case 'no-import':
|
case 'no-import':
|
||||||
fileLogger.debug(`No import of i18n found, skipping inlining.`);
|
fileLogger.debug('No import of i18n found, skipping inlining.');
|
||||||
return modifications;
|
return modifications;
|
||||||
case 'no-specifiers':
|
case 'no-specifiers':
|
||||||
fileLogger.debug(`Importing i18n without specifiers, removing the import.`);
|
fileLogger.debug('Importing i18n without specifiers, removing the import.');
|
||||||
modifications.push({
|
modifications.push({
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
begin: importSpecifierResult.importNode.start,
|
begin: importSpecifierResult.importNode.start,
|
||||||
|
@ -115,17 +120,18 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
|
||||||
let isSupported = true;
|
let isSupported = true;
|
||||||
estreeWalker.walk(programNode, {
|
estreeWalker.walk(programNode, {
|
||||||
enter(node) {
|
enter(node) {
|
||||||
if (node.type == 'VariableDeclaration') {
|
if (node.type === 'VariableDeclaration') {
|
||||||
assertType<estree.VariableDeclaration>(node);
|
assertType<estree.VariableDeclaration>(node);
|
||||||
for (let id of node.declarations.flatMap(x => declsOfPattern(x.id))) {
|
for (const id of node.declarations.flatMap(x => declsOfPattern(x.id))) {
|
||||||
if (id == localI18nIdentifier) {
|
if (id === localI18nIdentifier) {
|
||||||
isSupported = false;
|
isSupported = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (!isSupported) {
|
if (!isSupported) {
|
||||||
fileLogger.error(`Duplicated identifier "${localI18nIdentifier}" in variable declaration. Skipping inlining.`);
|
fileLogger.error(`Duplicated identifier "${localI18nIdentifier}" in variable declaration. Skipping inlining.`);
|
||||||
return modifications;
|
return modifications;
|
||||||
|
@ -141,8 +147,8 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
|
||||||
toSkip.add(i18nImport);
|
toSkip.add(i18nImport);
|
||||||
estreeWalker.walk(programNode, {
|
estreeWalker.walk(programNode, {
|
||||||
enter(this: WalkerContext, node, parent, property) {
|
enter(this: WalkerContext, node, parent, property) {
|
||||||
assertType<AstNode>(node)
|
assertType<AstNode>(node);
|
||||||
assertType<AstNode>(parent)
|
assertType<AstNode>(parent);
|
||||||
if (toSkip.has(node)) {
|
if (toSkip.has(node)) {
|
||||||
// This is the import specifier, skip processing it
|
// This is the import specifier, skip processing it
|
||||||
this.skip();
|
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
|
// 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') {
|
if (node.type === 'Identifier') {
|
||||||
assertType<estree.Identifier>(node)
|
assertType<estree.Identifier>(node);
|
||||||
assertType<estree.Property | estree.MemberExpression | estree.ExportSpecifier>(parent)
|
assertType<estree.Property | estree.MemberExpression | estree.ExportSpecifier>(parent);
|
||||||
if (parent.type === 'Property' && !parent.computed && property == 'key') return; // we don't care 'id' part of { id: expr }
|
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 === '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 (parent.type === 'ExportSpecifier' && property === 'exported') return; // we don't care 'id' part of { id: expr }
|
||||||
if (node.name == localI18nIdentifier) {
|
if (node.name === localI18nIdentifier) {
|
||||||
fileLogger.error(`${lineCol(sourceCode, node)}: Using i18n identifier "${localI18nIdentifier}" directly. Skipping inlining.`);
|
fileLogger.error(`${lineCol(sourceCode, node)}: Using i18n identifier "${localI18nIdentifier}" directly. Skipping inlining.`);
|
||||||
preserveI18nImport = true;
|
preserveI18nImport = true;
|
||||||
}
|
}
|
||||||
} else if (node.type === 'MemberExpression') {
|
} else if (node.type === 'MemberExpression') {
|
||||||
assertType<estree.MemberExpression>(node);
|
assertType<estree.MemberExpression>(node);
|
||||||
const i18nPath = parseI18nPropertyAccess(node);
|
const i18nPath = parseI18nPropertyAccess(node);
|
||||||
if (i18nPath != null && i18nPath.length >= 2 && i18nPath[0] == 'ts') {
|
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 (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('.')}`);
|
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('.')}`);
|
else fileLogger.debug(`${lineCol(sourceCode, node)}: found i18n property access ${i18nPath.join('.')}`);
|
||||||
// it's i18n.ts.propertyAccess
|
// it's i18n.ts.propertyAccess
|
||||||
|
@ -179,7 +185,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
|
||||||
localizedOnly: true,
|
localizedOnly: true,
|
||||||
});
|
});
|
||||||
this.skip();
|
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)`)
|
// it's parameterized locale substitution (`i18n.tsx.property(parameters)`)
|
||||||
// we expect the parameter to be an object literal
|
// we expect the parameter to be an object literal
|
||||||
fileLogger.debug(`${lineCol(sourceCode, node)}: found i18n function access (object) ${i18nPath.join('.')}`);
|
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 there is 'i18n' in the parameters, we care interior of the function
|
||||||
if (node.params.flatMap(param => declsOfPattern(param)).includes(localI18nIdentifier)) this.skip();
|
if (node.params.flatMap(param => declsOfPattern(param)).includes(localI18nIdentifier)) this.skip();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (!preserveI18nImport) {
|
if (!preserveI18nImport) {
|
||||||
fileLogger.debug(`removing i18n import statement`);
|
fileLogger.debug('removing i18n import statement');
|
||||||
modifications.push({
|
modifications.push({
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
begin: i18nImport.start,
|
begin: i18nImport.start,
|
||||||
|
@ -211,7 +218,7 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseI18nPropertyAccess(node: estree.Expression | estree.Super): string[] | null {
|
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;
|
if (node.type !== 'MemberExpression') return null;
|
||||||
// super.*
|
// super.*
|
||||||
if (node.object.type === 'Super') return null;
|
if (node.object.type === 'Super') return null;
|
||||||
|
@ -219,7 +226,6 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
|
||||||
// i18n?.property is not supported
|
// i18n?.property is not supported
|
||||||
if (node.optional) return null;
|
if (node.optional) return null;
|
||||||
|
|
||||||
|
|
||||||
let id: string | null = null;
|
let id: string | null = null;
|
||||||
if (node.computed) {
|
if (node.computed) {
|
||||||
if (node.property.type === 'Literal' && typeof node.property.value === 'string') {
|
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[] {
|
function declsOfPattern(pattern: estree.Pattern | null): string[] {
|
||||||
if (pattern == null) return [];
|
if (pattern == null) return [];
|
||||||
switch (pattern?.type) {
|
switch (pattern.type) {
|
||||||
case "Identifier":
|
case 'Identifier':
|
||||||
return [pattern.name];
|
return [pattern.name];
|
||||||
case "ObjectPattern":
|
case 'ObjectPattern':
|
||||||
return pattern.properties.flatMap(prop => {
|
return pattern.properties.flatMap(prop => {
|
||||||
switch (prop.type) {
|
switch (prop.type) {
|
||||||
case 'Property':
|
case 'Property':
|
||||||
|
@ -254,16 +260,16 @@ function declsOfPattern(pattern: estree.Pattern | null): string[] {
|
||||||
case 'RestElement':
|
case 'RestElement':
|
||||||
return declsOfPattern(prop.argument);
|
return declsOfPattern(prop.argument);
|
||||||
default:
|
default:
|
||||||
assertNever(prop)
|
assertNever(prop);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
case "ArrayPattern":
|
case 'ArrayPattern':
|
||||||
return pattern.elements.flatMap(p => declsOfPattern(p));
|
return pattern.elements.flatMap(p => declsOfPattern(p));
|
||||||
case "RestElement":
|
case 'RestElement':
|
||||||
return declsOfPattern(pattern.argument);
|
return declsOfPattern(pattern.argument);
|
||||||
case "AssignmentPattern":
|
case 'AssignmentPattern':
|
||||||
return declsOfPattern(pattern.left);
|
return declsOfPattern(pattern.left);
|
||||||
case "MemberExpression":
|
case 'MemberExpression':
|
||||||
// assignment pattern so no new variable is declared
|
// assignment pattern so no new variable is declared
|
||||||
return [];
|
return [];
|
||||||
default:
|
default:
|
||||||
|
@ -375,15 +381,15 @@ type SpecifierResult =
|
||||||
|
|
||||||
function findImportSpecifier(programNode: ProgramNode, i18nFileName: string, i18nSymbol: string): SpecifierResult {
|
function findImportSpecifier(programNode: ProgramNode, i18nFileName: string, i18nSymbol: string): SpecifierResult {
|
||||||
const imports = programNode.body.filter(x => x.type === 'ImportDeclaration');
|
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' };
|
if (!importNode) return { type: 'no-import' };
|
||||||
assertType<AstNode>(importNode);
|
assertType<AstNode>(importNode);
|
||||||
|
|
||||||
if (importNode.specifiers.length == 0) {
|
if (importNode.specifiers.length === 0) {
|
||||||
return { type: 'no-specifiers', importNode };
|
return { type: 'no-specifiers', importNode };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (importNode.specifiers.length != 1) {
|
if (importNode.specifiers.length !== 1) {
|
||||||
return { type: 'unexpected-specifiers', importNode };
|
return { type: 'unexpected-specifiers', importNode };
|
||||||
}
|
}
|
||||||
const i18nImportSpecifier = importNode.specifiers[0];
|
const i18nImportSpecifier = importNode.specifiers[0];
|
||||||
|
|
|
@ -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 {
|
export interface Logger {
|
||||||
debug(message: string): void;
|
debug(message: string): void;
|
||||||
|
@ -27,7 +34,7 @@ export function createLogger(): RootLogger {
|
||||||
type LogContext = {
|
type LogContext = {
|
||||||
warningCount: number;
|
warningCount: number;
|
||||||
errorCount: number;
|
errorCount: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
function loggerFactory(prefix: string, context: LogContext): RootLogger {
|
function loggerFactory(prefix: string, context: LogContext): RootLogger {
|
||||||
return {
|
return {
|
||||||
|
@ -63,4 +70,4 @@ export const blankLogger: Logger = {
|
||||||
error: () => void 0,
|
error: () => void 0,
|
||||||
info: () => void 0,
|
info: () => void 0,
|
||||||
prefixed: () => blankLogger,
|
prefixed: () => blankLogger,
|
||||||
}
|
};
|
||||||
|
|
|
@ -4,18 +4,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as estreeWalker from 'estree-walker';
|
import * as estreeWalker from 'estree-walker';
|
||||||
import type { Plugin } from 'vite';
|
|
||||||
import type { CallExpression, Expression, Program, } from 'estree';
|
|
||||||
import MagicString from 'magic-string';
|
import MagicString from 'magic-string';
|
||||||
import type { AstNode } from 'rollup';
|
|
||||||
import { assertType } from './utils.js';
|
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
|
// 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.
|
// 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
|
// locale inliner cannot know minifiedSymbol(i18n) is 'unref(i18n)' or 'otherFunctionsWithEffect(i18n)' so
|
||||||
// it is necessary to remove unref calls before minification.
|
// it is necessary to remove unref calls before minification.
|
||||||
export default function pluginRemoveUnrefI18n(
|
export function pluginRemoveUnrefI18n(
|
||||||
{
|
{
|
||||||
i18nSymbolName = 'i18n',
|
i18nSymbolName = 'i18n',
|
||||||
}: {
|
}: {
|
||||||
|
@ -42,12 +42,12 @@ export default function pluginRemoveUnrefI18n(
|
||||||
magicString.remove(arg.end, node.end);
|
magicString.remove(arg.end, node.end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
code: magicString.toString(),
|
code: magicString.toString(),
|
||||||
map: magicString.generateMap({ hires: true }),
|
map: magicString.generateMap({ hires: true }),
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
export function assertNever(x: never): never {
|
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}`);
|
throw new Error(`Unexpected type: ${(x as any)?.type ?? x}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import locales from '../../locales/index.js';
|
||||||
import meta from '../../package.json';
|
import meta from '../../package.json';
|
||||||
import packageInfo from './package.json' with { type: 'json' };
|
import packageInfo from './package.json' with { type: 'json' };
|
||||||
import pluginJson5 from './vite.json5.js';
|
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 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;
|
const host = url ? (new URL(url)).hostname : undefined;
|
||||||
|
|
|
@ -28,7 +28,8 @@ type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedSt
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
type Tsx<T extends ILocale> = {
|
type Tsx<T extends ILocale> = {
|
||||||
readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString<infer P>
|
// `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<infer P>
|
||||||
? (arg: { readonly [_ in P]: string | number }) => string
|
? (arg: { readonly [_ in P]: string | number }) => string
|
||||||
// @ts-expect-error -- 証明省略
|
// @ts-expect-error -- 証明省略
|
||||||
: Tsx<T[K]>;
|
: Tsx<T[K]>;
|
||||||
|
|
|
@ -14,7 +14,7 @@ import pluginJson5 from './vite.json5.js';
|
||||||
import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.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 type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js';
|
||||||
import pluginWatchLocales from './lib/vite-plugin-watch-locales.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 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;
|
const host = url ? (new URL(url)).hostname : undefined;
|
||||||
|
|
Loading…
Reference in New Issue