# 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