Creates Vite plugin to generate assets file (#35454)

This commit is contained in:
Echo 2025-07-22 15:58:04 +02:00 committed by GitHub
parent be3dc5b508
commit 0af2c4829f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 87 additions and 17 deletions

View File

@ -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<string, AssetManifestChunk> = {};
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),
});
},
};
}

View File

@ -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;
}