From 013c527406ca2adb948a0bde9331c8f14e0dc276 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 16 Jun 2025 16:25:12 +0200 Subject: [PATCH] Fix vite helpers crash in development mode (#35035) Co-authored-by: ChaosExAnima --- config/vite/plugin-mastodon-themes.ts | 141 +++++++++++++++++++++----- lib/vite_ruby/sri_extensions.rb | 4 +- 2 files changed, 120 insertions(+), 25 deletions(-) diff --git a/config/vite/plugin-mastodon-themes.ts b/config/vite/plugin-mastodon-themes.ts index 64bfa5e76..53281e29f 100644 --- a/config/vite/plugin-mastodon-themes.ts +++ b/config/vite/plugin-mastodon-themes.ts @@ -7,43 +7,28 @@ import path from 'node:path'; import yaml from 'js-yaml'; import type { Plugin } from 'vite'; +type Themes = Record; + export function MastodonThemes(): Plugin { + let projectRoot = ''; + let jsRoot = ''; + return { name: 'mastodon-themes', async config(userConfig) { if (!userConfig.root || !userConfig.envDir) { throw new Error('Unknown project directory'); } + projectRoot = userConfig.envDir; + jsRoot = userConfig.root; - const themesFile = path.resolve(userConfig.envDir, 'config/themes.yml'); const entrypoints: Record = {}; // Get all files mentioned in the themes.yml file. - const themesString = await fs.readFile(themesFile, 'utf8'); - const themes = yaml.load(themesString, { - filename: 'themes.yml', - schema: yaml.FAILSAFE_SCHEMA, - }); - - if (!themes || typeof themes !== 'object') { - throw new Error('Invalid themes.yml file'); - } + const themes = await loadThemesFromConfig(projectRoot); for (const [themeName, themePath] of Object.entries(themes)) { - if ( - typeof themePath !== 'string' || - themePath.split('.').length !== 2 || // Ensure it has exactly one period - !themePath.endsWith('css') - ) { - console.warn( - `Invalid theme path "${themePath}" in themes.yml, skipping`, - ); - continue; - } - entrypoints[`themes/${themeName}`] = path.resolve( - userConfig.root, - themePath, - ); + entrypoints[`themes/${themeName}`] = path.resolve(jsRoot, themePath); } return { @@ -54,5 +39,113 @@ export function MastodonThemes(): Plugin { }, }; }, + async configureServer(server) { + const themes = await loadThemesFromConfig(projectRoot); + server.middlewares.use((req, res, next) => { + if (!req.url?.startsWith('/packs-dev/themes/')) { + next(); + return; + } + + // Rewrite the URL to the entrypoint if it matches a theme. + if (isThemeFile(req.url ?? '', themes)) { + const themeName = pathToThemeName(req.url ?? ''); + req.url = `/packs-dev/${themes[themeName]}`; + } + next(); + }); + }, + async handleHotUpdate({ modules, server }) { + if (modules.length === 0) { + return; + } + const themes = await loadThemesFromConfig(projectRoot); + const themePathToName = new Map( + Object.entries(themes).map(([themeName, themePath]) => [ + path.resolve(jsRoot, themePath), + `/themes/${themeName}`, + ]), + ); + const themeNames = new Set(); + + const addIfMatches = (file: string | null) => { + if (!file) { + return false; + } + const themeName = themePathToName.get(file); + if (themeName) { + themeNames.add(themeName); + return true; + } + return false; + }; + + for (const module of modules) { + if (!addIfMatches(module.file)) { + for (const importer of module.importers) { + addIfMatches(importer.file); + } + } + } + + if (themeNames.size > 0) { + server.ws.send({ + type: 'update', + updates: Array.from(themeNames).map((themeName) => ({ + type: 'css-update', + path: themeName, + acceptedPath: themeName, + timestamp: Date.now(), + })), + }); + } + }, }; } + +async function loadThemesFromConfig(root: string) { + const themesFile = path.resolve(root, 'config/themes.yml'); + const themes: Themes = {}; + + const themesString = await fs.readFile(themesFile, 'utf8'); + const themesObject = yaml.load(themesString, { + filename: 'themes.yml', + schema: yaml.FAILSAFE_SCHEMA, + }); + + if (!themesObject || typeof themes !== 'object') { + throw new Error('Invalid themes.yml file'); + } + + for (const [themeName, themePath] of Object.entries(themesObject)) { + if ( + typeof themePath !== 'string' || + themePath.split('.').length !== 2 || // Ensure it has exactly one period + !themePath.endsWith('css') + ) { + console.warn(`Invalid theme path "${themePath}" in themes.yml, skipping`); + continue; + } + themes[themeName] = themePath; + } + + if (Object.keys(themes).length === 0) { + throw new Error('No valid themes found in themes.yml'); + } + + return themes; +} + +function pathToThemeName(file: string) { + const basename = path.basename(file); + return basename.split(/[.?]/)[0] ?? ''; +} + +function isThemeFile(file: string, themes: Themes) { + if (!file.includes('/themes/')) { + return false; + } + + const basename = pathToThemeName(file); + return basename in themes; +} diff --git a/lib/vite_ruby/sri_extensions.rb b/lib/vite_ruby/sri_extensions.rb index 5552e9cd0..1c616b496 100644 --- a/lib/vite_ruby/sri_extensions.rb +++ b/lib/vite_ruby/sri_extensions.rb @@ -9,7 +9,7 @@ module ViteRuby::ManifestIntegrityExtension def load_manifest # Invalidate the name lookup cache when reloading manifest - @name_lookup_cache = load_name_lookup_cache + @name_lookup_cache = load_name_lookup_cache unless dev_server_running? super end @@ -20,6 +20,8 @@ module ViteRuby::ManifestIntegrityExtension # Upstream's `virtual` type is a hack, re-implement it with efficient exact name lookup def resolve_virtual_entry(name) + return name if dev_server_running? + @name_lookup_cache ||= load_name_lookup_cache @name_lookup_cache.fetch(name)