diff --git a/app/controllers/concerns/theming_concern.rb b/app/controllers/concerns/theming_concern.rb index 892868cb57..38b31e932f 100644 --- a/app/controllers/concerns/theming_concern.rb +++ b/app/controllers/concerns/theming_concern.rb @@ -12,7 +12,7 @@ module ThemingConcern def current_skin @current_skin ||= begin skins = Themes.instance.skins_for(current_flavour) - [current_user&.setting_skin, Setting.skin, 'system', 'application'].find { |skin| skins.include?(skin) } + [current_user&.setting_skin, Setting.skin, 'system', 'default'].find { |skin| skins.include?(skin) } end end diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index e9014f6d84..7e7345dc8c 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -6,13 +6,11 @@ module ThemeHelper if theme == 'system' ''.html_safe.tap do |tags| - tags << vite_stylesheet_tag("skins/#{flavour}/mastodon-light.scss", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') - tags << vite_stylesheet_tag("skins/#{flavour}/application.scss", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') + tags << vite_stylesheet_tag("skins/#{flavour}/mastodon-light", type: :virtual, media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') + tags << vite_stylesheet_tag("skins/#{flavour}/default", type: :virtual, media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') end - elsif theme == 'default' - vite_stylesheet_tag "skins/#{flavour}/application.scss", media: 'all', crossorigin: 'anonymous' else - vite_stylesheet_tag "skins/#{flavour}/#{theme}.scss", media: 'all', crossorigin: 'anonymous' + vite_stylesheet_tag "skins/#{flavour}/#{theme}", type: :virtual, media: 'all', crossorigin: 'anonymous' end end diff --git a/app/javascript/skins/glitch/contrast.scss b/app/javascript/skins/glitch/contrast/common.scss similarity index 100% rename from app/javascript/skins/glitch/contrast.scss rename to app/javascript/skins/glitch/contrast/common.scss diff --git a/app/javascript/skins/glitch/application.scss b/app/javascript/skins/glitch/default/common.scss similarity index 100% rename from app/javascript/skins/glitch/application.scss rename to app/javascript/skins/glitch/default/common.scss diff --git a/app/javascript/skins/glitch/mastodon-light.scss b/app/javascript/skins/glitch/mastodon-light/common.scss similarity index 100% rename from app/javascript/skins/glitch/mastodon-light.scss rename to app/javascript/skins/glitch/mastodon-light/common.scss diff --git a/app/javascript/skins/vanilla/contrast.scss b/app/javascript/skins/vanilla/contrast/common.scss similarity index 100% rename from app/javascript/skins/vanilla/contrast.scss rename to app/javascript/skins/vanilla/contrast/common.scss diff --git a/app/javascript/skins/vanilla/application.scss b/app/javascript/skins/vanilla/default/common.scss similarity index 100% rename from app/javascript/skins/vanilla/application.scss rename to app/javascript/skins/vanilla/default/common.scss diff --git a/app/javascript/skins/vanilla/mastodon-light.scss b/app/javascript/skins/vanilla/mastodon-light/common.scss similarity index 100% rename from app/javascript/skins/vanilla/mastodon-light.scss rename to app/javascript/skins/vanilla/mastodon-light/common.scss diff --git a/app/javascript/skins/vanilla/win95.scss b/app/javascript/skins/vanilla/win95/common.scss similarity index 100% rename from app/javascript/skins/vanilla/win95.scss rename to app/javascript/skins/vanilla/win95/common.scss diff --git a/app/lib/themes.rb b/app/lib/themes.rb index 3e326ba623..a854c2e0db 100644 --- a/app/lib/themes.rb +++ b/app/lib/themes.rb @@ -69,7 +69,7 @@ class Themes def skins_for(name) skins = @flavours[name]['skins'] - skins.include?('application') && skins.include?('mastodon-light') ? ['system'] + skins : skins + skins.include?('default') && skins.include?('mastodon-light') ? ['system'] + skins : skins end def flavours_and_skins diff --git a/config/vite/plugin-glitch-themes.ts b/config/vite/plugin-glitch-themes.ts index 9326e40817..1681a0a443 100644 --- a/config/vite/plugin-glitch-themes.ts +++ b/config/vite/plugin-glitch-themes.ts @@ -43,14 +43,21 @@ export function GlitchThemes(): Plugin { } // Skins - // TODO: handle variants such as `skin/common.scss` const skinFiles = glob.sync( - `app/javascript/skins/${flavourName}/*.scss`, + `app/javascript/skins/${flavourName}/*.{css,scss}`, ); for (const entrypoint of skinFiles) { const name = `skins/${flavourName}/${path.basename(entrypoint)}`; entrypoints[name] = path.resolve(userConfig.envDir, entrypoint); } + + const alternateSkinFiles = glob.sync( + `app/javascript/skins/${flavourName}/*/{index,common,application}.{css,scss}`, + ); + for (const entrypoint of alternateSkinFiles) { + const name = `skins/${flavourName}/${path.basename(path.dirname(entrypoint))}`; + entrypoints[name] = path.resolve(userConfig.envDir, entrypoint); + } } return { diff --git a/config/vite/plugin-mastodon-themes.ts b/config/vite/plugin-mastodon-themes.ts index 07d79584a3..64bfa5e767 100644 --- a/config/vite/plugin-mastodon-themes.ts +++ b/config/vite/plugin-mastodon-themes.ts @@ -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, ); diff --git a/config/vite/plugin-name-lookup.ts b/config/vite/plugin-name-lookup.ts new file mode 100644 index 0000000000..82dbf712e2 --- /dev/null +++ b/config/vite/plugin-name-lookup.ts @@ -0,0 +1,68 @@ +import { relative, extname } from 'node:path'; + +import type { Plugin } from 'vite'; + +export function MastodonNameLookup(): Plugin { + const nameMap: Record = {}; + + 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, ''); +} diff --git a/lib/vite_ruby/sri_extensions.rb b/lib/vite_ruby/sri_extensions.rb index a8b9fd2a78..5552e9cd0b 100644 --- a/lib/vite_ruby/sri_extensions.rb +++ b/lib/vite_ruby/sri_extensions.rb @@ -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 diff --git a/spec/helpers/theme_helper_spec.rb b/spec/helpers/theme_helper_spec.rb index 9226383470..875a15321f 100644 --- a/spec/helpers/theme_helper_spec.rb +++ b/spec/helpers/theme_helper_spec.rb @@ -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) { ['glitch', '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 diff --git a/vite.config.mts b/vite.config.mts index fc1598f45f..c0e456d563 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -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 { GlitchThemes } from './config/vite/plugin-glitch-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; };