Merge remote-tracking branch 'parent/main' into kbtopic-remove-quote

This commit is contained in:
KMY 2025-05-27 10:51:58 +09:00
commit 7c65b6f9df
464 changed files with 8217 additions and 8135 deletions

View file

@ -9,22 +9,20 @@ class Mastodon::RedisConfiguration
def base
@base ||= setup_config(prefix: nil, defaults: DEFAULTS)
.merge(namespace: base_namespace)
end
def sidekiq
@sidekiq ||= setup_config(prefix: 'SIDEKIQ_')
.merge(namespace: sidekiq_namespace)
end
def cache
@cache ||= setup_config(prefix: 'CACHE_')
.merge({
namespace: cache_namespace,
namespace: 'cache',
expires_in: 10.minutes,
connect_timeout: 5,
pool: {
size: Sidekiq.server? ? Sidekiq[:concurrency] : Integer(ENV['MAX_THREADS'] || 5),
size: Sidekiq.server? ? Sidekiq.default_configuration[:concurrency] : Integer(ENV['MAX_THREADS'] || 5),
timeout: 5,
},
})
@ -36,24 +34,6 @@ class Mastodon::RedisConfiguration
ENV['REDIS_DRIVER'] == 'ruby' ? :ruby : :hiredis
end
def namespace
@namespace ||= ENV.fetch('REDIS_NAMESPACE', nil)
end
def base_namespace
return "mastodon_test#{ENV.fetch('TEST_ENV_NUMBER', nil)}" if Rails.env.test?
namespace
end
def sidekiq_namespace
namespace
end
def cache_namespace
namespace ? "#{namespace}_cache" : 'cache'
end
def setup_config(prefix: nil, defaults: {})
prefix = "#{prefix}REDIS_"
@ -62,7 +42,7 @@ class Mastodon::RedisConfiguration
password = ENV.fetch("#{prefix}PASSWORD", nil)
host = ENV.fetch("#{prefix}HOST", defaults[:host])
port = ENV.fetch("#{prefix}PORT", defaults[:port])
db = ENV.fetch("#{prefix}DB", defaults[:db])
db = Rails.env.test? ? ENV.fetch('TEST_ENV_NUMBER', defaults[:db]).to_i + 1 : ENV.fetch("#{prefix}DB", defaults[:db])
return { url:, driver: } if url

View file

@ -2,21 +2,28 @@
module PremailerBundledAssetStrategy
def load(url)
asset_host = ENV['CDN_HOST'] || ENV['WEB_DOMAIN'] || ENV.fetch('LOCAL_DOMAIN', nil)
if ViteRuby.instance.dev_server_running?
# Request from the dev server
return unless url.start_with?("/#{ViteRuby.config.public_output_dir}/")
if Webpacker.dev_server.running?
asset_host = "#{Webpacker.dev_server.protocol}://#{Webpacker.dev_server.host_with_port}"
url = File.join(asset_host, url)
headers = {}
# Vite dev server wants this header for CSS files, otherwise it will respond with a JS file that inserts the CSS (to support hot reloading)
headers['Accept'] = 'text/css' if url.end_with?('.scss', '.css')
Net::HTTP.get(
URI("#{ViteRuby.config.origin}#{url}"),
headers
).presence
else
url = url.delete_prefix(Rails.configuration.action_controller.asset_host) if Rails.configuration.action_controller.asset_host.present?
url = url.delete_prefix('/')
path = Rails.public_path.join(url)
return unless path.exist?
path.read
end
css = if url.start_with?('http')
HTTP.get(url).to_s
else
url = url[1..] if url.start_with?('/')
Rails.public_path.join(url).read
end
css.gsub(%r{url\(/}, "url(#{asset_host}/")
rescue ViteRuby::MissingEntrypointError
# If the path is not in the manifest, ignore it
end
module_function :load

View file

@ -1,16 +0,0 @@
# frozen_string_literal: true
class Redis
module NamespaceExtensions
def exists?(...)
call_with_namespace('exists?', ...)
end
def with
yield self
end
end
end
Redis::Namespace::COMMANDS['exists?'] = [:first]
Redis::Namespace.prepend(Redis::NamespaceExtensions)

View file

@ -31,6 +31,7 @@ class Sanitize
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
next true if /^(mention|hashtag)$/.match?(e) # semantic classes
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
next true if e == 'quote-inline'
end
node['class'] = class_list.join(' ')
@ -133,6 +134,7 @@ class Sanitize
'span' => %w(class translate),
'ol' => %w(start reversed),
'li' => %w(value),
'p' => %w(class),
},
add_attributes: {

View file

@ -14,7 +14,9 @@ end
if Rake::Task.task_defined?('assets:precompile')
Rake::Task['assets:precompile'].enhance do
Webpacker.manifest.refresh
Rake::Task['assets:generate_static_pages'].invoke
end
end
# We don't want vite_ruby to run yarn, we do that in a separate step
Rake::Task['vite:install_dependencies'].clear

View file

@ -63,7 +63,7 @@ namespace :db do
task pre_migration_check: :environment do
pg_version = ActiveRecord::Base.connection.database_version
abort 'This version of Mastodon requires PostgreSQL 12.0 or newer. Please update PostgreSQL before updating Mastodon.' if pg_version < 120_000
abort 'This version of Mastodon requires PostgreSQL 13.0 or newer. Please update PostgreSQL before updating Mastodon.' if pg_version < 130_000
schema_version = ActiveRecord::Migrator.current_version
abort <<~MESSAGE if ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS'] && schema_version < 2023_09_07_150100

422
lib/tasks/dev.rake Normal file
View file

@ -0,0 +1,422 @@
# frozen_string_literal: true
namespace :dev do
desc 'Populate database with test data. Can be run multiple times. Should not be run in production environments'
task populate_sample_data: :environment do
# Create a valid account to showcase multiple post types
showcase_account = Account.create_with(username: 'showcase_account').find_or_create_by!(id: 10_000_000)
showcase_user = User.create_with(
account_id: showcase_account.id,
agreement: true,
password: SecureRandom.hex,
email: ENV.fetch('TEST_DATA_SHOWCASE_EMAIL', 'showcase_account@joinmastodon.org'),
confirmed_at: Time.now.utc,
approved: true,
bypass_registration_checks: true
).find_or_create_by!(id: 10_000_000)
showcase_user.mark_email_as_confirmed!
showcase_user.approve!
french_post = Status.create_with(
text: 'Ceci est un sondage public écrit en Français',
language: 'fr',
account: showcase_account,
visibility: :public,
poll_attributes: {
voters_count: 0,
account: showcase_account,
expires_at: 1.day.from_now,
options: ['ceci est un choix', 'ceci est un autre choix'],
multiple: false,
}
).find_or_create_by!(id: 10_000_000)
private_mentionless = Status.create_with(
text: 'This is a private message written in English',
language: 'en',
account: showcase_account,
visibility: :private
).find_or_create_by!(id: 10_000_001)
public_self_reply_with_cw = Status.create_with(
text: 'This is a public self-reply written in English; it has a CW and a multi-choice poll',
spoiler_text: 'poll (CW example)',
language: 'en',
account: showcase_account,
visibility: :public,
thread: french_post,
poll_attributes: {
voters_count: 0,
account: showcase_account,
expires_at: 1.day.from_now,
options: ['this is a choice', 'this is another choice', 'you can chose any number of them'],
multiple: true,
}
).find_or_create_by!(id: 10_000_002)
ProcessHashtagsService.new.call(public_self_reply_with_cw)
unlisted_self_reply_with_cw_tag_mention = Status.create_with(
text: 'This is an unlisted (Quiet Public) self-reply written in #English; it has a CW, mentions @showcase_account, and uses an emoji 🦣',
spoiler_text: 'CW example',
language: 'en',
account: showcase_account,
visibility: :unlisted,
thread: public_self_reply_with_cw
).find_or_create_by!(id: 10_000_003)
Mention.find_or_create_by!(status: unlisted_self_reply_with_cw_tag_mention, account: showcase_account)
ProcessHashtagsService.new.call(unlisted_self_reply_with_cw_tag_mention)
media_attachment = MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/600x400.png'),
description: 'Mastodon logo'
).find_or_create_by!(id: 10_000_000)
status_with_media = Status.create_with(
text: "This is a public status with a picture and tags. The attached picture has an alt text\n\n#Mastodon #Logo #English #Test",
ordered_media_attachment_ids: [media_attachment.id],
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_004)
media_attachment.update(status_id: status_with_media.id)
ProcessHashtagsService.new.call(status_with_media)
media_attachment = MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/600x400.png'),
description: 'Mastodon logo'
).find_or_create_by!(id: 10_000_001)
status_with_sensitive_media = Status.create_with(
text: "This is the same public status with a picture and tags, but it is marked as sensitive. The attached picture has an alt text\n\n#Mastodon #Logo #English #Test",
ordered_media_attachment_ids: [media_attachment.id],
account: showcase_account,
visibility: :public,
sensitive: true,
thread: status_with_media
).find_or_create_by!(id: 10_000_005)
media_attachment.update(status_id: status_with_sensitive_media.id)
ProcessHashtagsService.new.call(status_with_sensitive_media)
media_attachment = MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/600x400.png'),
description: 'Mastodon logo'
).find_or_create_by!(id: 10_000_002)
status_with_cw_media = Status.create_with(
text: "This is the same public status with a picture and tags, but it is behind a CW. The attached picture has an alt text\n\n#Mastodon #Logo #English #Test",
spoiler_text: 'Mastodon logo',
ordered_media_attachment_ids: [media_attachment.id],
account: showcase_account,
visibility: :public,
sensitive: true,
thread: status_with_sensitive_media
).find_or_create_by!(id: 10_000_006)
media_attachment.update(status_id: status_with_cw_media.id)
ProcessHashtagsService.new.call(status_with_cw_media)
media_attachment = MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/boop.ogg'),
description: 'Mastodon boop'
).find_or_create_by!(id: 10_000_003)
status_with_audio = Status.create_with(
text: "This is the same public status with an audio file and tags. The attached picture has an alt text\n\n#Mastodon #English #Test",
ordered_media_attachment_ids: [media_attachment.id],
account: showcase_account,
visibility: :public,
thread: status_with_cw_media
).find_or_create_by!(id: 10_000_007)
media_attachment.update(status_id: status_with_audio.id)
ProcessHashtagsService.new.call(status_with_audio)
media_attachment = MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/boop.ogg'),
description: 'Mastodon boop'
).find_or_create_by!(id: 10_000_004)
status_with_sensitive_audio = Status.create_with(
text: "This is the same public status with an audio file and tags, but it is marked as sensitive. The attached picture has an alt text\n\n#Mastodon #English #Test",
ordered_media_attachment_ids: [media_attachment.id],
account: showcase_account,
visibility: :public,
sensitive: true,
thread: status_with_audio
).find_or_create_by!(id: 10_000_008)
media_attachment.update(status_id: status_with_sensitive_audio.id)
ProcessHashtagsService.new.call(status_with_sensitive_audio)
media_attachment = MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/boop.ogg'),
description: 'Mastodon boop'
).find_or_create_by!(id: 10_000_005)
status_with_cw_audio = Status.create_with(
text: "This is the same public status with an audio file and tags, but it is behind a CW. The attached picture has an alt text\n\n#Mastodon #English #Test",
spoiler_text: 'Mastodon boop',
ordered_media_attachment_ids: [media_attachment.id],
account: showcase_account,
visibility: :public,
sensitive: true,
thread: status_with_sensitive_audio
).find_or_create_by!(id: 10_000_009)
media_attachment.update(status_id: status_with_cw_audio.id)
ProcessHashtagsService.new.call(status_with_cw_audio)
media_attachments = [
MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/600x400.png'),
description: 'Mastodon logo'
).find_or_create_by!(id: 10_000_006),
MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/attachment.jpg')
).find_or_create_by!(id: 10_000_007),
MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/avatar-high.gif'),
description: 'Walking cartoon cat'
).find_or_create_by!(id: 10_000_008),
MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/text.png'),
description: 'Text saying “Hello Mastodon”'
).find_or_create_by!(id: 10_000_009),
]
status_with_multiple_attachments = Status.create_with(
text: "This is a post with multiple attachments, not all of which have a description\n\n#Mastodon #English #Test",
spoiler_text: 'multiple attachments',
ordered_media_attachment_ids: media_attachments.pluck(:id),
account: showcase_account,
visibility: :public,
sensitive: true,
thread: status_with_cw_audio
).find_or_create_by!(id: 10_000_010)
media_attachments.each { |attachment| attachment.update!(status_id: status_with_multiple_attachments.id) }
ProcessHashtagsService.new.call(status_with_multiple_attachments)
remote_account = Account.create_with(
username: 'fake.example',
domain: 'example.org',
uri: 'https://example.org/foo/bar',
url: 'https://example.org/foo/bar',
locked: true
).find_or_create_by!(id: 10_000_001)
remote_formatted_post = Status.create_with(
text: <<~HTML,
<p>This is a post with a variety of HTML in it</p>
<p>For instance, <strong>this text is bold</strong> and <b>this one as well</b>, while <del>this text is stricken through</del> and <s>this one as well</s>.</p>
<blockquote>
<p>This thing, here, is a block quote<br/>with some <strong>bold</strong> as well</p>
<ul>
<li>a list item</li>
<li>
and another with
<ul>
<li>nested</li>
<li>items!</li>
</ul>
</li>
</ul>
</blockquote>
<pre><code>// And this is some code
// with two lines of comments
</code></pre>
<p>And this is <code>inline</code> code</p>
<p>Finally, please observe this Ruby element: <ruby> <rp>(</rp><rt>Ashita</rt><rp>)</rp> </ruby></p>
HTML
account: remote_account,
uri: 'https://example.org/foo/bar/baz',
url: 'https://example.org/foo/bar/baz'
).find_or_create_by!(id: 10_000_011)
Status.create_with(account: showcase_account, reblog: remote_formatted_post).find_or_create_by!(id: 10_000_012)
unattached_quote_post = Status.create_with(
text: 'This is a quote of a post that does not exist',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_013)
Quote.create_with(
status: unattached_quote_post,
quoted_status: nil
).find_or_create_by!(id: 10_000_000)
self_quote = Status.create_with(
text: 'This is a quote of a public self-post',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_014)
Quote.create_with(
status: self_quote,
quoted_status: status_with_media,
state: :accepted
).find_or_create_by!(id: 10_000_001)
nested_self_quote = Status.create_with(
text: 'This is a quote of a public self-post which itself is a self-quote',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_015)
Quote.create_with(
status: nested_self_quote,
quoted_status: self_quote,
state: :accepted
).find_or_create_by!(id: 10_000_002)
recursive_self_quote = Status.create_with(
text: 'This is a recursive self-quote; no real reason for it to exist, but just to make sure we handle them gracefuly',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_016)
Quote.create_with(
status: recursive_self_quote,
quoted_status: recursive_self_quote,
state: :accepted
).find_or_create_by!(id: 10_000_003)
self_private_quote = Status.create_with(
text: 'This is a public post of a private self-post: the quoted post should not be visible to non-followers',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_017)
Quote.create_with(
status: self_private_quote,
quoted_status: private_mentionless,
state: :accepted
).find_or_create_by!(id: 10_000_004)
uncwed_quote_cwed = Status.create_with(
text: 'This is a quote without CW of a quoted post that has a CW',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_018)
Quote.create_with(
status: uncwed_quote_cwed,
quoted_status: public_self_reply_with_cw,
state: :accepted
).find_or_create_by!(id: 10_000_005)
cwed_quote_cwed = Status.create_with(
text: 'This is a quote with a CW of a quoted post that itself has a CW',
spoiler_text: 'Quote post with a CW',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_019)
Quote.create_with(
status: cwed_quote_cwed,
quoted_status: public_self_reply_with_cw,
state: :accepted
).find_or_create_by!(id: 10_000_006)
pending_quote_post = Status.create_with(
text: 'This quote post is pending',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_020)
Quote.create_with(
status: pending_quote_post,
quoted_status: remote_formatted_post,
activity_uri: 'https://foo/bar',
state: :pending
).find_or_create_by!(id: 10_000_007)
rejected_quote_post = Status.create_with(
text: 'This quote post is rejected',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_021)
Quote.create_with(
status: rejected_quote_post,
quoted_status: remote_formatted_post,
activity_uri: 'https://foo/foo',
state: :rejected
).find_or_create_by!(id: 10_000_008)
revoked_quote_post = Status.create_with(
text: 'This quote post is revoked',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_022)
Quote.create_with(
status: revoked_quote_post,
quoted_status: remote_formatted_post,
activity_uri: 'https://foo/baz',
state: :revoked
).find_or_create_by!(id: 10_000_009)
StatusPin.create_with(account: showcase_account, status: public_self_reply_with_cw).find_or_create_by!(id: 10_000_000)
StatusPin.create_with(account: showcase_account, status: private_mentionless).find_or_create_by!(id: 10_000_001)
showcase_account.update!(
display_name: 'Mastodon test/showcase account',
note: 'Test account to showcase many Mastodon features. Most of its posts are public, but some are private!'
)
remote_quote = Status.create_with(
text: <<~HTML,
<p>This is a self-quote of a remote formatted post</p>
<p class="quote-inline">RE: <a href="https://example.org/foo/bar/baz">https://example.org/foo/bar/baz</a></p>
HTML
account: remote_account,
uri: 'https://example.org/foo/bar/quote',
url: 'https://example.org/foo/bar/quote'
).find_or_create_by!(id: 10_000_023)
Quote.create_with(
status: remote_quote,
quoted_status: remote_formatted_post,
state: :accepted
).find_or_create_by!(id: 10_000_010)
Status.create_with(
account: showcase_account,
reblog: remote_quote
).find_or_create_by!(id: 10_000_024)
media_attachment = MediaAttachment.create_with(
account: showcase_account,
file: File.open('spec/fixtures/files/attachment.jpg')
).find_or_create_by!(id: 10_000_010)
quote_post_with_media = Status.create_with(
text: "This is a status with a picture and tags which also quotes a status with a picture.\n\n#Mastodon #Test",
ordered_media_attachment_ids: [media_attachment.id],
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_025)
media_attachment.update(status_id: quote_post_with_media.id)
ProcessHashtagsService.new.call(quote_post_with_media)
Quote.create_with(
status: quote_post_with_media,
quoted_status: status_with_media,
state: :accepted
).find_or_create_by!(id: 10_000_011)
showcase_sidekick_account = Account.create_with(username: 'showcase_sidekick').find_or_create_by!(id: 10_000_002)
sidekick_user = User.create_with(
account_id: showcase_sidekick_account.id,
agreement: true,
password: SecureRandom.hex,
email: ENV.fetch('TEST_DATA_SHOWCASE_SIDEKICK_EMAIL', 'showcase_sidekick@joinmastodon.org'),
confirmed_at: Time.now.utc,
approved: true,
bypass_registration_checks: true
).find_or_create_by!(id: 10_000_001)
sidekick_user.mark_email_as_confirmed!
sidekick_user.approve!
sidekick_post = Status.create_with(
text: 'This post only exists to be quoted.',
account: showcase_sidekick_account,
visibility: :public
).find_or_create_by!(id: 10_000_026)
sidekick_quote_post = Status.create_with(
text: 'This is a quote of a different user.',
account: showcase_account,
visibility: :public
).find_or_create_by!(id: 10_000_027)
Quote.create_with(
status: sidekick_quote_post,
quoted_status: sidekick_post,
activity_uri: 'https://foo/cross-account-quote',
state: :accepted
).find_or_create_by!(id: 10_000_012)
end
end

View file

@ -32,7 +32,7 @@ namespace :mastodon do
prompt.say('Single user mode disables registrations and redirects the landing page to your public profile.')
env['SINGLE_USER_MODE'] = prompt.yes?('Do you want to enable single user mode?', default: false)
%w(SECRET_KEY_BASE OTP_SECRET).each do |key|
%w(SECRET_KEY_BASE).each do |key|
env[key] = SecureRandom.hex(64)
end

View file

@ -1,34 +0,0 @@
# frozen_string_literal: true
# Disable this task as we use pnpm
require 'semantic_range'
Rake::Task['webpacker:check_yarn'].clear
namespace :webpacker do
desc 'Verifies if Yarn is installed'
task check_yarn: :environment do
begin
yarn_version = `yarn --version`.strip
raise Errno::ENOENT if yarn_version.blank?
yarn_range = '>=4 <5'
is_valid = begin
SemanticRange.satisfies?(yarn_version, yarn_range)
rescue
false
end
unless is_valid
warn "Mastodon and Webpacker requires Yarn \"#{yarn_range}\" and you are using #{yarn_version}"
warn 'Exiting!'
exit!
end
rescue Errno::ENOENT
warn 'Yarn not installed. Please see the Mastodon documentation to install the correct version.'
warn 'Exiting!'
exit!
end
end
end

View file

@ -0,0 +1,132 @@
# frozen_string_literal: true
module ViteRuby::ManifestIntegrityExtension
def path_and_integrity_for(name, **)
entry = lookup!(name, **)
{ path: entry.fetch('file'), integrity: entry.fetch('integrity', nil) }
end
# Find a manifest entry by the *final* file name
def integrity_hash_for_file(file_name)
@integrity_cache ||= {}
@integrity_cache[file_name] ||= begin
entry = manifest.find { |_key, entry| entry['file'] == file_name }
entry[1].fetch('integrity', nil) if entry
end
end
def resolve_entries_with_integrity(*names, **options)
entries = names.map { |name| lookup!(name, **options) }
script_paths = entries.map do |entry|
{
file: entry.fetch('file'),
# TODO: Secure this so we require the integrity hash outside of dev
integrity: entry['integrity'],
}
end
imports = dev_server_running? ? [] : entries.flat_map { |entry| entry['imports'] }.compact
{
scripts: script_paths,
imports: imports.filter_map { |entry| { file: entry.fetch('file'), integrity: entry.fetch('integrity') } }.uniq,
stylesheets: dev_server_running? ? [] : (entries + imports).flat_map { |entry| entry['css'] }.compact.uniq,
}
end
# We need to override this method to not include the manifest, as in our case it is too large and will cause a JSON max nesting error rather than raising the expected exception
def missing_entry_error(name, **)
raise ViteRuby::MissingEntrypointError.new(
file_name: resolve_entry_name(name, **),
last_build: builder.last_build_metadata,
manifest: '',
config: config
)
end
end
ViteRuby::Manifest.prepend ViteRuby::ManifestIntegrityExtension
module ViteRails::TagHelpers::IntegrityExtension
def vite_javascript_tag(*names,
type: 'module',
asset_type: :javascript,
skip_preload_tags: false,
skip_style_tags: false,
crossorigin: 'anonymous',
media: 'screen',
**options)
entries = vite_manifest.resolve_entries_with_integrity(*names, type: asset_type)
''.html_safe.tap do |tags|
entries.fetch(:scripts).each do |script|
tags << javascript_include_tag(
script[:file],
integrity: script[:integrity],
crossorigin: crossorigin,
type: type,
extname: false,
**options
)
end
unless skip_preload_tags
entries.fetch(:imports).each do |import|
tags << vite_preload_tag(import[:file], integrity: import[:integrity], crossorigin: crossorigin, **options)
end
end
options[:extname] = false if Rails::VERSION::MAJOR >= 7
unless skip_style_tags
entries.fetch(:stylesheets).each do |stylesheet|
# This is for stylesheets imported from Javascript. The entry for the JS entrypoint only contains the final CSS file name, so we need to look it up in the manifest
tags << stylesheet_link_tag(
stylesheet,
integrity: vite_manifest.integrity_hash_for_file(stylesheet),
media: media,
**options
)
end
end
end
end
def vite_stylesheet_tag(*names, **options)
''.html_safe.tap do |tags|
names.each do |name|
entry = vite_manifest.path_and_integrity_for(name, type: :stylesheet)
options[:extname] = false if Rails::VERSION::MAJOR >= 7
tags << stylesheet_link_tag(entry[:path], integrity: entry[:integrity], **options)
end
end
end
def vite_preload_file_tag(name,
asset_type: :javascript,
crossorigin: 'anonymous', **options)
''.html_safe.tap do |tags|
entries = vite_manifest.resolve_entries_with_integrity(name, type: asset_type)
entries.fetch(:scripts).each do |script|
tags << vite_preload_tag(script[:file], integrity: script[:integrity], crossorigin: crossorigin, **options)
end
end
rescue ViteRuby::MissingEntrypointError
# Ignore this error, it is not critical if the file is not preloaded
end
def vite_polyfills_tag(crossorigin: 'anonymous', **)
return if ViteRuby.instance.dev_server_running?
entry = vite_manifest.path_and_integrity_for('polyfills', type: :virtual)
javascript_include_tag(entry[:path], type: 'module', integrity: entry[:integrity], crossorigin: crossorigin, **)
end
end
ViteRails::TagHelpers.prepend ViteRails::TagHelpers::IntegrityExtension

View file

@ -1,27 +0,0 @@
# frozen_string_literal: true
module Webpacker::HelperExtensions
def javascript_pack_tag(name, **options)
src, integrity = current_webpacker_instance.manifest.lookup!(name, type: :javascript, with_integrity: true)
javascript_include_tag(src, options.merge(integrity: integrity))
end
def stylesheet_pack_tag(name, **options)
src, integrity = current_webpacker_instance.manifest.lookup!(name, type: :stylesheet, with_integrity: true)
stylesheet_link_tag(src, options.merge(integrity: integrity))
end
def preload_pack_asset(name, **options)
src, integrity = current_webpacker_instance.manifest.lookup!(name, with_integrity: true)
# This attribute will only work if the assets are on a different domain.
# And Webpack will (correctly) only add it in this case, so we need to conditionally set it here
# otherwise the preloaded request and the real request will have different crossorigin values
# and the preloaded file wont be loaded
crossorigin = 'anonymous' if Rails.configuration.action_controller.asset_host.present?
preload_link_tag(src, options.merge(integrity: integrity, crossorigin: crossorigin))
end
end
Webpacker::Helper.prepend(Webpacker::HelperExtensions)

View file

@ -1,17 +0,0 @@
# frozen_string_literal: true
module Webpacker::ManifestExtensions
def lookup(name, pack_type = {})
asset = super
if pack_type[:with_integrity] && asset.respond_to?(:dig)
[asset['src'], asset['integrity']]
elsif asset.respond_to?(:dig)
asset['src']
else
asset
end
end
end
Webpacker::Manifest.prepend(Webpacker::ManifestExtensions)