Files
jcoffey-dev 319ed76dbd
Crowdin / Upload translations / upload-translations (push) Skipped
Historical data migration test / test (14-alpine) (push) Canceled after 0s
Historical data migration test / test (15-alpine) (push) Canceled after 0s
Historical data migration test / test (16-alpine) (push) Canceled after 0s
Historical data migration test / test (17-alpine) (push) Canceled after 0s
Bundler Audit / security (push) Canceled after 0s
Haml Linting / lint (push) Canceled after 0s
JavaScript Linting / lint (push) Canceled after 0s
Ruby Linting / lint (push) Canceled after 0s
JavaScript Testing / test (push) Canceled after 0s
Ruby Testing / build (production) (push) Canceled after 0s
Ruby Testing / test (.ruby-version) (push) Canceled after 0s
Ruby Testing / test (3.3) (push) Canceled after 0s
Ruby Testing / test (3.4) (push) Canceled after 0s
Ruby Testing / End to End testing (.ruby-version) (push) Canceled after 0s
Ruby Testing / End to End testing (3.3) (push) Canceled after 0s
Ruby Testing / End to End testing (3.4) (push) Canceled after 0s
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Canceled after 0s
Check i18n / check-i18n (push) Canceled after 0s
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (javascript) (push) Canceled after 0s
CodeQL / Analyze (ruby) (push) Canceled after 0s
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Canceled after 0s
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Canceled after 0s
Ruby Testing / Elastic Search integration testing (3.4, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Canceled after 0s
Check formatting / lint (push) Canceled after 0s
CSS Linting / lint (push) Canceled after 0s
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.19.2) (push) Canceled after 0s
Chromatic / Check for relevant changes (push) Canceled after 0s
Chromatic / Run Chromatic (push) Canceled after 0s
Import Mastodon v4.7.3
Snapshot of upstream tag v4.7.3 (commit 8ff11c2bb7b9f0714e947b10fa3629c6542c7121) from
https://github.com/mastodon/mastodon, unmodified.
2026-10-05 22:03:13 -07:00

193 lines
5.9 KiB
Ruby

# frozen_string_literal: true
module Vite
# We use two manifest files to list the assets loaded in our backend:
# - manifest.json: This is the main file that lists all the javascript and stylesheets our frontend app uses
# - manifest-assets.json: This only lists media assets linked directly from our backend, mainly images
#
# Not all the assets listed in the manifest.json file are linked or used by the app view, so we don't need to
# keep track of those. The idea behind this class is to keep track only of the main entrypoints and all their
# subsequent imports. Imports in this case include other javascript modules and stylesheets generated by Vite.
#
# This class focuses on reducing the amount of data we keep per entrypoint and making fast lookups. To do so
# it keeps two main datastructures:
# - entries: A Hash with references to each main entrypoint (including media assets)
# - pool: An Array with all the referenced imports and stylesheets
#
# Each entrypoint has a list of IDs for each import and stylesheet that it needs to load, this ID is
# an index to the main pool. With this the referenced asset is stored only once and referenced across
# entries with a unique ID. In the original manifest files references are also nested, if an entrypoint
# imports a javascript module, this first module can import other modules and stylesheet and so on. To keep
# fetching entrypoints fast, the final list of modules and stylesheets to load per entrypoint
# is expanded when loading the manifest data and not when rendering views. Each entrypoint has the final
# list of IDs of imports and stylesheets it will need to link in the backend, without further recursive
# checks.
class Manifest
class MissingManifestError < StandardError; end
class MissingEntryError < StandardError
def initialize(entry)
super("Failed to find entry '#{entry}' in manifest files")
end
end
Entrypoint = Struct.new(:file, :integrity, :import_ids, :stylesheet_ids)
Asset = Struct.new(:file, :integrity)
attr_reader :config, :entries, :pool, :logger
def initialize(config:, logger:)
@config = config
@virtual = {}
@entries = {}
@pool = []
@loaded = false
@logger = logger
end
def reload
@loaded = false
load
end
def load
return if @loaded
retries = 0
begin
build if retries.positive?
load_manifest
@loaded = true
rescue Errno::ENOENT => e
if retries.zero? && config.auto_build?
retries = 1
retry
else
raise MissingManifestError, e.message
end
end
end
def fetch(name, type: nil)
load
key = type == :virtual ? @virtual[name] : name
entries[key]
end
def fetch!(name, type: nil)
fetch(name, type:) || raise(MissingEntryError, name)
end
def imports_for(entry)
find_entries(entry[:import_ids] || [])
end
def stylesheets_for(entry)
find_entries(entry[:stylesheet_ids] || [])
end
private
def load_manifest
@virtual = {}
@entries = {}
@pool = []
# intermediate Hash for fast lookups while parsing the manifest
@lookup = {}
logger.debug { "loading manifest file: #{config.manifest_path}" }
json = JSON.load_file(config.manifest_path)
json.each do |key, raw|
# Only select main entrypoints and locales
next unless raw['isEntry'] || key.start_with?('mastodon/locales/')
import_ids, stylesheet_ids = resolve_relations(json, raw)
@virtual[raw['name']] = key
@entries[key] = Entrypoint.new(
raw['file'],
raw['integrity'],
import_ids,
stylesheet_ids
)
end
logger.debug { "loading manifest file: #{config.manifest_assets_path}" }
assets = JSON.load_file(config.manifest_assets_path)
assets.each do |key, raw|
@entries[key] = Asset.new(
raw['file'],
raw['integrity']
)
end
ensure
@lookup = {}
end
def build
logger.debug { 'AutoBuild enabled' }
builder.build
end
def builder
@builder ||= Vite::Builder.new(config:, logger:)
end
def resolve_relations(manifest, raw)
# NOTE: By using Set we make sure we add each asset only once
imports = Set.new
stylesheets = Set.new
raw['imports']&.each do |import|
raw_import = manifest[import]
next unless raw_import
id, deps = lookup(file: raw_import['file'], integrity: raw_import['integrity']) do
resolve_relations(manifest, raw_import)
end
imports.merge(deps[0])
stylesheets.merge(deps[1])
imports.add id
end
raw['css']&.each do |stylesheet|
# the css field includes the file names of the stylesheets and not
# the src names which are the ones used as keys in the manifest
raw_style = manifest.values.find { |value| value['file'] == stylesheet }
# some listed CSS files are not included as a entry in the manifest,
# but they are among the generated assets. We can still link to them
# but without the integrity field
raw_style = { 'file' => stylesheet } if raw_style.nil?
id, = lookup(file: raw_style['file'], integrity: raw_style['integrity'])
stylesheets.add id
end
[imports, stylesheets]
end
# Lookup in the internal cache if an entry has been processed
# If not, yield and save the result of the block as the entry deps
def lookup(file:, integrity:)
id, deps = @lookup[file] || []
if id.nil?
@pool << Asset.new(file, integrity)
id = @pool.size - 1 # last ID
deps = yield if block_given?
@lookup[file] = [id, deps]
end
[id, deps]
end
def find_entries(ids)
ids.filter_map { |id| pool[id] }
end
end
end