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)
 | 
					  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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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) }
 | 
					    { 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user