feat: add Prism and Assets

This commit is contained in:
dualfroz committed 2026-09-07 15:23:58 +02:00
1 parent 6b7850d756
commit c149318d16
2 files changed
+118

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
require 'fileutils'
module Prism
module Assets
# Sprockets/Propshaft-lite precompile step: walks every file under
# `source`, content-addresses it (sha256, first 8 hex chars) into
# `name-digest.ext` under `output`, and writes a manifest.json mapping
# the original relative path ("css/application.css") to the
# fingerprinted one ("css/application-3f2a9c1e.css"). Re-running with
# unchanged file content reproduces the exact same fingerprinted name
# (pure function of the bytes, no timestamps involved), and any
# fingerprinted file left over from a PREVIOUS compile that no logical
# path points to anymore is deleted - the same cleanup
# `rake assets:clean` does in Rails, done automatically on every compile
# instead of as a separate step.
class Compiler
DIGEST_LENGTH = 8
def initialize(source:, output:)
@source = File.expand_path(source)
@output = File.expand_path(output)
end
def compile!
FileUtils.mkdir_p(@output)
previous = Manifest.load(manifest_path).to_h.values
manifest = Manifest.new
each_source_file do |absolute, logical_path|
fingerprinted = fingerprint(logical_path, absolute)
destination = File.join(@output, fingerprinted)
FileUtils.mkdir_p(File.dirname(destination))
FileUtils.cp(absolute, destination)
manifest[logical_path] = fingerprinted
end
stale = previous - manifest.to_h.values
stale.each { |path| FileUtils.rm_f(File.join(@output, path)) }
manifest.save(manifest_path)
manifest
end
def manifest_path
File.join(@output, 'manifest.json')
end
private
def each_source_file
return unless File.directory?(@source)
Dir.glob(File.join(@source, '**', '*')).sort.each do |absolute|
next unless File.file?(absolute)
yield absolute, absolute.delete_prefix("#{@source}/")
end
end
def fingerprint(logical_path, absolute)
digest = Digest::SHA256.file(absolute).hexdigest[0, DIGEST_LENGTH]
ext = File.extname(logical_path)
base = logical_path.delete_suffix(ext)
"#{base}-#{digest}#{ext}"
end
end
end
end
+50
View File
@@ -0,0 +1,50 @@
module Prism
module Assets
# Maps logical asset paths ("application.css") to fingerprinted paths
# ("application-3f2a9c1e.css") the way Sprockets/Propshaft's
# manifest.json does. Purely a lookup table plus JSON (de)serialization -
# Compiler is what actually populates one.
class Manifest
AssetNotFound = Class.new(StandardError)
def initialize(assets = {})
@assets = assets.transform_keys(&:to_s)
end
attr_reader :assets
def []=(logical_path, fingerprinted_path)
@assets[logical_path.to_s] = fingerprinted_path
end
# Raises when the manifest is authoritative (production-style,
# precompiled ahead of time) so a typo in a view never silently links
# to a 404 - the same tradeoff Rails makes with
# config.assets.unknown_asset_fallback = false.
def lookup(logical_path)
@assets.fetch(logical_path.to_s) do
raise AssetNotFound, "asset #{logical_path.inspect} is not in the manifest " \
"(known: #{@assets.keys.sort.join(', ')})"
end
end
def key?(logical_path)
@assets.key?(logical_path.to_s)
end
def to_h
@assets.dup
end
def save(path)
File.write(path, JSON.pretty_generate(@assets))
end
def self.load(path)
return new unless File.file?(path)
new(JSON.parse(File.read(path)))
end
end
end
end