diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 396536948..bc7b800d2 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -7,17 +7,257 @@
* BOOT LOADER
* サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。
* - 翻訳ファイルをフェッチする。
- * - バージョンに基づいて適切なメインスクリプトを読み込む。
+ * - 事前に挿入されたCLIENT_ENTRYを読んで適切なメインスクリプトを読み込む。
* - キャッシュされたコンパイル済みテーマを適用する。
* - クライアントの設定値に基づいて対応するHTMLクラス等を設定する。
+ * - もしメインスクリプトの読み込みなどでエラーが発生した場合は、renderErrorでエラーを描画する。
* テーマをこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。
- * 注: webpackは介さないため、このファイルではrequireやimportは使えません。
*/
'use strict';
+var misskey_loader = new Set();
+
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
-(async () => {
+function boot() {
+ const defaultSolutions = [
+ 'Clear the browser cache / ブラウザのキャッシュをクリアする',
+ 'Update your os and browser / ブラウザおよびOSを最新バージョンに更新する',
+ 'Disable an adblocker / アドブロッカーを無効にする',
+ '(Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する'
+ ];
+
+ const onErrorStyle = `
+ * {
+ font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+ }
+
+ #misskey_app,
+ #splash {
+ display: none !important;
+ }
+
+ body,
+ html {
+ background-color: #222;
+ color: #dfddcc;
+ justify-content: center;
+ margin: auto;
+ padding: 10px;
+ text-align: center;
+ }
+
+ button {
+ border-radius: 999px;
+ padding: 0px 12px 0px 12px;
+ border: none;
+ cursor: pointer;
+ margin-bottom: 12px;
+ }
+
+ .button-big {
+ background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
+ line-height: 50px;
+ }
+
+ .button-big:hover {
+ background: rgb(153, 204, 0);
+ }
+
+ .button-small {
+ background: #444;
+ line-height: 40px;
+ }
+
+ .button-small:hover {
+ background: #555;
+ }
+
+ .button-label-big {
+ color: #222;
+ font-weight: bold;
+ font-size: 1.2em;
+ padding: 12px;
+ }
+
+ .button-label-small {
+ color: rgb(153, 204, 0);
+ font-size: 16px;
+ padding: 12px;
+ }
+
+ a {
+ color: rgb(134, 179, 0);
+ text-decoration: none;
+ }
+
+ p,
+ li {
+ font-size: 16px;
+ }
+
+ .icon-warning {
+ color: #dec340;
+ height: 4rem;
+ padding-top: 2rem;
+ }
+
+ h1 {
+ font-size: 1.5em;
+ margin: 1em;
+ }
+
+ summary {
+ cursor: pointer;
+ }
+
+ code {
+ font-family: Fira, FiraCode, monospace;
+ }
+
+ #errors {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .errorInfo {
+ background: #333;
+ width: 40rem;
+ max-width: 100%;
+ border-radius: 10px;
+ justify-content: center;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ box-sizing: border-box;
+ }
+
+ .errorInfo > pre {
+ text-wrap: auto;
+ text-wrap: balance;
+ }
+ `;
+
+
+ const addStyle = (styleText) => {
+ try {
+ let css = document.createElement('style');
+ css.appendChild(document.createTextNode(styleText));
+ document.head.appendChild(css);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ const renderError = (code, details, solutions = defaultSolutions) => {
+ if (document.readyState === 'loading') {
+ window.addEventListener('DOMContentLoaded', () => {
+ renderError(code, details, solutions);
+ });
+ try {
+ addStyle(onErrorStyle);
+ } catch (e) { }
+ return;
+ }
+
+ let errorsElement = document.getElementById('errors');
+
+ if (!errorsElement) {
+ // エラー描画用のビューになっていない場合は、エラー描画用のビューに切り替える
+ document.body.innerHTML = `
+
+
Failed to load
読み込みに失敗しました
+
+ The following actions may solve the problem. / 以下を行うと解決する可能性があります。
+ ${solutions.map(x => `${x}
`).join('')}
+
+ Other options / その他のオプション
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ errorsElement = document.getElementById('errors');
+ }
+
+ if (typeof details === 'string') {
+ const errorEl = document.createElement('div');
+ errorEl.classList.add('errorInfo');
+
+ const titleCodeElement = document.createElement('code');
+ titleCodeElement.textContent = `ERROR CODE: ${code}`;
+ errorEl.appendChild(titleCodeElement);
+
+ errorEl.appendChild(document.createElement('br'));
+
+ const detailsCodeElement = document.createElement('code');
+ detailsCodeElement.textContent = details;
+ errorEl.appendChild(detailsCodeElement);
+
+ errorsElement.appendChild(errorEl);
+ } else if (details instanceof Error) {
+ const errorEl = document.createElement('details');
+ errorEl.classList.add('errorInfo');
+
+ const summaryElement = document.createElement('summary');
+ const titleCodeElement = document.createElement('code');
+ titleCodeElement.textContent = `ERROR CODE: ${code}`;
+ summaryElement.appendChild(titleCodeElement);
+ errorEl.appendChild(summaryElement);
+
+ const detailsPreElement = document.createElement('pre');
+ const detailsMessageElement = document.createElement('code');
+ detailsMessageElement.textContent = details.message;
+ detailsPreElement.appendChild(detailsMessageElement);
+ detailsPreElement.appendChild(document.createElement('br'));
+ const detailsCodeElement = document.createElement('code');
+ detailsCodeElement.textContent = details.stack;
+ detailsPreElement.appendChild(detailsCodeElement);
+ errorEl.appendChild(detailsPreElement);
+
+ errorsElement.appendChild(errorEl);
+ } else {
+ const errorEl = document.createElement('details');
+ errorEl.classList.add('errorInfo');
+
+ const summaryElement = document.createElement('summary');
+ const titleCodeElement = document.createElement('code');
+ titleCodeElement.textContent = `ERROR CODE: ${code}`;
+ summaryElement.appendChild(titleCodeElement);
+ errorEl.appendChild(summaryElement);
+
+ const detailsCodeElement = document.createElement('code');
+ detailsCodeElement.textContent = JSON.stringify(details);
+ errorEl.appendChild(detailsCodeElement);
+
+ errorsElement.appendChild(errorEl);
+ }
+
+ addStyle(onErrorStyle);
+ }
+
window.onerror = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED', e);
@@ -30,63 +270,65 @@
let forceError = localStorage.getItem('forceError');
if (forceError != null) {
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.')
+ renderError('FORCED_ERROR', Error('This error is forced by having forceError in local storage.'));
+ return;
}
- //#region Detect language & fetch translations
- if (!localStorage.hasOwnProperty('locale')) {
- const supportedLangs = LANGS;
- 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);
+ //#region After DOM loaded
+ async function oncontentload() {
+ const providedMetaEl = document.getElementById('misskey_meta');
+ const meta = providedMetaEl && providedMetaEl.textContent ? JSON.parse(providedMetaEl.textContent) : null;
+ const providedAt = providedMetaEl && providedMetaEl.dataset.generatedAt ? parseInt(providedMetaEl.dataset.generatedAt) : 0;
+ console.log('providedAt', providedAt, 'now', Date.now());
+ if (providedAt < Date.now() - 1000 * 60 * 60 * 24) {
+ // 古いデータがなぜか提供された場合は、エラーを描画する
+ renderError(
+ 'META_PROVIDED_AT_TOO_OLD',
+ 'This view is too old. Please reload.',
+ [
+ 'Reload / リロードする',
+ 'Clear the browser cache then reload / ブラウザのキャッシュをクリアしてリロードする',
+ 'Disable an adblocker / アドブロッカーを無効にする',
+ ]
+ );
+ return;
+ }
- // Fallback
- if (lang == null) lang = 'en-US';
+ //#region Detect language & fetch translations on first load
+ if (!localStorage.hasOwnProperty('locale')) {
+ const supportedLangs = LANGS;
+ 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';
+ }
+ }
+
+ const v = meta?.version;
+
+ // for https://github.com/misskey-dev/misskey/issues/10202
+ if (lang == null || lang.toString == null || lang.toString() === 'null') {
+ console.warn('invalid lang value detected!!!', typeof lang, lang);
+ lang = 'en-US';
+ }
+
+ const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
+ if (localRes.status === 200) {
+ localStorage.setItem('lang', lang);
+ localStorage.setItem('locale', await localRes.text());
+ localStorage.setItem('localeVersion', v);
+ } else {
+ renderError('LOCALE_FETCH');
+ return;
}
}
+ //#endregion
- const metaRes = await window.fetch('/api/meta', {
- method: 'POST',
- body: JSON.stringify({}),
- credentials: 'omit',
- cache: 'no-cache',
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- if (metaRes.status !== 200) {
- renderError('META_FETCH');
- return;
- }
- const meta = await metaRes.json();
- const v = meta.version;
- if (v == null) {
- renderError('META_FETCH_V');
- return;
- }
-
- // 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';
- }
-
- const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
- if (localRes.status === 200) {
- localStorage.setItem('lang', lang);
- localStorage.setItem('locale', await localRes.text());
- localStorage.setItem('localeVersion', v);
- } else {
- renderError('LOCALE_FETCH');
- return;
- }
- }
- //#endregion
-
- //#region Script
- async function importAppScript() {
await import(`/vite/${CLIENT_ENTRY}`)
.catch(async e => {
console.error(e);
@@ -94,12 +336,11 @@
});
}
- // タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
if (document.readyState !== 'loading') {
- importAppScript();
+ misskey_loader.add(oncontentload());
} else {
window.addEventListener('DOMContentLoaded', () => {
- importAppScript();
+ misskey_loader.add(oncontentload());
});
}
//#endregion
@@ -148,172 +389,6 @@
style.innerHTML = customCss;
document.head.appendChild(style);
}
+}
- async function addStyle(styleText) {
- let css = document.createElement('style');
- css.appendChild(document.createTextNode(styleText));
- document.head.appendChild(css);
- }
-
- function renderError(code, details) {
- let errorsElement = document.getElementById('errors');
-
- if (!errorsElement) {
- document.body.innerHTML = `
-
- Failed to load
読み込みに失敗しました
-
- The following actions may solve the problem. / 以下を行うと解決する可能性があります。
- Clear the browser cache / ブラウザのキャッシュをクリアする
- Update your os and browser / ブラウザおよびOSを最新バージョンに更新する
- Disable an adblocker / アドブロッカーを無効にする
- (Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する
-
- Other options / その他のオプション
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
- errorsElement = document.getElementById('errors');
- }
- const detailsElement = document.createElement('details');
- detailsElement.id = 'errorInfo';
- detailsElement.innerHTML = `
-
-
- ERROR CODE: ${code}
-
- ${JSON.stringify(details)}
`;
- errorsElement.appendChild(detailsElement);
- addStyle(`
- * {
- font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
- }
-
- #misskey_app,
- #splash {
- display: none !important;
- }
-
- body,
- html {
- background-color: #222;
- color: #dfddcc;
- justify-content: center;
- margin: auto;
- padding: 10px;
- text-align: center;
- }
-
- button {
- border-radius: 999px;
- padding: 0px 12px 0px 12px;
- border: none;
- cursor: pointer;
- margin-bottom: 12px;
- }
-
- .button-big {
- background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
- line-height: 50px;
- }
-
- .button-big:hover {
- background: rgb(153, 204, 0);
- }
-
- .button-small {
- background: #444;
- line-height: 40px;
- }
-
- .button-small:hover {
- background: #555;
- }
-
- .button-label-big {
- color: #222;
- font-weight: bold;
- font-size: 1.2em;
- padding: 12px;
- }
-
- .button-label-small {
- color: rgb(153, 204, 0);
- font-size: 16px;
- padding: 12px;
- }
-
- a {
- color: rgb(134, 179, 0);
- text-decoration: none;
- }
-
- p,
- li {
- font-size: 16px;
- }
-
- .icon-warning {
- color: #dec340;
- height: 4rem;
- padding-top: 2rem;
- }
-
- h1 {
- font-size: 1.5em;
- margin: 1em;
- }
-
- code {
- font-family: Fira, FiraCode, monospace;
- }
-
- #errorInfo {
- background: #333;
- margin-bottom: 2rem;
- padding: 0.5rem 1rem;
- width: 40rem;
- border-radius: 10px;
- justify-content: center;
- margin: auto;
- }
-
- #errorInfo summary {
- cursor: pointer;
- }
-
- #errorInfo summary > * {
- display: inline;
- }
-
- @media screen and (max-width: 500px) {
- #errorInfo {
- width: 50%;
- }
- `)
- }
-})();
+boot();