1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-16 06:15:30 +02:00

enhance(backend): bundle backend using Rolldown (#17068)

* enhance(backend): bundle backend using rolldown

* fix

* fix [ci skip]

* remove unused build script

* fix

* enhance: 起動からlistenまでかかる時間を減らす (MisskeyIO#1410)

* ✌️

* fix

* update rolldown

* fix(backend): extract static error classes to avoid rolldown design:paramtypes omission

* update rolldown

* Revert "fix(backend): extract static error classes to avoid rolldown design:paramtypes omission"

This reverts commit e2243c9dc3.

* fix

* perf: avoid generating sourcemap in production

* fix

* fix

* fix

* fix paths

* fix

* fix

* fix

* fix

* fix

* enhance: バックエンドの開発サーバー制御をrolldown側で行うように

* remove nodemon

* Update Changelog

* tweak config

* fix

* fix

* fix

* clean up

---------

Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com>
Co-authored-by: bab <mashirohira@gmail.com>
This commit is contained in:
かっこかり
2026-04-16 12:44:50 +09:00
committed by GitHub
parent 024f8bb102
commit 37bfcb604f
26 changed files with 504 additions and 349 deletions

View File

@@ -4,8 +4,7 @@
*/
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { resolve } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
import type { Config } from '@/config.js';
import type { DriveFilesRepository } from '@/models/_.js';
@@ -25,11 +24,6 @@ import { FileServerFileResolver } from './file/FileServerFileResolver.js';
import { FileServerProxyHandler } from './file/FileServerProxyHandler.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const assets = `${_dirname}/../../server/file/assets/`;
@Injectable()
export class FileServerService {
private logger: Logger;
@@ -37,6 +31,8 @@ export class FileServerService {
private proxyHandler: FileServerProxyHandler;
private fileResolver: FileServerFileResolver;
private readonly assets: string;
constructor(
@Inject(DI.config)
private config: Config,
@@ -52,6 +48,7 @@ export class FileServerService {
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('server', 'gray');
this.assets = resolve(this.config.rootDir, 'packages/backend/src/server/file/assets');
this.fileResolver = new FileServerFileResolver(
this.driveFilesRepository,
this.fileInfoService,
@@ -61,13 +58,13 @@ export class FileServerService {
this.driveHandler = new FileServerDriveHandler(
this.config,
this.fileResolver,
assets,
this.assets,
this.videoProcessingService,
);
this.proxyHandler = new FileServerProxyHandler(
this.config,
this.fileResolver,
assets,
this.assets,
this.imageProcessingService,
);
@@ -87,7 +84,7 @@ export class FileServerService {
fastify.register((fastify, options, done) => {
fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
fastify.get('/files/app-default.jpg', (request, reply) => {
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
const file = fs.createReadStream(`${this.assets}/dummy.png`);
reply.header('Content-Type', 'image/jpeg');
reply.header('Cache-Control', 'max-age=31536000, immutable');
return reply.send(file);
@@ -121,7 +118,7 @@ export class FileServerService {
reply.header('Cache-Control', 'max-age=300');
if (request.query && 'fallback' in request.query) {
return reply.sendFile('/dummy.png', assets);
return reply.sendFile('/dummy.png', this.assets);
}
if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) {

View File

@@ -4,9 +4,7 @@
*/
import { randomUUID } from 'node:crypto';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as fs from 'node:fs';
import { resolve } from 'node:path';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import sharp from 'sharp';
@@ -67,35 +65,17 @@ import { ErrorPage } from './views/error.js';
import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
let rootDir = _dirname;
// 見つかるまで上に遡る
while (!fs.existsSync(resolve(rootDir, 'packages'))) {
const parentDir = dirname(rootDir);
if (parentDir === rootDir) {
throw new Error('Cannot find root directory');
}
rootDir = parentDir;
}
const backendRootDir = resolve(rootDir, 'packages/backend');
const frontendRootDir = resolve(rootDir, 'packages/frontend');
const staticAssets = resolve(backendRootDir, 'assets');
const clientAssets = resolve(frontendRootDir, 'assets');
const assets = resolve(rootDir, 'built/_frontend_dist_');
const swAssets = resolve(rootDir, 'built/_sw_dist_');
const fluentEmojisDir = resolve(rootDir, 'fluent-emojis/dist');
const twemojiDir = resolve(backendRootDir, 'node_modules/@discordapp/twemoji/dist/svg');
const frontendViteOut = resolve(rootDir, 'built/_frontend_vite_');
const frontendEmbedViteOut = resolve(rootDir, 'built/_frontend_embed_vite_');
const tarball = resolve(rootDir, 'built/tarball');
@Injectable()
export class ClientServerService {
private logger: Logger;
private readonly staticAssets: string;
private readonly clientAssets: string;
private readonly assets: string;
private readonly swAssets: string;
private readonly fluentEmojisDir: string;
private readonly twemojiDir: string;
private readonly frontendViteOut: string;
private readonly frontendEmbedViteOut: string;
private readonly tarball: string;
constructor(
@Inject(DI.config)
@@ -149,6 +129,17 @@ export class ClientServerService {
private clientLoggerService: ClientLoggerService,
) {
//this.createServer = this.createServer.bind(this);
const backendRootdir = resolve(this.config.rootDir, 'packages/backend');
const frontendRootdir = resolve(this.config.rootDir, 'packages/frontend');
this.staticAssets = resolve(backendRootdir, 'assets');
this.clientAssets = resolve(frontendRootdir, 'assets');
this.assets = resolve(this.config.rootDir, 'built/_frontend_dist_');
this.swAssets = resolve(this.config.rootDir, 'built/_sw_dist_');
this.fluentEmojisDir = resolve(this.config.rootDir, 'fluent-emojis/dist');
this.twemojiDir = resolve(backendRootdir, 'node_modules/@discordapp/twemoji/dist/svg');
this.frontendViteOut = resolve(this.config.rootDir, 'built/_frontend_vite_');
this.frontendEmbedViteOut = resolve(this.config.rootDir, 'built/_frontend_embed_vite_');
this.tarball = resolve(this.config.rootDir, 'built/tarball');
}
@bindThis
@@ -223,17 +214,17 @@ export class ClientServerService {
//#region vite assets
if (this.config.frontendEmbedManifestExists) {
console.log(`[ClientServerService] Using built frontend vite assets. ${frontendViteOut}`);
console.log(`[ClientServerService] Using built frontend vite assets. ${this.frontendViteOut}`);
fastify.register((fastify, options, done) => {
fastify.register(fastifyStatic, {
root: frontendViteOut,
root: this.frontendViteOut,
prefix: '/vite/',
maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
fastify.register(fastifyStatic, {
root: frontendEmbedViteOut,
root: this.frontendEmbedViteOut,
prefix: '/embed_vite/',
maxAge: ms('30 days'),
immutable: true,
@@ -265,21 +256,21 @@ export class ClientServerService {
//#region static assets
fastify.register(fastifyStatic, {
root: staticAssets,
root: this.staticAssets,
prefix: '/static-assets/',
maxAge: ms('7 days'),
decorateReply: false,
});
fastify.register(fastifyStatic, {
root: clientAssets,
root: this.clientAssets,
prefix: '/client-assets/',
maxAge: ms('7 days'),
decorateReply: false,
});
fastify.register(fastifyStatic, {
root: assets,
root: this.assets,
prefix: '/assets/',
maxAge: ms('7 days'),
decorateReply: false,
@@ -287,7 +278,7 @@ export class ClientServerService {
fastify.register((fastify, options, done) => {
fastify.register(fastifyStatic, {
root: tarball,
root: this.tarball,
prefix: '/tarball/',
maxAge: ms('30 days'),
immutable: true,
@@ -298,11 +289,11 @@ export class ClientServerService {
});
fastify.get('/favicon.ico', async (request, reply) => {
return reply.sendFile('/favicon.ico', staticAssets);
return reply.sendFile('/favicon.ico', this.staticAssets);
});
fastify.get('/apple-touch-icon.png', async (request, reply) => {
return reply.sendFile('/apple-touch-icon.png', staticAssets);
return reply.sendFile('/apple-touch-icon.png', this.staticAssets);
});
fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => {
@@ -315,7 +306,7 @@ export class ClientServerService {
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
return reply.sendFile(path, fluentEmojisDir, {
return reply.sendFile(path, this.fluentEmojisDir, {
maxAge: ms('30 days'),
});
});
@@ -330,7 +321,7 @@ export class ClientServerService {
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
return reply.sendFile(path, twemojiDir, {
return reply.sendFile(path, this.twemojiDir, {
maxAge: ms('30 days'),
});
});
@@ -344,7 +335,7 @@ export class ClientServerService {
}
const mask = await sharp(
`${twemojiDir}/${path.replace('.png', '')}.svg`,
`${this.twemojiDir}/${path.replace('.png', '')}.svg`,
{ density: 1000 },
)
.resize(488, 488)
@@ -380,7 +371,7 @@ export class ClientServerService {
// ServiceWorker
fastify.get('/sw.js', async (request, reply) => {
return await reply.sendFile('/sw.js', swAssets, {
return await reply.sendFile('/sw.js', this.swAssets, {
maxAge: ms('10 minutes'),
});
});
@@ -390,7 +381,7 @@ export class ClientServerService {
// Embed Javascript
fastify.get('/embed.js', async (request, reply) => {
return await reply.sendFile('/embed.js', staticAssets, {
return await reply.sendFile('/embed.js', this.staticAssets, {
maxAge: ms('1 day'),
});
});

View File

@@ -3,9 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promises as fsp, existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { promises as fsp } from 'node:fs';
import { languages } from 'i18n/const';
import { Injectable, Inject } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
@@ -18,25 +17,11 @@ import type { Config } from '@/config.js';
import type { MiMeta } from '@/models/Meta.js';
import type { CommonData, ViteFiles } from './views/_.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
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 frontendAssetsFetched = false;
private readonly frontendViteBuilt: string;
private readonly frontendEmbedViteBuilt: string;
public frontendViteFiles: ViteFiles | null = null;
public frontendBootloaderJs: string | null = null;
public frontendBootloaderCss: string | null = null;
@@ -53,6 +38,8 @@ export class HtmlTemplateService {
private metaEntityService: MetaEntityService,
) {
this.frontendViteBuilt = resolve(this.config.rootDir, 'built/_frontend_vite_');
this.frontendEmbedViteBuilt = resolve(this.config.rootDir, 'built/_frontend_embed_vite_');
}
// 初期ロードで読み込むべきファイルのパスを収集する。
@@ -118,22 +105,22 @@ export class HtmlTemplateService {
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),
fsp.readFile(resolve(this.frontendViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null),
fsp.readFile(resolve(this.frontendViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null),
fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null),
fsp.readFile(resolve(this.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);
const manifestContent = await fsp.readFile(resolve(this.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);
const manifestContent = await fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'manifest.json'), 'utf-8').catch(() => null);
embedFeViteManifest = manifestContent ? JSON.parse(manifestContent) : null;
}