From 0af2c4829ff86cf84ed9e7804d023f31e805542b Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 22 Jul 2025 15:58:04 +0200 Subject: [PATCH] Creates Vite plugin to generate assets file (#35454) --- config/vite/plugin-assets-manifest.ts | 84 +++++++++++++++++++++++++++ vite.config.mts | 20 +------ 2 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 config/vite/plugin-assets-manifest.ts diff --git a/config/vite/plugin-assets-manifest.ts b/config/vite/plugin-assets-manifest.ts new file mode 100644 index 0000000000..3d465549ce --- /dev/null +++ b/config/vite/plugin-assets-manifest.ts @@ -0,0 +1,84 @@ +// Heavily inspired by https://github.com/ElMassimo/vite_ruby + +import { createHash } from 'node:crypto'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +import glob from 'fast-glob'; +import type { Plugin } from 'vite'; + +interface AssetManifestChunk { + file: string; + integrity: string; +} + +const ALGORITHM = 'sha384'; + +export function MastodonAssetsManifest(): Plugin { + let manifest: string | boolean = true; + let jsRoot = ''; + + return { + name: 'mastodon-assets-manifest', + applyToEnvironment(environment) { + return !!environment.config.build.manifest; + }, + configResolved(resolvedConfig) { + manifest = resolvedConfig.build.manifest; + jsRoot = resolvedConfig.root; + }, + async generateBundle() { + // Glob all assets and return an array of absolute paths. + const assetPaths = await glob('{fonts,icons,images}/**/*', { + cwd: jsRoot, + absolute: true, + }); + + const assetManifest: Record = {}; + const excludeExts = ['', '.md']; + for (const file of assetPaths) { + // Exclude files like markdown or README files with no extension. + const ext = path.extname(file); + if (excludeExts.includes(ext)) { + continue; + } + + // Read the file and emit it as an asset. + const contents = await fs.readFile(file); + const ref = this.emitFile({ + name: path.basename(file), + type: 'asset', + source: contents, + }); + const hashedFilename = this.getFileName(ref); + + // With the emitted file information, hash the contents and store in manifest. + const name = path.relative(jsRoot, file); + const hash = createHash(ALGORITHM) + .update(contents) + .digest() + .toString('base64'); + assetManifest[name] = { + file: hashedFilename, + integrity: `${ALGORITHM}-${hash}`, + }; + } + + if (Object.keys(assetManifest).length === 0) { + console.warn('Asset manifest is empty'); + return; + } + + // Get manifest location and emit the manifest. + const manifestDir = + typeof manifest === 'string' ? path.dirname(manifest) : '.vite'; + const fileName = `${manifestDir}/manifest-assets.json`; + + this.emitFile({ + fileName, + type: 'asset', + source: JSON.stringify(assetManifest, null, 2), + }); + }, + }; +} diff --git a/vite.config.mts b/vite.config.mts index b47bea382c..7f93157b7e 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -4,7 +4,6 @@ import { readdir } from 'node:fs/promises'; import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin'; import legacy from '@vitejs/plugin-legacy'; import react from '@vitejs/plugin-react'; -import glob from 'fast-glob'; import postcssPresetEnv from 'postcss-preset-env'; import Compress from 'rollup-plugin-gzip'; import { visualizer } from 'rollup-plugin-visualizer'; @@ -24,6 +23,7 @@ import { MastodonServiceWorkerLocales } from './config/vite/plugin-sw-locales'; import { MastodonEmojiCompressed } from './config/vite/plugin-emoji-compressed'; import { MastodonThemes } from './config/vite/plugin-mastodon-themes'; import { MastodonNameLookup } from './config/vite/plugin-name-lookup'; +import { MastodonAssetsManifest } from './config/vite/plugin-assets-manifest'; const jsRoot = path.resolve(__dirname, 'app/javascript'); @@ -120,6 +120,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { }, }), MastodonThemes(), + MastodonAssetsManifest(), viteStaticCopy({ targets: [ { @@ -144,7 +145,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { isProdBuild && (Compress() as PluginOption), command === 'build' && manifestSRI({ - manifestPaths: ['.vite/manifest.json', '.vite/manifest-assets.json'], + manifestPaths: ['.vite/manifest.json'], }), VitePWA({ srcDir: path.resolve(jsRoot, 'mastodon/service_worker'), @@ -211,21 +212,6 @@ async function findEntrypoints() { } } - // Lastly other assets - const assetEntrypoints = await glob('{fonts,icons,images}/**/*', { - cwd: jsRoot, - absolute: true, - }); - const excludeExts = ['', '.md']; - for (const file of assetEntrypoints) { - const ext = path.extname(file); - if (excludeExts.includes(ext)) { - continue; - } - const name = path.basename(file); - entrypoints[name] = path.resolve(jsRoot, file); - } - return entrypoints; }