cat_herder 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 479a2393cf3c34ee5fca8210d9e642232ab012b6475a5204811e913cfbb9a4df
4
+ data.tar.gz: 461262ce3786120921eadfb1b082a4e494a6aacf7bb5806e0b569c3dbfd0c47b
5
+ SHA512:
6
+ metadata.gz: d44bbb630714a52bbed16ed50f5c107e50212ddadcc28d830acd50864ae68c64cf320140fc1103c5ccd7c90ece4acf083c786f5279ac0a478a17377ce15e0d89
7
+ data.tar.gz: cda8936e2835e4e8635317da66c5611ff745799c02e91ac6c150e7dc85d835c3425b1ecfb3b5e8c23228b815fcc358cf00169a83bd8d55dde230e58b9268c973
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Jonathan Hefner
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # cat_herder
2
+
3
+ Minimal Rails asset pipeline experiment:
4
+
5
+ * Assets are fingerprinted and copied to `public/assets` in development, just
6
+ like `rails assets:precompile`. Thus they are served directly, without any
7
+ special routes or middleware.
8
+
9
+ * ERB assets are evaluated; all other assets are copied verbatim.
10
+
11
+ * ERB assets can call `asset_path` and all other [`AssetUrlHelper`][] helpers.
12
+
13
+ * Currently, `*_url` helpers always return a path instead of a full URL (the
14
+ same as `*_path` helpers). Assuming these helpers are primarily used for
15
+ import statements (e.g. `@import url(...)`), this shouldn't pose a problem,
16
+ because the browser resolves such partial URLs to the asset host rather than
17
+ the page host. The benefit of the current implementation is that it
18
+ sidesteps the issue of cache invalidation when `config.asset_host` changes.
19
+
20
+ * ERB assets can call `render` to render the content of another asset inline.
21
+
22
+ * ERB assets can call `glob` to iterate over other assets. Using a given
23
+ pattern, `glob` will search all load paths. With a combination of `glob` and
24
+ `render`, assets can perform their own bundling.
25
+
26
+ * ERB assets can call `resolve` to get an absolute path to an asset file. This
27
+ can be used to pass the asset to an external command, e.g.:
28
+
29
+ ```erb
30
+ <%# styles.css.erb %>
31
+ <%= `sass #{resolve "styles.sass"}` %>
32
+ ```
33
+
34
+ * All calls to `asset_path` / `compute_asset_path`, `render`, and `resolve` will
35
+ add the resulting asset to the current asset's dependencies, so that the
36
+ current asset will be recompiled when any of its dependencies are.
37
+
38
+ * Partial assets are prefixed with an underscore (like view partials), and are
39
+ not copied to `public/assets` by `rails assets:precompile`. They can be
40
+ referenced using their logical path without the underscore (like view
41
+ partials). This allows "private" files, such as raw input files or config
42
+ files, to be placed in any of the asset load paths and be evaluated like other
43
+ assets. (Calls to `compute_asset_path` that resolve to a partial asset will
44
+ raise an error to prevent broken URLs.)
45
+
46
+ [`AssetUrlHelper`]: https://api.rubyonrails.org/classes/ActionView/Helpers/AssetUrlHelper.html
47
+
48
+
49
+ ## Installation
50
+
51
+ Add this line to your application's Gemfile:
52
+
53
+ ```ruby
54
+ gem "cat_herder"
55
+ ```
56
+
57
+ And run:
58
+
59
+ ```bash
60
+ $ bundle install
61
+ ```
62
+
63
+ Then disable Sprockets, and require *cat_herder* in your `config/application.rb`
64
+ file:
65
+
66
+ ```ruby
67
+ require "cat_herder/railtie"
68
+ ```
69
+
70
+
71
+ ## Contributing
72
+
73
+ Run `bin/test` to run the tests.
74
+
75
+
76
+ ## License
77
+
78
+ [MIT License](MIT-LICENSE)
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require "rake/testtask"
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'test'
9
+ t.pattern = 'test/**/*_test.rb'
10
+ t.verbose = false
11
+ end
12
+
13
+ task default: :test
data/lib/cat_herder.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cat_herder/version"
4
+
5
+ module CatHerder
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :AssetNotFound
9
+ autoload :AssetNotPublic
10
+ autoload :Assets
11
+ autoload :Current
12
+ autoload :Helper
13
+
14
+ EMPTY_ARRAY = [].freeze
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CatHerder
4
+ class AssetNotFound < StandardError
5
+ def initialize(logical_path)
6
+ super("Could not find asset #{logical_path.inspect}.")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CatHerder
4
+ class AssetNotPublic < StandardError
5
+ def initialize(asset)
6
+ super("Asset #{asset.logical_path.inspect} (#{asset.source_path.inspect}) does not expose a public path.")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "active_support/core_ext/enumerable"
5
+
6
+ module CatHerder
7
+ module Assets
8
+ extend ActiveSupport::Autoload
9
+
10
+ autoload :ErbAsset
11
+ autoload :VerbatimAsset
12
+
13
+ mattr_accessor :load_paths, default: []
14
+ mattr_accessor :public_subpath, default: "assets"
15
+ mattr_accessor :cache_store
16
+ mattr_accessor :precompiled, default: false
17
+ singleton_class.alias_method :precompiled?, :precompiled
18
+
19
+ class << self
20
+ def cache
21
+ @cache ||= ActiveSupport::Cache.lookup_store(*cache_store)
22
+ end
23
+
24
+ def public_path
25
+ @public_path ||= Rails.public_path.join(public_subpath)
26
+ end
27
+
28
+ def public_files
29
+ public_path.glob("**/*").select(&:file?)
30
+ end
31
+
32
+ def public_file_logical_path(public_file)
33
+ public_file.dirname.relative_path_from(public_path).to_s
34
+ end
35
+
36
+ def resolve_logical_path(logical_path)
37
+ Current.resolved_paths[logical_path] ||= begin
38
+ logical_dirname, logical_basename = File.split(logical_path)
39
+ basename_pattern = /\A_?#{Regexp.escape logical_basename}(?:\.erb)?\z/
40
+ load_paths.find do |load_path|
41
+ dirname = File.expand_path(logical_dirname, load_path)
42
+ basename = Current.dir_children(dirname).find { |name| basename_pattern.match?(name) }
43
+ break File.join(dirname, basename) if basename
44
+ end
45
+ end
46
+ end
47
+
48
+ def [](logical_path)
49
+ source_path = resolve_logical_path(logical_path) or raise AssetNotFound, logical_path
50
+ (@assets ||= {})[source_path] ||= (source_path.end_with?(".erb") ? ErbAsset : VerbatimAsset).new(logical_path, source_path)
51
+ end
52
+
53
+ def glob(*logical_patterns, &block)
54
+ logical_patterns.map! { |pattern| "#{pattern}{,.erb}" }
55
+ load_paths.flat_map { |load_path| Dir.glob(*logical_patterns, base: load_path) }.
56
+ each { |path| path.sub!(%r"_?([^/]+?)(?:\.erb)?\z", '\1') }.uniq.
57
+ tap { |logical_paths| logical_paths.each(&block) if block }
58
+ end
59
+
60
+ def precompile
61
+ public_path.rmtree if public_path.exist?
62
+ glob("**/[^_]*") { |logical_path| self[logical_path].compile }
63
+ @assets&.each_value { |asset| asset.public_file.dirname.rmtree if asset.partial? && asset.public_file.exist? }
64
+ cache.clear
65
+ end
66
+
67
+ def precompiled_asset_paths
68
+ @precompiled_asset_paths ||= public_files.index_by { |file| public_file_logical_path(file) }.
69
+ transform_values! { |file| "/#{file.relative_path_from(Rails.public_path)}" }
70
+ end
71
+
72
+ def precompiled_asset_path(logical_path)
73
+ precompiled_asset_paths[logical_path] or raise AssetNotFound, logical_path
74
+ end
75
+
76
+ def clean
77
+ public_files.each do |file|
78
+ logical_path = public_file_logical_path(file)
79
+ file.delete unless resolve_logical_path(logical_path) && file == self[logical_path].public_file
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CatHerder
4
+ module Assets
5
+ class Asset
6
+ attr_reader :logical_path, :source_path, :partial
7
+ alias :partial? :partial
8
+
9
+ def initialize(logical_path, source_path)
10
+ @logical_path = logical_path
11
+ @source_path = source_path
12
+ @partial = File.basename(source_path).start_with?("_")
13
+ @metadata = Assets.cache.read([self, "metadata"]) || {}
14
+ end
15
+
16
+ def cache_key
17
+ source_path.delete_prefix(Rails.root.to_s)
18
+ end
19
+
20
+ def digest_class
21
+ ActiveSupport::Digest.hash_digest_class
22
+ end
23
+
24
+ def digest
25
+ @metadata[:digest]
26
+ end
27
+
28
+ def dependencies
29
+ @metadata[:dependencies]&.map { |logical_path| Assets[logical_path] } || EMPTY_ARRAY
30
+ end
31
+
32
+ def dependency_digests
33
+ @metadata[:dependency_digests] || EMPTY_ARRAY
34
+ end
35
+
36
+ def mtime
37
+ @metadata[:mtime] || Float::NAN
38
+ end
39
+
40
+ def source_mtime
41
+ Current.mtime(source_path)
42
+ end
43
+
44
+ def stale?
45
+ mtime != source_mtime || dependency_digests != dependencies.map(&:digest) || dependencies.any?(&:stale?)
46
+ end
47
+
48
+ def public_subpath
49
+ File.join(Assets.public_subpath, logical_path, "#{digest}#{File.extname(logical_path)}")
50
+ end
51
+
52
+ def public_file
53
+ Rails.public_path.join(public_subpath)
54
+ end
55
+
56
+ def written?
57
+ Current.mtime(public_file.to_s) > 0
58
+ end
59
+
60
+ def compile
61
+ write if !written? || stale?
62
+ end
63
+
64
+ def write_metadata(digest:, dependencies: nil)
65
+ @metadata = {
66
+ mtime: source_mtime,
67
+ digest: digest,
68
+ dependencies: dependencies&.map(&:logical_path),
69
+ dependency_digests: dependencies&.map(&:digest),
70
+ }
71
+ Assets.cache.write([self, "metadata"], @metadata)
72
+ end
73
+
74
+ def asset_path
75
+ raise AssetNotPublic, self if partial?
76
+ compile
77
+ File.join("/", public_subpath)
78
+ end
79
+
80
+ def render
81
+ compile
82
+ read
83
+ end
84
+
85
+ def write; raise NotImplementedError; end
86
+ def read; raise NotImplementedError; end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/helpers/asset_url_helper"
4
+ require "cat_herder/assets/asset"
5
+
6
+ module CatHerder
7
+ module Assets
8
+ class ErbAsset < Asset
9
+ def write
10
+ result, dependencies = evaluate_erb
11
+ write_metadata(digest: digest_class.hexdigest(result), dependencies: dependencies)
12
+ public_file.tap { |file| file.dirname.mkpath }.write(result)
13
+ end
14
+
15
+ def read
16
+ public_file.read
17
+ end
18
+
19
+ private
20
+ def evaluate_erb
21
+ ruby = Assets.cache.fetch([self, "ruby"], version: source_mtime) do
22
+ require "erubi"
23
+ Erubi::Engine.new(File.read(source_path), filename: source_path).src
24
+ end
25
+ context = ErbContext.new(logical_path)
26
+ [context.instance_eval(ruby), context._dependencies]
27
+ end
28
+
29
+ class ErbContext
30
+ include ActionView::Helpers::AssetUrlHelper
31
+
32
+ attr_reader :_dependencies
33
+
34
+ def initialize(logical_path)
35
+ @_logical_path = logical_path
36
+ @_dependencies = []
37
+ end
38
+
39
+ def compute_asset_path(logical_path, *)
40
+ _dependency(logical_path).asset_path
41
+ end
42
+
43
+ def resolve(logical_path)
44
+ _dependency(logical_path).source_path
45
+ end
46
+
47
+ def render(logical_path)
48
+ _dependency(logical_path).render
49
+ end
50
+
51
+ def glob(*logical_patterns, &block)
52
+ Assets.glob(*logical_patterns.map { |pattern| _expand_logical_path(pattern) }, &block)
53
+ end
54
+
55
+ private
56
+ def _dependency(logical_path)
57
+ dependency = Assets[_expand_logical_path(logical_path)]
58
+ @_dependencies << dependency unless @_dependencies.include?(dependency)
59
+ dependency
60
+ end
61
+
62
+ def _expand_logical_path(logical_path)
63
+ logical_path.start_with?("./", "../") ? Pathname(@_logical_path).dirname.join(logical_path).to_s : logical_path
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "cat_herder/assets/asset"
5
+
6
+ module CatHerder
7
+ module Assets
8
+ class VerbatimAsset < Asset
9
+ def write
10
+ write_metadata(digest: digest_class.file(source_path).hexdigest)
11
+ FileUtils.cp(source_path, public_file.tap { |file| file.dirname.mkpath }) unless partial?
12
+ end
13
+
14
+ def written?
15
+ partial? || super
16
+ end
17
+
18
+ def read
19
+ File.read(source_path)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CatHerder
4
+ class Current < ActiveSupport::CurrentAttributes
5
+ attribute :resolved_paths, :mtime_cache, :dir_children_cache
6
+
7
+ def resolved_paths
8
+ super || (self.resolved_paths = {})
9
+ end
10
+
11
+ def mtime(path)
12
+ (self.mtime_cache ||= {})[path] ||= File.file?(path) ? File.mtime(path).to_f : Float::NAN
13
+ end
14
+
15
+ def dir_children(path)
16
+ (self.dir_children_cache ||= {})[path] ||= File.directory?(path) ? Dir.children(path) : EMPTY_ARRAY
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CatHerder
4
+ module Helper
5
+ def compute_asset_path(logical_path, options = {})
6
+ Assets.precompiled? ? Assets.precompiled_asset_path(logical_path) : Assets[logical_path].asset_path
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "cat_herder"
5
+
6
+ module CatHerder
7
+ class Railtie < ::Rails::Railtie
8
+ config.assets = ActiveSupport::OrderedOptions.new
9
+
10
+ initializer "assets.configure" do |app|
11
+ Assets.load_paths = [
12
+ *app.paths["app/assets"].existent_directories,
13
+ *app.paths["lib/assets"].existent_directories,
14
+ *app.paths["vendor/assets"].existent_directories,
15
+ *app.config.assets.paths,
16
+ ]
17
+ Assets.public_subpath = app.config.assets.prefix.delete_prefix("/") if app.config.assets.prefix
18
+ Assets.cache_store = app.config.assets.cache_store || [:file_store, app.root.join("tmp/assets.cache")]
19
+ Assets.precompiled = app.config.assets.compile == false
20
+ end
21
+
22
+ server do
23
+ if Assets.precompiled?
24
+ Assets.precompiled_asset_paths # warm up
25
+ else
26
+ Assets.clean
27
+ end
28
+ end
29
+
30
+ ActiveSupport.on_load(:action_view) do
31
+ include Helper
32
+ end
33
+
34
+ rake_tasks do
35
+ load "tasks/cat_herder_tasks.rake"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module CatHerder
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :cat_herder do
4
+ desc "Compile all assets"
5
+ task :precompile => :environment do
6
+ CatHerder::Assets.precompile
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cat_herder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Hefner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-03-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ description:
28
+ email:
29
+ - jonathan@hefner.pro
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - lib/cat_herder.rb
38
+ - lib/cat_herder/asset_not_found.rb
39
+ - lib/cat_herder/asset_not_public.rb
40
+ - lib/cat_herder/assets.rb
41
+ - lib/cat_herder/assets/asset.rb
42
+ - lib/cat_herder/assets/erb_asset.rb
43
+ - lib/cat_herder/assets/verbatim_asset.rb
44
+ - lib/cat_herder/current.rb
45
+ - lib/cat_herder/helper.rb
46
+ - lib/cat_herder/railtie.rb
47
+ - lib/cat_herder/version.rb
48
+ - lib/tasks/cat_herder_tasks.rake
49
+ homepage: https://github.com/jonathanhefner/cat_herder
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/jonathanhefner/cat_herder
54
+ source_code_uri: https://github.com/jonathanhefner/cat_herder
55
+ changelog_uri: https://github.com/jonathanhefner/cat_herder/blob/master/CHANGELOG.md
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.1.4
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: Minimal Rails asset pipeline experiment
75
+ test_files: []