This commit is contained in:
anatawa12 2025-09-14 05:43:21 +05:30 committed by GitHub
commit b815b16ec4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 139 additions and 135 deletions

View File

@ -185,8 +185,10 @@ export type Config = {
driveUrl: string; driveUrl: string;
userAgent: string; userAgent: string;
frontendEntry: { file: string | null }; frontendEntry: { file: string | null };
frontendBootLoader: { file: string };
frontendManifestExists: boolean; frontendManifestExists: boolean;
frontendEmbedEntry: { file: string | null }; frontendEmbedEntry: { file: string | null };
frontendEmbedBootLoader: { file: string };
frontendEmbedManifestExists: boolean; frontendEmbedManifestExists: boolean;
mediaProxy: string; mediaProxy: string;
externalMediaProxyEnabled: boolean; externalMediaProxyEnabled: boolean;
@ -235,10 +237,10 @@ export function loadConfig(): Config {
const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json'); const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
const frontendManifest = frontendManifestExists ? const frontendManifest = frontendManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8')) JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
: { 'src/_boot_.ts': { file: null } }; : { 'src/_boot_.ts': { file: null }, 'src/_bootloader.ts': { file: 'src/_bootloader.ts' } };
const frontendEmbedManifest = frontendEmbedManifestExists ? const frontendEmbedManifest = frontendEmbedManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8')) JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
: { 'src/boot.ts': { file: null } }; : { 'src/boot.ts': { file: null }, 'src/_bootloader.ts': { file: 'src/_bootloader.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
@ -312,8 +314,10 @@ export function loadConfig(): Config {
: null, : null,
userAgent: `Misskey/${version} (${config.url})`, userAgent: `Misskey/${version} (${config.url})`,
frontendEntry: frontendManifest['src/_boot_.ts'], frontendEntry: frontendManifest['src/_boot_.ts'],
frontendBootLoader: frontendManifest['src/_bootloader.ts'],
frontendManifestExists: frontendManifestExists, frontendManifestExists: frontendManifestExists,
frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'], frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
frontendEmbedBootLoader: frontendEmbedManifest['src/_bootloader.ts'],
frontendEmbedManifestExists: frontendEmbedManifestExists, frontendEmbedManifestExists: frontendEmbedManifestExists,
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000, perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,

View File

@ -2,6 +2,7 @@ block vars
block loadClientEntry block loadClientEntry
- const entry = config.frontendEmbedEntry; - const entry = config.frontendEmbedEntry;
- const bootLoader = config.frontendEmbedBootLoader;
doctype html doctype html
@ -47,8 +48,7 @@ html(class='embed')
script(type='application/json' id='misskey_embedCtx' data-generated-at=now) script(type='application/json' id='misskey_embedCtx' data-generated-at=now)
!= embedCtx != embedCtx
script script(type='module', src=`/embed_vite/${bootLoader.file}`)
include ../boot.embed.js
body body
noscript: p noscript: p

View File

@ -2,6 +2,7 @@ block vars
block loadClientEntry block loadClientEntry
- const entry = config.frontendEntry; - const entry = config.frontendEntry;
- const bootLoader = config.frontendBootLoader;
- const baseUrl = config.url; - const baseUrl = config.url;
doctype html doctype html
@ -76,8 +77,7 @@ html
script(type='application/json' id='misskey_clientCtx' data-generated-at=now) script(type='application/json' id='misskey_clientCtx' data-generated-at=now)
!= clientCtx != clientCtx
script script(type='module', src=`/vite/${bootLoader.file}`)
include ../boot.js
body body
noscript: p noscript: p

View File

@ -6,6 +6,7 @@
type FIXME = any; type FIXME = any;
declare const _LANGS_: string[][]; declare const _LANGS_: string[][];
declare const _LANG_IDS_: string[];
declare const _VERSION_: string; declare const _VERSION_: string;
declare const _ENV_: string; declare const _ENV_: string;
declare const _DEV_: boolean; declare const _DEV_: boolean;

View File

@ -5,18 +5,24 @@
'use strict'; 'use strict';
import { addStyle, bootloaderLocales, detectLanguage } from '@@/js/bootloader';
interface Window {
CLIENT_ENTRY: string | undefined;
}
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので // ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
(async () => { (async () => {
window.onerror = (e) => { window.onerror = (error) => {
console.error(e); console.error(error);
renderError('SOMETHING_HAPPENED'); renderError('SOMETHING_HAPPENED');
}; };
window.onunhandledrejection = (e) => { window.onunhandledrejection = (error) => {
console.error(e); console.error(error);
renderError('SOMETHING_HAPPENED_IN_PROMISE'); renderError('SOMETHING_HAPPENED_IN_PROMISE');
}; };
let forceError = localStorage.getItem('forceError'); const forceError = localStorage.getItem('forceError');
if (forceError != null) { if (forceError != null) {
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
return; return;
@ -31,33 +37,13 @@
document.documentElement.classList.add('noborder'); document.documentElement.classList.add('noborder');
} }
//#region Detect language & fetch translations const lang = detectLanguage();
const supportedLangs = LANGS;
/** @type { string } */
let lang = localStorage.getItem('lang');
if (lang == null || !supportedLangs.includes(lang)) {
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
// Fallback
if (lang == null) lang = 'en-US';
}
}
// for https://github.com/misskey-dev/misskey/issues/10202
if (lang == null || lang.toString == null || lang.toString() === 'null') {
console.error('invalid lang value detected!!!', typeof lang, lang);
lang = 'en-US';
}
//#endregion
//#region Script //#region Script
async function importAppScript() { async function importAppScript() {
await import(CLIENT_ENTRY ? `/embed_vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/embed_vite/src/_boot_.ts') await import(`/embed_vite/${(window.CLIENT_ENTRY ?? 'src/boot.ts').replace('scripts', lang)}`)
.catch(async e => { .catch(async error => {
console.error(e); console.error(error);
renderError('APP_IMPORT'); renderError('APP_IMPORT');
}); });
} }
@ -72,44 +58,19 @@
} }
//#endregion //#endregion
async function addStyle(styleText) { async function renderError(code: string, _details?) {
let css = document.createElement('style');
css.appendChild(document.createTextNode(styleText));
document.head.appendChild(css);
}
async function renderError(code) {
// Cannot set property 'innerHTML' of null を回避 // Cannot set property 'innerHTML' of null を回避
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
} }
let messages = null; const messages = bootloaderLocales();
const bootloaderLocales = localStorage.getItem('bootloaderLocales');
if (bootloaderLocales) {
messages = JSON.parse(bootloaderLocales);
}
if (!messages) {
// older version of misskey does not store bootloaderLocales, stores locale as a whole
const legacyLocale = localStorage.getItem('locale');
if (legacyLocale) {
const parsed = JSON.parse(legacyLocale);
messages = {
...(parsed._bootErrors ?? {}),
reload: parsed.reload,
};
}
}
if (!messages) messages = {};
const title = messages?.title || 'Failed to initialize Misskey';
const reload = messages?.reload || 'Reload';
document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg> document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
<div class="message">${title}</div> <div class="message">${messages.title}</div>
<div class="submessage">Error Code: ${code}</div> <div class="submessage">Error Code: ${code}</div>
<button onclick="location.reload(!0)"> <button onclick="location.reload(!0)">
<div>${reload}</div> <div>${messages.reload}</div>
</button>`; </button>`;
addStyle(` addStyle(`
#misskey_app, #misskey_app,

View File

@ -121,6 +121,7 @@ export function getConfig(): UserConfig {
define: { define: {
_VERSION_: JSON.stringify(meta.version), _VERSION_: JSON.stringify(meta.version),
_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])),
_LANG_IDS_: JSON.stringify(Object.keys(locales)),
_ENV_: JSON.stringify(process.env.NODE_ENV), _ENV_: JSON.stringify(process.env.NODE_ENV),
_DEV_: process.env.NODE_ENV !== 'production', _DEV_: process.env.NODE_ENV !== 'production',
_PERF_PREFIX_: JSON.stringify('Misskey:'), _PERF_PREFIX_: JSON.stringify('Misskey:'),
@ -139,6 +140,7 @@ export function getConfig(): UserConfig {
input: { input: {
i18n: './src/i18n.ts', i18n: './src/i18n.ts',
entry: './src/boot.ts', entry: './src/boot.ts',
bootloader: './src/_bootloader.ts',
}, },
external: externalPackages.map(p => p.match), external: externalPackages.map(p => p.match),
preserveEntrySignatures: 'allow-extension', preserveEntrySignatures: 'allow-extension',

View File

@ -7,6 +7,7 @@
type FIXME = any; type FIXME = any;
declare const _LANGS_: string[][]; declare const _LANGS_: string[][];
declare const _LANG_IDS_: string[];
declare const _VERSION_: string; declare const _VERSION_: string;
declare const _ENV_: string; declare const _ENV_: string;
declare const _DEV_: boolean; declare const _DEV_: boolean;

View File

@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// common portion of bootloader for frontend and frontend-embed
import type { Locale } from '../../../locales/index.js';
export function detectLanguage(): string {
const supportedLangs = _LANG_IDS_;
let lang: string | null | undefined = localStorage.getItem('lang');
if (lang == null || !supportedLangs.includes(lang)) {
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
// Fallback
lang ??= 'en-US';
}
}
// for https://github.com/misskey-dev/misskey/issues/10202
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (lang == null || lang.toString == null || lang.toString() === 'null') {
console.error('invalid lang value detected!!!', typeof lang, lang);
lang = 'en-US';
}
return lang;
}
type BootLoaderLocales = Locale['_bootErrors'] & Pick<Locale, 'reload'>;
export function bootloaderLocales(): BootLoaderLocales {
let messages: Partial<BootLoaderLocales> | null = null;
const bootloaderLocalesJson = localStorage.getItem('bootloaderLocales');
if (bootloaderLocalesJson) {
messages = JSON.parse(bootloaderLocalesJson);
}
if (!messages) {
// older version of misskey does not store bootloaderLocales, stores locale as a whole
const legacyLocale = localStorage.getItem('locale');
if (legacyLocale) {
const parsed = JSON.parse(legacyLocale);
messages = {
...(parsed._bootErrors ?? {}),
reload: parsed.reload,
};
}
}
return Object.assign({
title: 'Failed to initialize Misskey',
solution: 'The following actions may solve the problem.',
solution1: 'Update your os and browser',
solution2: 'Disable an adblocker',
solution3: 'Clear the browser cache',
solution4: '(Tor Browser) Set dom.webaudio.enabled to true',
otherOption: 'Other options',
otherOption1: 'Clear preferences and cache',
otherOption2: 'Start the simple client',
otherOption3: 'Start the repair tool',
otherOption4: 'Start Misskey in safe mode',
reload: 'Reload',
}, messages) as BootLoaderLocales;
}
export function addStyle(styleText: string) {
const styleElement = document.createElement('style');
styleElement.appendChild(document.createTextNode(styleText));
document.head.appendChild(styleElement);
}

View File

@ -6,6 +6,7 @@
type FIXME = any; type FIXME = any;
declare const _LANGS_: string[][]; declare const _LANGS_: string[][];
declare const _LANG_IDS_: string[];
declare const _VERSION_: string; declare const _VERSION_: string;
declare const _ENV_: string; declare const _ENV_: string;
declare const _DEV_: boolean; declare const _DEV_: boolean;

View File

@ -3,50 +3,40 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/* eslint no-restricted-globals: off */
'use strict'; 'use strict';
import { addStyle, bootloaderLocales, detectLanguage } from '@@/js/bootloader';
interface Window {
CLIENT_ENTRY: string | undefined;
}
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので // ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
(async () => { (async () => {
window.onerror = (e) => { const CLIENT_ENTRY = window.CLIENT_ENTRY;
console.error(e);
renderError('SOMETHING_HAPPENED', e); window.onerror = (error) => {
console.error(error);
renderError('SOMETHING_HAPPENED', error);
}; };
window.onunhandledrejection = (e) => { window.onunhandledrejection = (error) => {
console.error(e); console.error(error);
renderError('SOMETHING_HAPPENED_IN_PROMISE', e); renderError('SOMETHING_HAPPENED_IN_PROMISE', error);
}; };
let forceError = localStorage.getItem('forceError'); const forceError = localStorage.getItem('forceError');
if (forceError != null) { if (forceError != null) {
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
return; return;
} }
//#region Detect language const lang = detectLanguage();
const supportedLangs = LANGS;
/** @type { string } */
let lang = localStorage.getItem('lang');
if (lang == null || !supportedLangs.includes(lang)) {
if (supportedLangs.includes(navigator.language)) {
lang = navigator.language;
} else {
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
// Fallback
if (lang == null) lang = 'en-US';
}
}
// for https://github.com/misskey-dev/misskey/issues/10202
if (lang == null || lang.toString == null || lang.toString() === 'null') {
console.error('invalid lang value detected!!!', typeof lang, lang);
lang = 'en-US';
}
//#endregion
//#region Script //#region Script
async function importAppScript() { async function importAppScript() {
await import(CLIENT_ENTRY ? `/vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/vite/src/_boot_.ts') await import(`/vite/${(CLIENT_ENTRY ?? 'src/_boot_.ts').replace('scripts', lang)}`)
.catch(async e => { .catch(async e => {
console.error(e); console.error(e);
renderError('APP_IMPORT', e); renderError('APP_IMPORT', e);
@ -78,7 +68,7 @@
if (!isSafeMode) { if (!isSafeMode) {
const theme = localStorage.getItem('theme'); const theme = localStorage.getItem('theme');
if (theme) { if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) { for (const [k, v] of Object.entries(JSON.parse(theme) as Record<string, string>)) {
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
// HTMLの theme-color 適用 // HTMLの theme-color 適用
@ -119,50 +109,13 @@
} }
} }
async function addStyle(styleText) {
let css = document.createElement('style');
css.appendChild(document.createTextNode(styleText));
document.head.appendChild(css);
}
async function renderError(code, details) { async function renderError(code, details) {
// Cannot set property 'innerHTML' of null を回避 // Cannot set property 'innerHTML' of null を回避
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
} }
let messages = null; const messages = bootloaderLocales();
const bootloaderLocales = localStorage.getItem('bootloaderLocales');
if (bootloaderLocales) {
messages = JSON.parse(bootloaderLocales);
}
if (!messages) {
// older version of misskey does not store bootloaderLocales, stores locale as a whole
const legacyLocale = localStorage.getItem('locale');
if (legacyLocale) {
const parsed = JSON.parse(legacyLocale);
messages = {
...(parsed._bootErrors ?? {}),
reload: parsed.reload,
};
}
}
if (!messages) messages = {};
messages = Object.assign({
title: 'Failed to initialize Misskey',
solution: 'The following actions may solve the problem.',
solution1: 'Update your os and browser',
solution2: 'Disable an adblocker',
solution3: 'Clear the browser cache',
solution4: '(Tor Browser) Set dom.webaudio.enabled to true',
otherOption: 'Other options',
otherOption1: 'Clear preferences and cache',
otherOption2: 'Start the simple client',
otherOption3: 'Start the repair tool',
otherOption4: 'Start Misskey in safe mode',
reload: 'Reload',
}, messages);
const safeModeUrl = new URL(window.location.href); const safeModeUrl = new URL(window.location.href);
safeModeUrl.searchParams.set('safemode', 'true'); safeModeUrl.searchParams.set('safemode', 'true');
@ -214,7 +167,7 @@
<br> <br>
<div id="errors"></div> <div id="errors"></div>
`; `;
errorsElement = document.getElementById('errors'); errorsElement = document.getElementById('errors')!;
} }
const detailsElement = document.createElement('details'); const detailsElement = document.createElement('details');
detailsElement.id = 'errorInfo'; detailsElement.id = 'errorInfo';

View File

@ -25,3 +25,8 @@ declare module 'search-index:settings' {
declare module 'search-index:admin' { declare module 'search-index:admin' {
export const searchIndexes: XGeneratedSearchIndexItem[]; export const searchIndexes: XGeneratedSearchIndexItem[];
} }
declare module 'virtual:supported-langs' {
const value: string[];
export default value;
}

View File

@ -160,6 +160,7 @@ export function getConfig(): UserConfig {
define: { define: {
_VERSION_: JSON.stringify(meta.version), _VERSION_: JSON.stringify(meta.version),
_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])),
_LANG_IDS_: JSON.stringify(Object.keys(locales)),
_ENV_: JSON.stringify(process.env.NODE_ENV), _ENV_: JSON.stringify(process.env.NODE_ENV),
_DEV_: process.env.NODE_ENV !== 'production', _DEV_: process.env.NODE_ENV !== 'production',
_PERF_PREFIX_: JSON.stringify('Misskey:'), _PERF_PREFIX_: JSON.stringify('Misskey:'),
@ -178,6 +179,7 @@ export function getConfig(): UserConfig {
input: { input: {
i18n: './src/i18n.ts', i18n: './src/i18n.ts',
entry: './src/_boot_.ts', entry: './src/_boot_.ts',
bootloader: './src/_bootloader.ts',
}, },
external: externalPackages.map(p => p.match), external: externalPackages.map(p => p.match),
preserveEntrySignatures: 'allow-extension', preserveEntrySignatures: 'allow-extension',

View File

@ -53,8 +53,8 @@ async function buildBackendScript() {
await fs.mkdir('./packages/backend/built/server/web', { recursive: true }); await fs.mkdir('./packages/backend/built/server/web', { recursive: true });
for (const file of [ for (const file of [
'./packages/backend/src/server/web/boot.js', //'./packages/backend/src/server/web/boot.js',
'./packages/backend/src/server/web/boot.embed.js', //'./packages/backend/src/server/web/boot.embed.js',
'./packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/bios.js',
'./packages/backend/src/server/web/cli.js', './packages/backend/src/server/web/cli.js',
'./packages/backend/src/server/web/error.js', './packages/backend/src/server/web/error.js',