propshaft 0.7.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a149fdc0fb45487422a71e21df47105ee0071e23f752f596f54cd98400bc99e2
4
- data.tar.gz: 9dcd99aa258a91aa99ac46f1da9379712d836f06c42cd8ce9e6df8b020cc94c6
3
+ metadata.gz: 0cec588585c21b06e765dbf70ac6f0d3a4b9ad2826c8a11120553a8db0931559
4
+ data.tar.gz: dde9f2d2a7095731ea93585e237421b73adc4acc71218ef574e566468796d1eb
5
5
  SHA512:
6
- metadata.gz: 79779f26c1d121049451c5a8d7545e9fbfbcdd2b01a966f89fa0a23aa2b140ea2932828bd4c1b9edbab8be106366f8f92f9daa91bfa1c7af2207619a6fcce9fe
7
- data.tar.gz: 7f609b53f85ef6bdeb53972c11d9a54301cd974c7d5573d5d93fbeb043daed863e08407bb95b45d7c8a55a122770dcb5f9f8b2d55f6830c2adbb5465281613ad
6
+ metadata.gz: c372a207074f656261b8be407d224780678fd68e6caf645323c898ca06cb04f4cc0e1b2ab2ac2d59179162bb3a6c1e137297635aef8c49d1a0e4b3d279e609c6
7
+ data.tar.gz: 80c3963e4963ae52b19793b8de066a173b1227a5b6ea1d1ff4420f88b1939a09f2a476fd55e52a4a18fe688c53bc0862756700723557a108d052441ade893259
data/README.md CHANGED
@@ -49,7 +49,7 @@ But for greenfield apps using the default import-map approach, Propshaft can als
49
49
 
50
50
  ## Will Propshaft replace Sprockets as the Rails default?
51
51
 
52
- Most likely, but Sprockets need to be supported as well for a long time to come. Plenty of apps and gems were built on Sprocket features, and they won't be migrating soon. Still working out the compatibility story. This is very much alpha software at the moment.
52
+ Most likely, but Sprockets needs to be supported as well for a long time to come. Plenty of apps and gems were built on Sprocket features, and they won't be migrating soon. Still working out the compatibility story. This is very much beta software at the moment.
53
53
 
54
54
 
55
55
  ## License
@@ -4,8 +4,8 @@ require "propshaft/resolver/static"
4
4
  require "propshaft/server"
5
5
  require "propshaft/processor"
6
6
  require "propshaft/compilers"
7
- require "propshaft/compilers/css_asset_urls"
8
- require "propshaft/compilers/source_mapping_urls"
7
+ require "propshaft/compiler/css_asset_urls"
8
+ require "propshaft/compiler/source_mapping_urls"
9
9
 
10
10
  class Propshaft::Assembly
11
11
  attr_reader :config
@@ -15,7 +15,7 @@ class Propshaft::Assembly
15
15
  end
16
16
 
17
17
  def load_path
18
- @load_path ||= Propshaft::LoadPath.new(config.paths, version: config.version)
18
+ @load_path ||= Propshaft::LoadPath.new(config.paths, compilers: compilers, version: config.version)
19
19
  end
20
20
 
21
21
  def resolver
@@ -2,15 +2,10 @@ require "digest/sha1"
2
2
  require "action_dispatch/http/mime_type"
3
3
 
4
4
  class Propshaft::Asset
5
- PREDIGESTED_REGEX = /-([0-9a-zA-Z]{7,128}\.digested)/
5
+ attr_reader :path, :logical_path, :load_path
6
6
 
7
- attr_reader :path, :logical_path, :version
8
-
9
- def initialize(path, logical_path:, version: nil)
10
- @path = path
11
- @digest = logical_path.to_s[PREDIGESTED_REGEX, 1]
12
- @logical_path = Pathname.new(@digest ? logical_path.sub("-#{@digest}", "") : logical_path)
13
- @version = version
7
+ def initialize(path, logical_path:, load_path:)
8
+ @path, @logical_path, @load_path = path, Pathname.new(logical_path), load_path
14
9
  end
15
10
 
16
11
  def content
@@ -26,18 +21,31 @@ class Propshaft::Asset
26
21
  end
27
22
 
28
23
  def digest
29
- @digest ||= Digest::SHA1.hexdigest("#{content}#{version}")
24
+ @digest ||= Digest::SHA1.hexdigest("#{content_with_compile_references}#{load_path.version}").first(8)
30
25
  end
31
26
 
32
27
  def digested_path
33
- logical_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
28
+ if already_digested?
29
+ logical_path
30
+ else
31
+ logical_path.sub(/\.(\w+(\.map)?)$/) { |ext| "-#{digest}#{ext}" }
32
+ end
34
33
  end
35
34
 
36
35
  def fresh?(digest)
37
- self.digest == digest
36
+ self.digest == digest || already_digested?
38
37
  end
39
38
 
40
39
  def ==(other_asset)
41
40
  logical_path.hash == other_asset.logical_path.hash
42
41
  end
42
+
43
+ private
44
+ def content_with_compile_references
45
+ content + load_path.find_referenced_by(self).collect(&:content).join
46
+ end
47
+
48
+ def already_digested?
49
+ logical_path.to_s =~ /-([0-9a-zA-Z_-]{7,128})\.digested/
50
+ end
43
51
  end
@@ -1,17 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Propshaft::Compilers::CssAssetUrls
4
- attr_reader :assembly, :url_prefix
3
+ require "propshaft/compiler"
5
4
 
5
+ class Propshaft::Compiler::CssAssetUrls < Propshaft::Compiler
6
6
  ASSET_URL_PATTERN = /url\(\s*["']?(?!(?:\#|%23|data|http|\/\/))([^"'\s?#)]+)([#?][^"')]+)?\s*["']?\)/
7
7
 
8
- def initialize(assembly)
9
- @assembly = assembly
10
- @url_prefix = File.join(assembly.config.host.to_s, assembly.config.prefix.to_s).chomp("/")
8
+ def compile(asset, input)
9
+ input.gsub(ASSET_URL_PATTERN) { asset_url resolve_path(asset.logical_path.dirname, $1), asset.logical_path, $2, $1 }
11
10
  end
12
11
 
13
- def compile(logical_path, input)
14
- input.gsub(ASSET_URL_PATTERN) { asset_url resolve_path(logical_path.dirname, $1), logical_path, $2, $1 }
12
+ def referenced_by(asset, references: Set.new)
13
+ asset.content.scan(ASSET_URL_PATTERN).each do |referenced_asset_url, _|
14
+ referenced_asset = load_path.find(resolve_path(asset.logical_path.dirname, referenced_asset_url))
15
+
16
+ if referenced_asset && references.exclude?(referenced_asset)
17
+ references << referenced_asset
18
+ references.merge referenced_by(referenced_asset, references: references)
19
+ end
20
+ end
21
+
22
+ references
15
23
  end
16
24
 
17
25
  private
@@ -26,7 +34,7 @@ class Propshaft::Compilers::CssAssetUrls
26
34
  end
27
35
 
28
36
  def asset_url(resolved_path, logical_path, fingerprint, pattern)
29
- if asset = assembly.load_path.find(resolved_path)
37
+ if asset = load_path.find(resolved_path)
30
38
  %[url("#{url_prefix}/#{asset.digested_path}#{fingerprint}")]
31
39
  else
32
40
  Propshaft.logger.warn "Unable to resolve '#{pattern}' for missing asset '#{resolved_path}' in #{logical_path}"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "propshaft/compiler"
4
+
5
+ class Propshaft::Compiler::SourceMappingUrls < Propshaft::Compiler
6
+ SOURCE_MAPPING_PATTERN = %r{(//|/\*)# sourceMappingURL=(.+\.map)(\s*?\*\/)?\s*?\Z}
7
+
8
+ def compile(asset, input)
9
+ input.gsub(SOURCE_MAPPING_PATTERN) { source_mapping_url(asset.logical_path, asset_path($2, asset.logical_path), $1, $3) }
10
+ end
11
+
12
+ private
13
+ def asset_path(source_mapping_url, logical_path)
14
+ source_mapping_url.gsub!(/^(.+\/)?#{url_prefix}\//, "")
15
+
16
+ if logical_path.dirname.to_s == "."
17
+ source_mapping_url
18
+ else
19
+ logical_path.dirname.join(source_mapping_url).to_s
20
+ end
21
+ end
22
+
23
+ def source_mapping_url(logical_path, resolved_path, comment_start, comment_end)
24
+ if asset = load_path.find(resolved_path)
25
+ "#{comment_start}# sourceMappingURL=#{url_prefix}/#{asset.digested_path}#{comment_end}"
26
+ else
27
+ Propshaft.logger.warn "Removed sourceMappingURL comment for missing asset '#{resolved_path}' from #{logical_path}"
28
+ "#{comment_start}#{comment_end}"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base compiler from which other compilers can inherit
4
+ class Propshaft::Compiler
5
+ attr_reader :assembly
6
+ delegate :config, :load_path, to: :assembly
7
+
8
+ def initialize(assembly)
9
+ @assembly = assembly
10
+ end
11
+
12
+ # Override this in a specific compiler
13
+ def compile(asset, input)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def referenced_by(asset)
18
+ Set.new
19
+ end
20
+
21
+ private
22
+ def url_prefix
23
+ @url_prefix ||= File.join(config.relative_url_root.to_s, config.prefix.to_s).chomp("/")
24
+ end
25
+ end
@@ -23,11 +23,21 @@ class Propshaft::Compilers
23
23
  if relevant_registrations = registrations[asset.content_type.to_s]
24
24
  asset.content.dup.tap do |input|
25
25
  relevant_registrations.each do |compiler|
26
- input.replace compiler.new(assembly).compile(asset.logical_path, input)
26
+ input.replace compiler.new(assembly).compile(asset, input)
27
27
  end
28
28
  end
29
29
  else
30
30
  asset.content
31
31
  end
32
32
  end
33
+
34
+ def referenced_by(asset)
35
+ Set.new.tap do |references|
36
+ if relevant_registrations = registrations[asset.content_type.to_s]
37
+ relevant_registrations.each do |compiler|
38
+ references.merge compiler.new(assembly).referenced_by(asset)
39
+ end
40
+ end
41
+ end
42
+ end
33
43
  end
@@ -3,5 +3,28 @@ module Propshaft
3
3
  def compute_asset_path(path, options = {})
4
4
  Rails.application.assets.resolver.resolve(path) || raise(MissingAssetError.new(path))
5
5
  end
6
+
7
+ # Add an option to call `stylesheet_link_tag` with `:all` to include every css file found on the load path
8
+ # or `:app` to include css files found in `Rails.root("app/assets/**/*.css")`, which will exclude lib/ and plugins.
9
+ def stylesheet_link_tag(*sources, **options)
10
+ case sources.first
11
+ when :all
12
+ super(*all_stylesheets_paths , **options)
13
+ when :app
14
+ super(*app_stylesheets_paths , **options)
15
+ else
16
+ super
17
+ end
18
+ end
19
+
20
+ # Returns a sorted and unique array of logical paths for all stylesheets in the load path.
21
+ def all_stylesheets_paths
22
+ Rails.application.assets.load_path.asset_paths_by_type("css")
23
+ end
24
+
25
+ # Returns a sorted and unique array of logical paths for all stylesheets in app/assets/**/*.css.
26
+ def app_stylesheets_paths
27
+ Rails.application.assets.load_path.asset_paths_by_glob("#{Rails.root.join("app/assets")}/**/*.css")
28
+ end
6
29
  end
7
30
  end
@@ -1,23 +1,32 @@
1
1
  require "propshaft/asset"
2
2
 
3
3
  class Propshaft::LoadPath
4
- attr_reader :paths, :version
4
+ attr_reader :paths, :compilers, :version
5
5
 
6
- def initialize(paths = [], version: nil)
7
- @paths = dedup(paths)
8
- @version = version
6
+ def initialize(paths = [], compilers:, version: nil)
7
+ @paths, @compilers, @version = dedup(paths), compilers, version
9
8
  end
10
9
 
11
10
  def find(asset_name)
12
11
  assets_by_path[asset_name]
13
12
  end
14
13
 
15
- def assets(content_types: nil)
16
- if content_types
17
- assets_by_path.values.select { |asset| asset.content_type.in?(content_types) }
18
- else
19
- assets_by_path.values
20
- end
14
+ def find_referenced_by(asset)
15
+ compilers.referenced_by(asset).delete(self)
16
+ end
17
+
18
+ def assets
19
+ assets_by_path.values
20
+ end
21
+
22
+ def asset_paths_by_type(content_type)
23
+ (@cached_asset_paths_by_type ||= Hash.new)[content_type] ||=
24
+ extract_logical_paths_from(assets.select { |a| a.content_type == Mime::EXTENSION_LOOKUP[content_type] })
25
+ end
26
+
27
+ def asset_paths_by_glob(glob)
28
+ (@cached_asset_paths_by_glob ||= Hash.new)[glob] ||=
29
+ extract_logical_paths_from(assets.select { |a| a.path.fnmatch?(glob) })
21
30
  end
22
31
 
23
32
  def manifest
@@ -35,9 +44,13 @@ class Propshaft::LoadPath
35
44
  @cache_sweeper ||= begin
36
45
  exts_to_watch = Mime::EXTENSION_LOOKUP.map(&:first)
37
46
  files_to_watch = Array(paths).collect { |dir| [ dir.to_s, exts_to_watch ] }.to_h
47
+ mutex = Mutex.new
38
48
 
39
49
  Rails.application.config.file_watcher.new([], files_to_watch) do
40
- clear_cache
50
+ mutex.synchronize do
51
+ clear_cache
52
+ seed_cache
53
+ end
41
54
  end
42
55
  end
43
56
  end
@@ -48,8 +61,7 @@ class Propshaft::LoadPath
48
61
  paths.each do |path|
49
62
  without_dotfiles(all_files_from_tree(path)).each do |file|
50
63
  logical_path = file.relative_path_from(path)
51
- asset = Propshaft::Asset.new(file, logical_path: logical_path, version: version)
52
- mapped[asset.logical_path.to_s] ||= asset
64
+ mapped[logical_path.to_s] ||= Propshaft::Asset.new(file, logical_path: logical_path, load_path: self)
53
65
  end if path.exist?
54
66
  end
55
67
  end
@@ -59,12 +71,22 @@ class Propshaft::LoadPath
59
71
  path.children.flat_map { |child| child.directory? ? all_files_from_tree(child) : child }
60
72
  end
61
73
 
74
+ def extract_logical_paths_from(assets)
75
+ assets.collect { |asset| asset.logical_path.to_s }.sort
76
+ end
77
+
62
78
  def without_dotfiles(files)
63
79
  files.reject { |file| file.basename.to_s.starts_with?(".") }
64
80
  end
65
81
 
66
82
  def clear_cache
67
83
  @cached_assets_by_path = nil
84
+ @cached_asset_paths_by_type = nil
85
+ @cached_asset_paths_by_glob = nil
86
+ end
87
+
88
+ def seed_cache
89
+ assets_by_path
68
90
  end
69
91
 
70
92
  def dedup(paths)
@@ -42,7 +42,7 @@ class Propshaft::OutputPath
42
42
  modified_at = [ 0, Time.now - mtime ].max
43
43
  modified_at < expires_at || limit < count
44
44
  end
45
-
45
+
46
46
  def remove(path)
47
47
  FileUtils.rm(@path.join(path))
48
48
  Propshaft.logger.info "Removed #{path}"
@@ -20,8 +20,8 @@ class Propshaft::Processor
20
20
  FileUtils.rm_r(output_path) if File.exist?(output_path)
21
21
  end
22
22
 
23
- def clean
24
- Propshaft::OutputPath.new(output_path, load_path.manifest).clean(2, 1.hour)
23
+ def clean(count)
24
+ Propshaft::OutputPath.new(output_path, load_path.manifest).clean(count, 1.hour)
25
25
  end
26
26
 
27
27
  private
@@ -11,13 +11,13 @@ module Propshaft
11
11
  config.assets.prefix = "/assets"
12
12
  config.assets.quiet = false
13
13
  config.assets.compilers = [
14
- [ "text/css", Propshaft::Compilers::CssAssetUrls ],
15
- [ "text/css", Propshaft::Compilers::SourceMappingUrls ],
16
- [ "text/javascript", Propshaft::Compilers::SourceMappingUrls ]
14
+ [ "text/css", Propshaft::Compiler::CssAssetUrls ],
15
+ [ "text/css", Propshaft::Compiler::SourceMappingUrls ],
16
+ [ "text/javascript", Propshaft::Compiler::SourceMappingUrls ]
17
17
  ]
18
18
  config.assets.sweep_cache = Rails.env.development?
19
19
  config.assets.server = Rails.env.development? || Rails.env.test?
20
- config.assets.host = nil
20
+ config.assets.relative_url_root = nil
21
21
 
22
22
  # Register propshaft initializer to copy the assets path in all the Rails Engines.
23
23
  # This makes possible for us to keep all `assets` config in this Railtie, but still
@@ -31,7 +31,7 @@ module Propshaft
31
31
  end
32
32
 
33
33
  config.after_initialize do |app|
34
- config.assets.host = app.config.asset_host
34
+ config.assets.relative_url_root ||= app.config.relative_url_root
35
35
  config.assets.output_path ||=
36
36
  Pathname.new(File.join(app.config.paths["public"].first, app.config.assets.prefix))
37
37
 
@@ -4,7 +4,7 @@ namespace :assets do
4
4
  Rails.application.assets.processor.process
5
5
  if Rails.env.development?
6
6
  puts "Warning: You are precompiling assets in development. Rails will not " \
7
- "serve any changed assets until you delete public/assets/.manifest.json"
7
+ "serve any changed assets until you delete public#{Rails.application.config.assets.prefix}/.manifest.json"
8
8
  end
9
9
  end
10
10
 
@@ -14,8 +14,9 @@ namespace :assets do
14
14
  end
15
15
 
16
16
  desc "Removes old files in config.assets.output_path"
17
- task clean: :environment do
18
- Rails.application.assets.processor.clean
17
+ task :clean, [:count] => [:environment] do |_, args|
18
+ count = args.fetch(:count, 2)
19
+ Rails.application.assets.processor.clean(count.to_i)
19
20
  end
20
21
 
21
22
  desc "Print all the assets available in config.assets.paths"
@@ -1,4 +1,5 @@
1
1
  require "rack/utils"
2
+ require "rack/version"
2
3
 
3
4
  class Propshaft::Server
4
5
  def initialize(assembly)
@@ -14,16 +15,16 @@ class Propshaft::Server
14
15
  [
15
16
  200,
16
17
  {
17
- "content-length" => compiled_content.length.to_s,
18
- "content-type" => asset.content_type.to_s,
19
- "accept-encoding" => "vary",
20
- "etag" => asset.digest,
21
- "cache-control" => "public, max-age=31536000, immutable"
18
+ Rack::CONTENT_LENGTH => compiled_content.length.to_s,
19
+ Rack::CONTENT_TYPE => asset.content_type.to_s,
20
+ VARY => "Accept-Encoding",
21
+ Rack::ETAG => asset.digest,
22
+ Rack::CACHE_CONTROL => "public, max-age=31536000, immutable"
22
23
  },
23
24
  [ compiled_content ]
24
25
  ]
25
26
  else
26
- [ 404, { "content-type" => "text/plain", "content-length" => "9" }, [ "Not found" ] ]
27
+ [ 404, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "9" }, [ "Not found" ] ]
27
28
  end
28
29
  end
29
30
 
@@ -34,9 +35,15 @@ class Propshaft::Server
34
35
  private
35
36
  def extract_path_and_digest(env)
36
37
  full_path = Rack::Utils.unescape(env["PATH_INFO"].to_s.sub(/^\//, ""))
37
- digest = full_path[/-([0-9a-zA-Z]{7,128}(?:\.digested)?)\.[^.]+\z/, 1]
38
+ digest = full_path[/-([0-9a-zA-Z]{7,128})\.(?!digested)[^.]+\z/, 1]
38
39
  path = digest ? full_path.sub("-#{digest}", "") : full_path
39
40
 
40
41
  [ path, digest ]
41
42
  end
43
+
44
+ if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3")
45
+ VARY = "Vary"
46
+ else
47
+ VARY = "vary"
48
+ end
42
49
  end
@@ -1,3 +1,3 @@
1
1
  module Propshaft
2
- VERSION = "0.7.0"
2
+ VERSION = "0.9.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: propshaft
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-04 00:00:00.000000000 Z
11
+ date: 2024-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -78,9 +78,10 @@ files:
78
78
  - lib/propshaft.rb
79
79
  - lib/propshaft/assembly.rb
80
80
  - lib/propshaft/asset.rb
81
+ - lib/propshaft/compiler.rb
82
+ - lib/propshaft/compiler/css_asset_urls.rb
83
+ - lib/propshaft/compiler/source_mapping_urls.rb
81
84
  - lib/propshaft/compilers.rb
82
- - lib/propshaft/compilers/css_asset_urls.rb
83
- - lib/propshaft/compilers/source_mapping_urls.rb
84
85
  - lib/propshaft/errors.rb
85
86
  - lib/propshaft/helper.rb
86
87
  - lib/propshaft/load_path.rb
@@ -113,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
114
  - !ruby/object:Gem::Version
114
115
  version: '0'
115
116
  requirements: []
116
- rubygems_version: 3.4.6
117
+ rubygems_version: 3.5.10
117
118
  signing_key:
118
119
  specification_version: 4
119
120
  summary: Deliver assets for Rails.
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Propshaft::Compilers::SourceMappingUrls
4
- attr_reader :assembly
5
-
6
- SOURCE_MAPPING_PATTERN = %r{^(//|/\*)# sourceMappingURL=(.+\.map)}
7
-
8
- def initialize(assembly)
9
- @assembly = assembly
10
- end
11
-
12
- def compile(logical_path, input)
13
- input.gsub(SOURCE_MAPPING_PATTERN) { source_mapping_url(asset_path($2, logical_path), $1) }
14
- end
15
-
16
- private
17
- def asset_path(source_mapping_url, logical_path)
18
- if logical_path.dirname.to_s == "."
19
- source_mapping_url
20
- else
21
- logical_path.dirname.join(source_mapping_url).to_s
22
- end
23
- end
24
-
25
- def source_mapping_url(resolved_path, comment)
26
- if asset = assembly.load_path.find(resolved_path)
27
- "#{comment}# sourceMappingURL=#{assembly.config.prefix}/#{asset.digested_path}"
28
- else
29
- Propshaft.logger.warn "Removed sourceMappingURL comment for missing asset '#{resolved_path}' from #{resolved_path}"
30
- nil
31
- end
32
- end
33
- end