diff --git a/CHANGELOG.md b/CHANGELOG.md index c2f4cf8acd..c69f05e972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Fix: 署名付きGETリクエストにおいてAcceptヘッダを署名の対象から除外(Acceptヘッダを正規化するCDNやリバースプロキシを使用している際に挙動がおかしくなる問題を修正) - Fix: WebSocket接続におけるノートの非表示ロジックを修正 - Fix: チャンネルミュートを有効にしている際に、一部のタイムラインやノート一覧が空になる問題を修正 +- Fix: 初期読込時に必要なフロントエンドのアセットがすべて読み込まれていない問題を修正 ## 2026.3.1 diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 4cd82bed87..6a83359d38 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -10,7 +10,6 @@ import { type FastifyServerOptions } from 'fastify'; import type * as Sentry from '@sentry/node'; import type * as SentryVue from '@sentry/vue'; import type { RedisOptions } from 'ioredis'; -import type { ManifestChunk } from 'vite'; type RedisOptionsSource = Partial & { host: string; @@ -189,9 +188,7 @@ export type Config = { authUrl: string; driveUrl: string; userAgent: string; - frontendEntry: ManifestChunk; frontendManifestExists: boolean; - frontendEmbedEntry: ManifestChunk; frontendEmbedManifestExists: boolean; mediaProxy: string; externalMediaProxyEnabled: boolean; @@ -250,12 +247,6 @@ export function loadConfig(): Config { const frontendManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_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; @@ -337,9 +328,7 @@ export function loadConfig(): Config { config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator : null, userAgent: `Misskey/${version} (${config.url})`, - frontendEntry: frontendManifest['src/_boot_.ts'], frontendManifestExists: frontendManifestExists, - frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'], frontendEmbedManifestExists: frontendEmbedManifestExists, perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000, perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, diff --git a/packages/backend/src/server/web/HtmlTemplateService.ts b/packages/backend/src/server/web/HtmlTemplateService.ts index 8ff985530d..36272c81d5 100644 --- a/packages/backend/src/server/web/HtmlTemplateService.ts +++ b/packages/backend/src/server/web/HtmlTemplateService.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { dirname } from 'node:path'; +import { dirname, resolve } from 'node:path'; 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 { Injectable, Inject } from '@nestjs/common'; 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 { MetaEntityService } from '@/core/entities/MetaEntityService.js'; import type { FastifyReply } from 'fastify'; +import type { Manifest } from 'vite'; import type { Config } from '@/config.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 _dirname = dirname(_filename); -const frontendVitePublic = `${_dirname}/../../../../frontend/public/`; -const frontendEmbedVitePublic = `${_dirname}/../../../../frontend-embed/public/`; +let rootDir = _dirname; +// 見つかるまで上に遡る +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() export class HtmlTemplateService { - private frontendBootloadersFetched = false; + private frontendAssetsFetched = false; + public frontendViteFiles: ViteFiles | null = null; public frontendBootloaderJs: string | null = null; public frontendBootloaderCss: string | null = null; + public frontendEmbedViteFiles: ViteFiles | null = null; public frontendEmbedBootloaderJs: string | null = null; public frontendEmbedBootloaderCss: string | null = null; @@ -42,18 +55,92 @@ export class HtmlTemplateService { ) { } + // 初期ロードで読み込むべきファイルのパスを収集する。 + // See https://ja.vite.dev/guide/backend-integration @bindThis - private async prepareFrontendBootloaders() { - if (this.frontendBootloadersFetched) return; - this.frontendBootloadersFetched = true; + private collectViteAssetFiles(manifest: Manifest): ViteFiles { + const entryFile = Object.values(manifest).find((chunk) => chunk.isEntry); + if (!entryFile) return { + entryJs: null, + css: [], + modulePreloads: [], + }; - const [bootJs, bootCss, embedBootJs, embedBootCss] = await Promise.all([ - fsp.readFile(`${frontendVitePublic}loader/boot.js`, 'utf-8').catch(() => null), - fsp.readFile(`${frontendVitePublic}loader/style.css`, 'utf-8').catch(() => null), - fsp.readFile(`${frontendEmbedVitePublic}loader/boot.js`, 'utf-8').catch(() => null), - fsp.readFile(`${frontendEmbedVitePublic}loader/style.css`, 'utf-8').catch(() => null), + const seenChunkIds = new Set(); + const cssFiles = new Set(); + const modulePreloads = new Set(); + + 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) { this.frontendBootloaderJs = bootJs; } @@ -62,6 +149,10 @@ export class HtmlTemplateService { this.frontendBootloaderCss = bootCss; } + if (embedFeViteManifest != null) { + this.frontendEmbedViteFiles = this.collectViteAssetFiles(embedFeViteManifest); + } + if (embedBootJs != null) { this.frontendEmbedBootloaderJs = embedBootJs; } @@ -73,7 +164,7 @@ export class HtmlTemplateService { @bindThis public async getCommonData(): Promise { - await this.prepareFrontendBootloaders(); + await this.prepareFrontendAssets(); return { version: this.config.version, @@ -90,8 +181,10 @@ export class HtmlTemplateService { metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(this.meta)), now: Date.now(), federationEnabled: this.meta.federation !== 'none', + frontendViteFiles: this.frontendViteFiles, frontendBootloaderJs: this.frontendBootloaderJs, frontendBootloaderCss: this.frontendBootloaderCss, + frontendEmbedViteFiles: this.frontendEmbedViteFiles, frontendEmbedBootloaderJs: this.frontendEmbedBootloaderJs, frontendEmbedBootloaderCss: this.frontendEmbedBootloaderCss, }; diff --git a/packages/backend/src/server/web/views/_.ts b/packages/backend/src/server/web/views/_.ts index ac7418f362..f9b290b13a 100644 --- a/packages/backend/src/server/web/views/_.ts +++ b/packages/backend/src/server/web/views/_.ts @@ -24,6 +24,12 @@ export type MinimumCommonData = { config: Config; }; +export type ViteFiles = { + entryJs: string | null; + css: string[]; + modulePreloads: string[]; +}; + export type CommonData = MinimumCommonData & { langs: string[]; instanceName: string; @@ -36,8 +42,10 @@ export type CommonData = MinimumCommonData & { instanceUrl: string; now: number; federationEnabled: boolean; + frontendViteFiles: ViteFiles | null; frontendBootloaderJs: string | null; frontendBootloaderCss: string | null; + frontendEmbedViteFiles: ViteFiles | null; frontendEmbedBootloaderJs: string | null; frontendEmbedBootloaderCss: string | null; metaJson?: string; diff --git a/packages/backend/src/server/web/views/base-embed.tsx b/packages/backend/src/server/web/views/base-embed.tsx index 011b66592e..a656bb28a7 100644 --- a/packages/backend/src/server/web/views/base-embed.tsx +++ b/packages/backend/src/server/web/views/base-embed.tsx @@ -46,11 +46,11 @@ export function BaseEmbed(props: PropsWithChildren