mirror of
https://github.com/mastodon/mastodon.git
synced 2026-08-16 18:56:52 -05:00
242 lines
7.7 KiB
Ruby
242 lines
7.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Vite
|
|
# This class generates the HTML tags needed to load assets in the app views
|
|
#
|
|
# It does it by following different strategies:
|
|
# - dev_server: If Vite's dev server is running, it generates a simple tag
|
|
# for an asset that the server will know how to handle
|
|
# - manifest: When using a manifest, each asset is expanded in a collection
|
|
# of tags that include the main asset and all of their dependencies.
|
|
# For example a javascript asset includes an include tag for the main
|
|
# javascript file, preload tags for each of their dependencies and
|
|
# stylesheet links for each css module file generated by Vite
|
|
#
|
|
# When both :dev_server and :manifest strategies are defined, it will first
|
|
# try to use the :dev_server and if it's not running it will fallback to :manifest
|
|
class Tagger
|
|
SKIP = Object.new
|
|
|
|
module Common
|
|
def vite_preload_tag(helper, *sources, crossorigin:, **options)
|
|
url_options = options.extract!(:host, :protocol)
|
|
asset_paths = sources.map { |source| helper.path_to_asset(source, **url_options) }
|
|
helper.try(:request).try(
|
|
:send_early_hints,
|
|
'Link' => asset_paths.map do |href|
|
|
%(<#{href}>; rel=modulepreload; as=script; crossorigin=#{crossorigin})
|
|
end.join(',')
|
|
)
|
|
|
|
tags = asset_paths.map do |href|
|
|
helper.tag.link(rel: 'modulepreload', href:, as: 'script', crossorigin:, **options)
|
|
end
|
|
|
|
helper.safe_join(tags)
|
|
end
|
|
end
|
|
|
|
# When Vite's dev server is running we just need to generate tags
|
|
# linking to the main assets (javascript, stylesheets, etc) and the
|
|
# dev server will do the rest
|
|
class DevServerStrategy
|
|
include Common
|
|
|
|
attr_reader :config, :resolver, :dev_server
|
|
|
|
def initialize(config:, dev_server:)
|
|
@config = config
|
|
@dev_server = dev_server || DevServer.new(config:)
|
|
@resolver = NameResolver.new(config:)
|
|
end
|
|
|
|
def vite_client_tag(helper, crossorigin: 'anonymous', **)
|
|
return SKIP unless dev_server.running?
|
|
|
|
src = resolver.bundle_path('@vite/client')
|
|
helper.javascript_include_tag(src, type: 'module', extname: false, crossorigin:, **)
|
|
end
|
|
|
|
def vite_react_refresh_tag(helper, **options)
|
|
return SKIP unless dev_server.running?
|
|
|
|
options[:nonce] = true unless options.key?(:nonce)
|
|
|
|
react_refresh = resolver.bundle_path('@react-refresh')
|
|
|
|
preamble = <<~REACT_PREAMBLE_CODE.html_safe # rubocop:disable Rails/OutputSafety
|
|
import RefreshRuntime from '#{react_refresh}'
|
|
RefreshRuntime.injectIntoGlobalHook(window)
|
|
window.$RefreshReg$ = () => {}
|
|
window.$RefreshSig$ = () => (type) => type
|
|
window.__vite_plugin_react_preamble_installed__ = true
|
|
REACT_PREAMBLE_CODE
|
|
|
|
helper.javascript_tag(preamble, type: :module, **options)
|
|
end
|
|
|
|
def vite_javascript_tag(helper, *names, type: 'module', crossorigin: '', asset_type: '', **) # rubocop:disable Lint/UnusedMethodArgument
|
|
return SKIP unless dev_server.running?
|
|
|
|
scripts = names.map { |name| resolver.full_path(name) }
|
|
|
|
helper.javascript_include_tag(*scripts, crossorigin:, type:, extname: false, **)
|
|
end
|
|
|
|
def vite_stylesheet_tag(helper, *names, type: :stylesheet, **options) # rubocop:disable Lint/UnusedMethodArgument
|
|
return SKIP unless dev_server.running?
|
|
|
|
style_paths = names.map { |name| resolver.full_path(name) }
|
|
|
|
options[:extname] = false
|
|
|
|
helper.stylesheet_link_tag(*style_paths, **options)
|
|
end
|
|
|
|
def vite_asset_path(helper, name, **_options)
|
|
return SKIP unless dev_server.running?
|
|
|
|
helper.path_to_asset resolver.full_path(name)
|
|
end
|
|
|
|
def vite_polyfills_tag(*)
|
|
return SKIP unless dev_server.running?
|
|
|
|
''
|
|
end
|
|
|
|
def vite_preload_file_tag(helper, name, crossorigin: 'anonymous', **)
|
|
return SKIP unless dev_server.running?
|
|
|
|
vite_preload_tag(helper, resolver.full_path(name), crossorigin:, **)
|
|
end
|
|
end
|
|
|
|
class ManifestStrategy
|
|
include Common
|
|
|
|
attr_reader :config, :manifest, :resolver
|
|
|
|
def initialize(config:, manifest: nil)
|
|
@config = config
|
|
@manifest = manifest || Manifest.new(config:, logger: config.logger)
|
|
@resolver = NameResolver.new(config:)
|
|
end
|
|
|
|
def vite_client_tag(*)
|
|
''
|
|
end
|
|
|
|
def vite_react_refresh_tag(*)
|
|
''
|
|
end
|
|
|
|
def vite_javascript_tag(helper, *names, type: 'module', crossorigin: '', asset_type: '', media: nil, **)
|
|
names = names.map { |name| resolver.entrypoint_path(name) }
|
|
|
|
scripts = []
|
|
preloads = []
|
|
stylesheets = []
|
|
entries = names.map { |name| manifest.fetch!(name, type: asset_type) }
|
|
|
|
entries.each do |entry|
|
|
scripts << helper.javascript_include_tag(
|
|
resolver.bundle_path(entry[:file]),
|
|
integrity: entry[:integrity],
|
|
crossorigin:,
|
|
type:,
|
|
extname: false,
|
|
**
|
|
)
|
|
|
|
preloads = manifest.imports_for(entry).map do |import|
|
|
vite_preload_tag(
|
|
helper,
|
|
resolver.bundle_path(import[:file]),
|
|
integrity: import[:integrity],
|
|
crossorigin:,
|
|
**
|
|
)
|
|
end
|
|
|
|
stylesheets = manifest.stylesheets_for(entry).map do |stylesheet|
|
|
helper.stylesheet_link_tag(
|
|
resolver.bundle_path(stylesheet[:file]),
|
|
integrity: stylesheet[:integrity],
|
|
crossorigin:,
|
|
media:,
|
|
**
|
|
)
|
|
end
|
|
end
|
|
|
|
helper.safe_join(scripts + preloads + stylesheets)
|
|
end
|
|
|
|
def vite_stylesheet_tag(helper, *names, type: :stylesheet, **options)
|
|
options[:extname] = false
|
|
|
|
stylesheets = names.map do |name|
|
|
entry = manifest.fetch!(resolver.entrypoint_path(name), type:)
|
|
helper.stylesheet_link_tag(
|
|
resolver.bundle_path(entry[:file]),
|
|
integrity: entry[:integrity],
|
|
**options
|
|
)
|
|
end
|
|
|
|
helper.safe_join(stylesheets)
|
|
end
|
|
|
|
def vite_asset_path(helper, name, type: nil, **)
|
|
entry = manifest.fetch!(resolver.entrypoint_path(name), type:)
|
|
helper.path_to_asset resolver.bundle_path(entry[:file])
|
|
end
|
|
|
|
def vite_polyfills_tag(helper, crossorigin: 'anonymous', **)
|
|
entry = manifest.fetch!('polyfills', type: :virtual)
|
|
|
|
helper.javascript_include_tag(
|
|
resolver.bundle_path(entry[:file]),
|
|
type: 'module',
|
|
integrity: entry[:integrity],
|
|
crossorigin:,
|
|
**
|
|
)
|
|
end
|
|
|
|
def vite_preload_file_tag(helper, name, crossorigin: 'anonymous', **)
|
|
entry = manifest.fetch(resolver.entrypoint_path(name))
|
|
return SKIP unless entry
|
|
|
|
vite_preload_tag(helper, resolver.bundle_path(entry[:file]), integrity: entry[:integrity], crossorigin:, **)
|
|
end
|
|
end
|
|
|
|
attr_reader :strategies
|
|
|
|
def initialize(config:, manifest: nil, dev_server: nil)
|
|
@strategies = config.tag_strategies.filter_map do |name|
|
|
case name
|
|
when :dev_server
|
|
DevServerStrategy.new(config:, dev_server:)
|
|
when :manifest
|
|
ManifestStrategy.new(config:, manifest:)
|
|
end
|
|
end
|
|
end
|
|
|
|
def method_missing(method, ...)
|
|
return super unless method.to_s.start_with?('vite_')
|
|
|
|
strategies.lazy.map { |s| s.__send__(method, ...) }.find { |result| result != SKIP } || ''
|
|
end
|
|
|
|
def respond_to_missing?(method, include_private = false)
|
|
return super unless method.to_s.start_with?('vite_')
|
|
|
|
strategies.all? { |s| s.respond_to?(method, include_private) }
|
|
end
|
|
end
|
|
end
|