forked from mirrors/misskey
fix(backend): 初期読込時に必要なフロントエンドのアセットがすべて読み込まれていない問題を修正 (#17254)
* fix: バックエンドのCSS読み込みの方法が悪いのを修正 * fix: 使用されないpreloadを削除 * Update Changelog * add comments
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
- Fix: 署名付きGETリクエストにおいてAcceptヘッダを署名の対象から除外(Acceptヘッダを正規化するCDNやリバースプロキシを使用している際に挙動がおかしくなる問題を修正)
|
- Fix: 署名付きGETリクエストにおいてAcceptヘッダを署名の対象から除外(Acceptヘッダを正規化するCDNやリバースプロキシを使用している際に挙動がおかしくなる問題を修正)
|
||||||
- Fix: WebSocket接続におけるノートの非表示ロジックを修正
|
- Fix: WebSocket接続におけるノートの非表示ロジックを修正
|
||||||
- Fix: チャンネルミュートを有効にしている際に、一部のタイムラインやノート一覧が空になる問題を修正
|
- Fix: チャンネルミュートを有効にしている際に、一部のタイムラインやノート一覧が空になる問題を修正
|
||||||
|
- Fix: 初期読込時に必要なフロントエンドのアセットがすべて読み込まれていない問題を修正
|
||||||
|
|
||||||
|
|
||||||
## 2026.3.1
|
## 2026.3.1
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { type FastifyServerOptions } from 'fastify';
|
|||||||
import type * as Sentry from '@sentry/node';
|
import type * as Sentry from '@sentry/node';
|
||||||
import type * as SentryVue from '@sentry/vue';
|
import type * as SentryVue from '@sentry/vue';
|
||||||
import type { RedisOptions } from 'ioredis';
|
import type { RedisOptions } from 'ioredis';
|
||||||
import type { ManifestChunk } from 'vite';
|
|
||||||
|
|
||||||
type RedisOptionsSource = Partial<RedisOptions> & {
|
type RedisOptionsSource = Partial<RedisOptions> & {
|
||||||
host: string;
|
host: string;
|
||||||
@@ -189,9 +188,7 @@ export type Config = {
|
|||||||
authUrl: string;
|
authUrl: string;
|
||||||
driveUrl: string;
|
driveUrl: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
frontendEntry: ManifestChunk;
|
|
||||||
frontendManifestExists: boolean;
|
frontendManifestExists: boolean;
|
||||||
frontendEmbedEntry: ManifestChunk;
|
|
||||||
frontendEmbedManifestExists: boolean;
|
frontendEmbedManifestExists: boolean;
|
||||||
mediaProxy: string;
|
mediaProxy: string;
|
||||||
externalMediaProxyEnabled: boolean;
|
externalMediaProxyEnabled: boolean;
|
||||||
@@ -250,12 +247,6 @@ export function loadConfig(): Config {
|
|||||||
|
|
||||||
const frontendManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'));
|
const frontendManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'));
|
||||||
const frontendEmbedManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'));
|
const frontendEmbedManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'));
|
||||||
const frontendManifest = frontendManifestExists ?
|
|
||||||
JSON.parse(fs.readFileSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'), 'utf-8'))
|
|
||||||
: { 'src/_boot_.ts': { file: null } };
|
|
||||||
const frontendEmbedManifest = frontendEmbedManifestExists ?
|
|
||||||
JSON.parse(fs.readFileSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'), 'utf-8'))
|
|
||||||
: { 'src/boot.ts': { file: null } };
|
|
||||||
|
|
||||||
const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source;
|
const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source;
|
||||||
|
|
||||||
@@ -337,9 +328,7 @@ export function loadConfig(): Config {
|
|||||||
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
|
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
|
||||||
: null,
|
: null,
|
||||||
userAgent: `Misskey/${version} (${config.url})`,
|
userAgent: `Misskey/${version} (${config.url})`,
|
||||||
frontendEntry: frontendManifest['src/_boot_.ts'],
|
|
||||||
frontendManifestExists: frontendManifestExists,
|
frontendManifestExists: frontendManifestExists,
|
||||||
frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
|
|
||||||
frontendEmbedManifestExists: frontendEmbedManifestExists,
|
frontendEmbedManifestExists: frontendEmbedManifestExists,
|
||||||
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
||||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
|
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { dirname } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { promises as fsp } from 'node:fs';
|
import { promises as fsp, existsSync } from 'node:fs';
|
||||||
import { languages } from 'i18n/const';
|
import { languages } from 'i18n/const';
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@@ -13,21 +13,34 @@ import { bindThis } from '@/decorators.js';
|
|||||||
import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js';
|
import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js';
|
||||||
import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
|
import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
|
||||||
import type { FastifyReply } from 'fastify';
|
import type { FastifyReply } from 'fastify';
|
||||||
|
import type { Manifest } from 'vite';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiMeta } from '@/models/Meta.js';
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
import type { CommonData } from './views/_.js';
|
import type { CommonData, ViteFiles } from './views/_.js';
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
|
|
||||||
const frontendVitePublic = `${_dirname}/../../../../frontend/public/`;
|
let rootDir = _dirname;
|
||||||
const frontendEmbedVitePublic = `${_dirname}/../../../../frontend-embed/public/`;
|
// 見つかるまで上に遡る
|
||||||
|
while (!existsSync(resolve(rootDir, 'packages'))) {
|
||||||
|
const parentDir = dirname(rootDir);
|
||||||
|
if (parentDir === rootDir) {
|
||||||
|
throw new Error('Cannot find root directory');
|
||||||
|
}
|
||||||
|
rootDir = parentDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontendViteBuilt = resolve(rootDir, 'built/_frontend_vite_');
|
||||||
|
const frontendEmbedViteBuilt = resolve(rootDir, 'built/_frontend_embed_vite_');
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HtmlTemplateService {
|
export class HtmlTemplateService {
|
||||||
private frontendBootloadersFetched = false;
|
private frontendAssetsFetched = false;
|
||||||
|
public frontendViteFiles: ViteFiles | null = null;
|
||||||
public frontendBootloaderJs: string | null = null;
|
public frontendBootloaderJs: string | null = null;
|
||||||
public frontendBootloaderCss: string | null = null;
|
public frontendBootloaderCss: string | null = null;
|
||||||
|
public frontendEmbedViteFiles: ViteFiles | null = null;
|
||||||
public frontendEmbedBootloaderJs: string | null = null;
|
public frontendEmbedBootloaderJs: string | null = null;
|
||||||
public frontendEmbedBootloaderCss: string | null = null;
|
public frontendEmbedBootloaderCss: string | null = null;
|
||||||
|
|
||||||
@@ -42,18 +55,92 @@ export class HtmlTemplateService {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初期ロードで読み込むべきファイルのパスを収集する。
|
||||||
|
// See https://ja.vite.dev/guide/backend-integration
|
||||||
@bindThis
|
@bindThis
|
||||||
private async prepareFrontendBootloaders() {
|
private collectViteAssetFiles(manifest: Manifest): ViteFiles {
|
||||||
if (this.frontendBootloadersFetched) return;
|
const entryFile = Object.values(manifest).find((chunk) => chunk.isEntry);
|
||||||
this.frontendBootloadersFetched = true;
|
if (!entryFile) return {
|
||||||
|
entryJs: null,
|
||||||
|
css: [],
|
||||||
|
modulePreloads: [],
|
||||||
|
};
|
||||||
|
|
||||||
const [bootJs, bootCss, embedBootJs, embedBootCss] = await Promise.all([
|
const seenChunkIds = new Set<string>();
|
||||||
fsp.readFile(`${frontendVitePublic}loader/boot.js`, 'utf-8').catch(() => null),
|
const cssFiles = new Set<string>();
|
||||||
fsp.readFile(`${frontendVitePublic}loader/style.css`, 'utf-8').catch(() => null),
|
const modulePreloads = new Set<string>();
|
||||||
fsp.readFile(`${frontendEmbedVitePublic}loader/boot.js`, 'utf-8').catch(() => null),
|
|
||||||
fsp.readFile(`${frontendEmbedVitePublic}loader/style.css`, 'utf-8').catch(() => null),
|
if (entryFile.css) {
|
||||||
|
entryFile.css.forEach((css) => cssFiles.add(css));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryFile.imports != null && Array.isArray(entryFile.imports)) {
|
||||||
|
function collectImports(imports: string[], recursive = false) {
|
||||||
|
for (const importId of imports) {
|
||||||
|
if (seenChunkIds.has(importId)) continue;
|
||||||
|
seenChunkIds.add(importId);
|
||||||
|
|
||||||
|
const importedChunk = manifest[importId];
|
||||||
|
if (!importedChunk) return;
|
||||||
|
|
||||||
|
if (importedChunk.css) {
|
||||||
|
importedChunk.css.forEach((css) => cssFiles.add(css));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importedChunk.imports != null && Array.isArray(importedChunk.imports)) {
|
||||||
|
collectImports(importedChunk.imports, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recursive) {
|
||||||
|
modulePreloads.add(importedChunk.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collectImports(entryFile.imports);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
entryJs: entryFile.file,
|
||||||
|
css: Array.from(cssFiles),
|
||||||
|
modulePreloads: Array.from(modulePreloads),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async prepareFrontendAssets() {
|
||||||
|
if (this.frontendAssetsFetched) return;
|
||||||
|
this.frontendAssetsFetched = true;
|
||||||
|
|
||||||
|
const [
|
||||||
|
bootJs,
|
||||||
|
bootCss,
|
||||||
|
embedBootJs,
|
||||||
|
embedBootCss,
|
||||||
|
] = await Promise.all([
|
||||||
|
fsp.readFile(resolve(frontendViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null),
|
||||||
|
fsp.readFile(resolve(frontendViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null),
|
||||||
|
fsp.readFile(resolve(frontendEmbedViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null),
|
||||||
|
fsp.readFile(resolve(frontendEmbedViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
let feViteManifest: Manifest | null = null;
|
||||||
|
let embedFeViteManifest: Manifest | null = null;
|
||||||
|
|
||||||
|
if (this.config.frontendManifestExists) {
|
||||||
|
const manifestContent = await fsp.readFile(resolve(frontendViteBuilt, 'manifest.json'), 'utf-8').catch(() => null);
|
||||||
|
feViteManifest = manifestContent ? JSON.parse(manifestContent) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.frontendEmbedManifestExists) {
|
||||||
|
const manifestContent = await fsp.readFile(resolve(frontendEmbedViteBuilt, 'manifest.json'), 'utf-8').catch(() => null);
|
||||||
|
embedFeViteManifest = manifestContent ? JSON.parse(manifestContent) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feViteManifest != null) {
|
||||||
|
this.frontendViteFiles = this.collectViteAssetFiles(feViteManifest);
|
||||||
|
}
|
||||||
|
|
||||||
if (bootJs != null) {
|
if (bootJs != null) {
|
||||||
this.frontendBootloaderJs = bootJs;
|
this.frontendBootloaderJs = bootJs;
|
||||||
}
|
}
|
||||||
@@ -62,6 +149,10 @@ export class HtmlTemplateService {
|
|||||||
this.frontendBootloaderCss = bootCss;
|
this.frontendBootloaderCss = bootCss;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (embedFeViteManifest != null) {
|
||||||
|
this.frontendEmbedViteFiles = this.collectViteAssetFiles(embedFeViteManifest);
|
||||||
|
}
|
||||||
|
|
||||||
if (embedBootJs != null) {
|
if (embedBootJs != null) {
|
||||||
this.frontendEmbedBootloaderJs = embedBootJs;
|
this.frontendEmbedBootloaderJs = embedBootJs;
|
||||||
}
|
}
|
||||||
@@ -73,7 +164,7 @@ export class HtmlTemplateService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getCommonData(): Promise<CommonData> {
|
public async getCommonData(): Promise<CommonData> {
|
||||||
await this.prepareFrontendBootloaders();
|
await this.prepareFrontendAssets();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: this.config.version,
|
version: this.config.version,
|
||||||
@@ -90,8 +181,10 @@ export class HtmlTemplateService {
|
|||||||
metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(this.meta)),
|
metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(this.meta)),
|
||||||
now: Date.now(),
|
now: Date.now(),
|
||||||
federationEnabled: this.meta.federation !== 'none',
|
federationEnabled: this.meta.federation !== 'none',
|
||||||
|
frontendViteFiles: this.frontendViteFiles,
|
||||||
frontendBootloaderJs: this.frontendBootloaderJs,
|
frontendBootloaderJs: this.frontendBootloaderJs,
|
||||||
frontendBootloaderCss: this.frontendBootloaderCss,
|
frontendBootloaderCss: this.frontendBootloaderCss,
|
||||||
|
frontendEmbedViteFiles: this.frontendEmbedViteFiles,
|
||||||
frontendEmbedBootloaderJs: this.frontendEmbedBootloaderJs,
|
frontendEmbedBootloaderJs: this.frontendEmbedBootloaderJs,
|
||||||
frontendEmbedBootloaderCss: this.frontendEmbedBootloaderCss,
|
frontendEmbedBootloaderCss: this.frontendEmbedBootloaderCss,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ export type MinimumCommonData = {
|
|||||||
config: Config;
|
config: Config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ViteFiles = {
|
||||||
|
entryJs: string | null;
|
||||||
|
css: string[];
|
||||||
|
modulePreloads: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type CommonData = MinimumCommonData & {
|
export type CommonData = MinimumCommonData & {
|
||||||
langs: string[];
|
langs: string[];
|
||||||
instanceName: string;
|
instanceName: string;
|
||||||
@@ -36,8 +42,10 @@ export type CommonData = MinimumCommonData & {
|
|||||||
instanceUrl: string;
|
instanceUrl: string;
|
||||||
now: number;
|
now: number;
|
||||||
federationEnabled: boolean;
|
federationEnabled: boolean;
|
||||||
|
frontendViteFiles: ViteFiles | null;
|
||||||
frontendBootloaderJs: string | null;
|
frontendBootloaderJs: string | null;
|
||||||
frontendBootloaderCss: string | null;
|
frontendBootloaderCss: string | null;
|
||||||
|
frontendEmbedViteFiles: ViteFiles | null;
|
||||||
frontendEmbedBootloaderJs: string | null;
|
frontendEmbedBootloaderJs: string | null;
|
||||||
frontendEmbedBootloaderCss: string | null;
|
frontendEmbedBootloaderCss: string | null;
|
||||||
metaJson?: string;
|
metaJson?: string;
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ export function BaseEmbed(props: PropsWithChildren<CommonProps<{
|
|||||||
<link rel="icon" href={props.icon ?? '/favicon.ico'} />
|
<link rel="icon" href={props.icon ?? '/favicon.ico'} />
|
||||||
<link rel="apple-touch-icon" href={props.appleTouchIcon ?? '/apple-touch-icon.png'} />
|
<link rel="apple-touch-icon" href={props.appleTouchIcon ?? '/apple-touch-icon.png'} />
|
||||||
|
|
||||||
{!props.config.frontendEmbedManifestExists ? <script type="module" src="/embed_vite/@vite/client"></script> : null}
|
{props.frontendEmbedViteFiles == null ? <script type="module" src="/embed_vite/@vite/client"></script> : null}
|
||||||
|
|
||||||
{props.config.frontendEmbedEntry.css != null ? props.config.frontendEmbedEntry.css.map((href) => (
|
{(props.frontendEmbedViteFiles?.css ?? []).map((href) => (
|
||||||
<link rel="stylesheet" href={`/embed_vite/${href}`} />
|
<link rel="stylesheet" href={`/embed_vite/${href}`} />
|
||||||
)) : null}
|
))}
|
||||||
|
|
||||||
{props.titleSlot ?? <title safe>{props.title || 'Misskey'}</title>}
|
{props.titleSlot ?? <title safe>{props.title || 'Misskey'}</title>}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export function BaseEmbed(props: PropsWithChildren<CommonProps<{
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const VERSION = '{props.version}';
|
const VERSION = '{props.version}';
|
||||||
const CLIENT_ENTRY = {JSON.stringify(props.config.frontendEmbedEntry.file)};
|
const CLIENT_ENTRY = {JSON.stringify(props.frontendEmbedViteFiles?.entryJs ?? null)};
|
||||||
const LANGS = {JSON.stringify(props.langs)};
|
const LANGS = {JSON.stringify(props.langs)};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ export function Layout(props: PropsWithChildren<CommonProps<{
|
|||||||
{props.infoImageUrl != null ? <link rel="prefetch" as="image" href={props.infoImageUrl} /> : null}
|
{props.infoImageUrl != null ? <link rel="prefetch" as="image" href={props.infoImageUrl} /> : null}
|
||||||
{props.notFoundImageUrl != null ? <link rel="prefetch" as="image" href={props.notFoundImageUrl} /> : null}
|
{props.notFoundImageUrl != null ? <link rel="prefetch" as="image" href={props.notFoundImageUrl} /> : null}
|
||||||
|
|
||||||
{!props.config.frontendManifestExists ? <script type="module" src="/vite/@vite/client"></script> : null}
|
{props.frontendViteFiles == null ? <script type="module" src="/vite/@vite/client"></script> : null}
|
||||||
|
|
||||||
{props.config.frontendEntry.css != null ? props.config.frontendEntry.css.map((href) => (
|
{(props.frontendViteFiles?.css ?? []).map((href) => (
|
||||||
<link rel="stylesheet" href={`/vite/${href}`} />
|
<link rel="stylesheet" href={`/vite/${href}`} />
|
||||||
)) : null}
|
))}
|
||||||
|
|
||||||
{props.titleSlot ?? <title safe>{props.title || 'Misskey'}</title>}
|
{props.titleSlot ?? <title safe>{props.title || 'Misskey'}</title>}
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ export function Layout(props: PropsWithChildren<CommonProps<{
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const VERSION = '{props.version}';
|
const VERSION = '{props.version}';
|
||||||
const CLIENT_ENTRY = {JSON.stringify(props.config.frontendEntry.file)};
|
const CLIENT_ENTRY = {JSON.stringify(props.frontendViteFiles?.entryJs ?? null)};
|
||||||
const LANGS = {JSON.stringify(props.langs)};
|
const LANGS = {JSON.stringify(props.langs)};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user