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:
		@@ -4,13 +4,11 @@ module ThemeHelper
 | 
			
		||||
  def theme_style_tags(theme)
 | 
			
		||||
    if theme == 'system'
 | 
			
		||||
      ''.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('styles/application.scss', media: '(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('themes/default', type: :virtual, media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
 | 
			
		||||
      end
 | 
			
		||||
    elsif theme == 'default'
 | 
			
		||||
      vite_stylesheet_tag 'styles/application.scss', media: 'all', crossorigin: 'anonymous'
 | 
			
		||||
    else
 | 
			
		||||
      vite_stylesheet_tag "styles/#{theme}.scss", media: 'all', crossorigin: 'anonymous'
 | 
			
		||||
      vite_stylesheet_tag "themes/#{theme}", type: :virtual, media: 'all', crossorigin: 'anonymous'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ export function MastodonThemes(): Plugin {
 | 
			
		||||
        throw new Error('Invalid themes.yml file');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (const themePath of Object.values(themes)) {
 | 
			
		||||
      for (const [themeName, themePath] of Object.entries(themes)) {
 | 
			
		||||
        if (
 | 
			
		||||
          typeof themePath !== 'string' ||
 | 
			
		||||
          themePath.split('.').length !== 2 || // Ensure it has exactly one period
 | 
			
		||||
@@ -40,7 +40,7 @@ export function MastodonThemes(): Plugin {
 | 
			
		||||
          );
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
        entrypoints[path.basename(themePath)] = path.resolve(
 | 
			
		||||
        entrypoints[`themes/${themeName}`] = path.resolve(
 | 
			
		||||
          userConfig.root,
 | 
			
		||||
          themePath,
 | 
			
		||||
        );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										68
									
								
								config/vite/plugin-name-lookup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								config/vite/plugin-name-lookup.ts
									
									
									
									
									
										Normal 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, '');
 | 
			
		||||
}
 | 
			
		||||
@@ -7,6 +7,24 @@ module ViteRuby::ManifestIntegrityExtension
 | 
			
		||||
    { path: entry.fetch('file'), integrity: entry.fetch('integrity', nil) }
 | 
			
		||||
  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
 | 
			
		||||
  def integrity_hash_for_file(file_name)
 | 
			
		||||
    @integrity_cache ||= {}
 | 
			
		||||
@@ -94,10 +112,10 @@ module ViteRails::TagHelpers::IntegrityExtension
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def vite_stylesheet_tag(*names, **options)
 | 
			
		||||
  def vite_stylesheet_tag(*names, type: :stylesheet, **options)
 | 
			
		||||
    ''.html_safe.tap do |tags|
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ RSpec.describe ThemeHelper do
 | 
			
		||||
          )
 | 
			
		||||
        expect(html_links.last.attributes.symbolize_keys)
 | 
			
		||||
          .to include(
 | 
			
		||||
            href: have_attributes(value: match(/application/)),
 | 
			
		||||
            href: have_attributes(value: match(/default/)),
 | 
			
		||||
            media: have_attributes(value: '(prefers-color-scheme: dark)')
 | 
			
		||||
          )
 | 
			
		||||
      end
 | 
			
		||||
@@ -26,10 +26,10 @@ RSpec.describe ThemeHelper do
 | 
			
		||||
    context 'when using "default" theme' do
 | 
			
		||||
      let(:theme) { 'default' }
 | 
			
		||||
 | 
			
		||||
      it 'returns the application stylesheet' do
 | 
			
		||||
      it 'returns the default stylesheet' do
 | 
			
		||||
        expect(html_links.last.attributes.symbolize_keys)
 | 
			
		||||
          .to include(
 | 
			
		||||
            href: have_attributes(value: match(/application/))
 | 
			
		||||
            href: have_attributes(value: match(/default/))
 | 
			
		||||
          )
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ import postcssPresetEnv from 'postcss-preset-env';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
      optimizeLodashImports() as PluginOption,
 | 
			
		||||
      !!process.env.ANALYZE_BUNDLE_SIZE && (visualizer() as PluginOption),
 | 
			
		||||
      MastodonNameLookup(),
 | 
			
		||||
    ],
 | 
			
		||||
  } satisfies UserConfig;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user