Fix theme name requirement regression with efficient lookup by name (#35007)

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo 2025-06-12 16:09:45 +02:00 committed by GitHub
parent 2254f47702
commit 825312d4b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 98 additions and 12 deletions

View File

@ -4,13 +4,11 @@ module ThemeHelper
def theme_style_tags(theme) def theme_style_tags(theme)
if theme == 'system' if theme == 'system'
''.html_safe.tap do |tags| ''.html_safe.tap do |tags|
tags << vite_stylesheet_tag('styles/mastodon-light.scss', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') tags << vite_stylesheet_tag('themes/mastodon-light', type: :virtual, media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
tags << vite_stylesheet_tag('styles/application.scss', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') tags << vite_stylesheet_tag('themes/default', type: :virtual, media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
end end
elsif theme == 'default'
vite_stylesheet_tag 'styles/application.scss', media: 'all', crossorigin: 'anonymous'
else else
vite_stylesheet_tag "styles/#{theme}.scss", media: 'all', crossorigin: 'anonymous' vite_stylesheet_tag "themes/#{theme}", type: :virtual, media: 'all', crossorigin: 'anonymous'
end end
end end

View File

@ -29,7 +29,7 @@ export function MastodonThemes(): Plugin {
throw new Error('Invalid themes.yml file'); throw new Error('Invalid themes.yml file');
} }
for (const themePath of Object.values(themes)) { for (const [themeName, themePath] of Object.entries(themes)) {
if ( if (
typeof themePath !== 'string' || typeof themePath !== 'string' ||
themePath.split('.').length !== 2 || // Ensure it has exactly one period themePath.split('.').length !== 2 || // Ensure it has exactly one period
@ -40,7 +40,7 @@ export function MastodonThemes(): Plugin {
); );
continue; continue;
} }
entrypoints[path.basename(themePath)] = path.resolve( entrypoints[`themes/${themeName}`] = path.resolve(
userConfig.root, userConfig.root,
themePath, themePath,
); );

View File

@ -0,0 +1,68 @@
import { relative, extname } from 'node:path';
import type { Plugin } from 'vite';
export function MastodonNameLookup(): Plugin {
const nameMap: Record<string, string> = {};
let root = '';
return {
name: 'mastodon-name-lookup',
applyToEnvironment(environment) {
return !!environment.config.build.manifest;
},
configResolved(userConfig) {
root = userConfig.root;
},
generateBundle(options, bundle) {
if (!root) {
throw new Error(
'MastodonNameLookup plugin requires the root to be set in the config.',
);
}
// Iterate over all chunks in the bundle and create a lookup map
for (const file in bundle) {
const chunk = bundle[file];
if (
chunk?.type !== 'chunk' ||
!chunk.isEntry ||
!chunk.facadeModuleId
) {
continue;
}
const relativePath = relative(
root,
sanitizeFileName(chunk.facadeModuleId),
);
const ext = extname(relativePath);
const name = chunk.name.replace(ext, '');
if (nameMap[name]) {
throw new Error(
`Entrypoint ${relativePath} conflicts with ${nameMap[name]}`,
);
}
nameMap[name] = relativePath;
}
this.emitFile({
type: 'asset',
fileName: '.vite/manifest-lookup.json',
source: JSON.stringify(nameMap, null, 2),
});
},
};
}
// Taken from https://github.com/rollup/rollup/blob/4f69d33af3b2ec9320c43c9e6c65ea23a02bdde3/src/utils/sanitizeFileName.ts
// https://datatracker.ietf.org/doc/html/rfc2396
// eslint-disable-next-line no-control-regex
const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$%&*+,:;<=>?[\]^`{|}\u007F]/g;
function sanitizeFileName(name: string): string {
return name.replace(INVALID_CHAR_REGEX, '');
}

View File

@ -7,6 +7,24 @@ module ViteRuby::ManifestIntegrityExtension
{ path: entry.fetch('file'), integrity: entry.fetch('integrity', nil) } { path: entry.fetch('file'), integrity: entry.fetch('integrity', nil) }
end end
def load_manifest
# Invalidate the name lookup cache when reloading manifest
@name_lookup_cache = load_name_lookup_cache
super
end
def load_name_lookup_cache
Oj.load(config.build_output_dir.join('.vite/manifest-lookup.json').read)
end
# Upstream's `virtual` type is a hack, re-implement it with efficient exact name lookup
def resolve_virtual_entry(name)
@name_lookup_cache ||= load_name_lookup_cache
@name_lookup_cache.fetch(name)
end
# Find a manifest entry by the *final* file name # Find a manifest entry by the *final* file name
def integrity_hash_for_file(file_name) def integrity_hash_for_file(file_name)
@integrity_cache ||= {} @integrity_cache ||= {}
@ -94,10 +112,10 @@ module ViteRails::TagHelpers::IntegrityExtension
end end
end end
def vite_stylesheet_tag(*names, **options) def vite_stylesheet_tag(*names, type: :stylesheet, **options)
''.html_safe.tap do |tags| ''.html_safe.tap do |tags|
names.each do |name| names.each do |name|
entry = vite_manifest.path_and_integrity_for(name, type: :stylesheet) entry = vite_manifest.path_and_integrity_for(name, type:)
options[:extname] = false if Rails::VERSION::MAJOR >= 7 options[:extname] = false if Rails::VERSION::MAJOR >= 7

View File

@ -17,7 +17,7 @@ RSpec.describe ThemeHelper do
) )
expect(html_links.last.attributes.symbolize_keys) expect(html_links.last.attributes.symbolize_keys)
.to include( .to include(
href: have_attributes(value: match(/application/)), href: have_attributes(value: match(/default/)),
media: have_attributes(value: '(prefers-color-scheme: dark)') media: have_attributes(value: '(prefers-color-scheme: dark)')
) )
end end
@ -26,10 +26,10 @@ RSpec.describe ThemeHelper do
context 'when using "default" theme' do context 'when using "default" theme' do
let(:theme) { 'default' } let(:theme) { 'default' }
it 'returns the application stylesheet' do it 'returns the default stylesheet' do
expect(html_links.last.attributes.symbolize_keys) expect(html_links.last.attributes.symbolize_keys)
.to include( .to include(
href: have_attributes(value: match(/application/)) href: have_attributes(value: match(/default/))
) )
end end
end end

View File

@ -16,6 +16,7 @@ import postcssPresetEnv from 'postcss-preset-env';
import { MastodonServiceWorkerLocales } from './config/vite/plugin-sw-locales'; import { MastodonServiceWorkerLocales } from './config/vite/plugin-sw-locales';
import { MastodonEmojiCompressed } from './config/vite/plugin-emoji-compressed'; import { MastodonEmojiCompressed } from './config/vite/plugin-emoji-compressed';
import { MastodonThemes } from './config/vite/plugin-mastodon-themes'; import { MastodonThemes } from './config/vite/plugin-mastodon-themes';
import { MastodonNameLookup } from './config/vite/plugin-name-lookup';
const jsRoot = path.resolve(__dirname, 'app/javascript'); const jsRoot = path.resolve(__dirname, 'app/javascript');
@ -125,6 +126,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
// Old library types need to be converted // Old library types need to be converted
optimizeLodashImports() as PluginOption, optimizeLodashImports() as PluginOption,
!!process.env.ANALYZE_BUNDLE_SIZE && (visualizer() as PluginOption), !!process.env.ANALYZE_BUNDLE_SIZE && (visualizer() as PluginOption),
MastodonNameLookup(),
], ],
} satisfies UserConfig; } satisfies UserConfig;
}; };