Files
mastodon/lib/vite/tagger.rb

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