propshaft 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: 2eb4dab520f6e9bc958d7ea65ed10531ab418ac34c3daefdcec0b4d61094bcdd
4
+ data.tar.gz: 8d76024de20625ee78b0cc8409a2e74b66ac0ccd8203ba14e263d89b649da21d
5
+ SHA512:
6
+ metadata.gz: 5f8fbd11cd6a8a9031be35139887e1cec8e3823a4eb68e79ad61df91a307aa1222927fd8d87316c6b55df979cdd91f57bc4d653e092190a1b7933db175e44f14
7
+ data.tar.gz: 70a5ad6e8382e5dafc4bed6b26e1f9ba5008f6bd536961393b807cbb47580dbfc95c1c1cbe04b1b4954968213c2aa112ed822e156fe8f044209bade7ccc79de8
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2021 Basecamp
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,43 @@
1
+ # Propshaft
2
+
3
+ Propshaft is an asset pipeline library for Rails. It's built for era where bundling assets to save on HTTP connections is no longer urgent, where JavaScript and CSS is either compiled by dedicated Node.js bundlers or served directly to the browsers, and where increases in bandwidth has made the need for minification less pressing. These factors allow for a dramatically simpler and faster asset pipeline compared to previous options, like Sprockets.
4
+
5
+ So that's what Propshaft doesn't do. Here's what it actually does provide:
6
+
7
+ 1. Configurable load path: You can register directories from multiple places in your app and gems, and reference assets from all of these paths as though they were one.
8
+ 1. Digest processing: All assets in the load path will be copied (or compiled) in a precompilation step for production that also stamps all of them with a digest hash, so you can use long-expiry cache headers for better performance. The digested assets can be referred to through their logical path because the processing leaves a manifest file that provides a way to translate.
9
+ 1. Development server: There's no need to precompile the assets in development. You can refer to them via the same asset_path helpers and they'll be served by a development server.
10
+ 1. Basic compiler step: Propshaft was explicitly not designed to provide full transpiler capabilities. You can get that better elsewhere. But it does offer a simple input->output compiler setup that by default is used to translate `asset-path` function calls in CSS to `url(digested-asset)` instead.
11
+
12
+
13
+ ## Installation
14
+
15
+ With Rails 7+, you can start a new application with propshaft using `rails new myapp -a propshaft` (pending the merge of [rails/rails#43261](https://github.com/rails/rails/pull/43261)).
16
+
17
+
18
+ ## Usage
19
+
20
+ Propshaft makes all the assets from all the paths its been configured with through `config.assets.paths` available for serving and will copy all of them into `public/assets` when precompiling. This is unlike Sprockets, which did not copy over assets that hadn't been explicitly included in one of bundled assets.
21
+
22
+ These assets can be referenced through their logical path using the normal helpers like `asset_path`, `image_tag`, `javascript_include_tag`, and all the other asset helper tags. These logical references are automatically converted into digest-aware paths in production when `assets:precompile` has been run (through a json mapping file found in `public/assets/.manifest.json`).
23
+
24
+ Additionally, Propshaft ships with a CSS function called `asset-path("image.svg")` that'll be compiled into `url("/assets/image-f2e1ec14d6856e1958083094170ca6119c529a73.svg")` when doing `assets:precompile`. This function is applied to all `.css` files.
25
+
26
+
27
+ ## Migrating from Sprockets
28
+
29
+ Propshaft does a lot less than Sprockets, by design, so it might well be a fair bit of work to migrate, if it's even desirable. This is particularly true if you rely on Sprockets to provide any form of transpiling, like CoffeeScript or Sass, or if you rely on any gems that do. You'll need to either stop transpiling or use a Node-based transpiler, like those in `jsbundling-rails` and `cssbundling-rails`.
30
+
31
+ On the other hand, if you're already bundling JavaScript and CSS through a Node-based setup, then Propshaft is going to slot in easily. Since you don't need another tool to bundle or transpile. Just to digest and serve.
32
+
33
+ But for greenfield apps using the default import-map approach, Propshaft can also work well, if you're able to deal with vanilla CSS.
34
+
35
+
36
+ ## Will Propshaft replace Sprockets as the Rails default?
37
+
38
+ 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.
39
+
40
+
41
+ ## License
42
+
43
+ Propshaft is released under the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/setup"
2
+ require "bundler/gem_tasks"
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new do |test|
6
+ test.libs << "test"
7
+ test.test_files = FileList["test/**/*_test.rb"]
8
+ test.warning = false
9
+ end
10
+
11
+ task default: :test
@@ -0,0 +1,50 @@
1
+ require "propshaft/load_path"
2
+ require "propshaft/resolver/dynamic"
3
+ require "propshaft/resolver/static"
4
+ require "propshaft/server"
5
+ require "propshaft/processor"
6
+ require "propshaft/compilers"
7
+ require "propshaft/compilers/css_asset_urls"
8
+
9
+ class Propshaft::Assembly
10
+ attr_reader :config
11
+
12
+ def initialize(config)
13
+ @config = config
14
+ end
15
+
16
+ def load_path
17
+ Propshaft::LoadPath.new(config.paths)
18
+ end
19
+
20
+ def resolver
21
+ if manifest_path.exist?
22
+ Propshaft::Resolver::Static.new manifest_path: manifest_path, prefix: config.prefix
23
+ else
24
+ Propshaft::Resolver::Dynamic.new load_path: load_path, prefix: config.prefix
25
+ end
26
+ end
27
+
28
+ def server
29
+ Propshaft::Server.new(self)
30
+ end
31
+
32
+ def processor
33
+ Propshaft::Processor.new \
34
+ load_path: load_path, output_path: config.output_path, compilers: compilers
35
+ end
36
+
37
+ def compilers
38
+ @compilers ||=
39
+ Propshaft::Compilers.new(self).tap do |compilers|
40
+ Array(config.compilers).each do |(mime_type, klass)|
41
+ compilers.register mime_type, klass
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+ def manifest_path
48
+ config.output_path.join(Propshaft::Processor::MANIFEST_FILENAME)
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ require "digest/sha1"
2
+ require "action_dispatch/http/mime_type"
3
+
4
+ class Propshaft::Asset
5
+ attr_reader :path, :logical_path
6
+
7
+ def initialize(path, logical_path:)
8
+ @path, @logical_path = path, Pathname.new(logical_path)
9
+ end
10
+
11
+ def content
12
+ File.binread(path)
13
+ end
14
+
15
+ def content_type
16
+ Mime::Type.lookup_by_extension(logical_path.extname.from(1))
17
+ end
18
+
19
+ def length
20
+ content.size
21
+ end
22
+
23
+ def digest
24
+ Digest::SHA1.hexdigest(content)
25
+ end
26
+
27
+ def digested_path
28
+ logical_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
29
+ end
30
+
31
+ def ==(other_asset)
32
+ logical_path.hash == other_asset.logical_path.hash
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ class Propshaft::Compilers::CssAssetUrls
2
+ attr_reader :assembly
3
+
4
+ def initialize(assembly)
5
+ @assembly = assembly
6
+ end
7
+
8
+ def compile(input)
9
+ input.gsub(/asset-path\(["']([^"')]+)["']\)/) do |match|
10
+ %[url("/#{assembly.config.prefix}/#{assembly.load_path.find($1).digested_path}")]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ class Propshaft::Compilers
2
+ attr_reader :registrations, :assembly
3
+
4
+ def initialize(assembly)
5
+ @assembly = assembly
6
+ @registrations = Hash.new
7
+ end
8
+
9
+ def register(mime_type, klass)
10
+ registrations[mime_type] ||= []
11
+ registrations[mime_type] << klass
12
+ end
13
+
14
+ def any?
15
+ registrations.any?
16
+ end
17
+
18
+ def compilable?(asset)
19
+ registrations[asset.content_type.to_s].present?
20
+ end
21
+
22
+ def compile(asset)
23
+ if relevant_registrations = registrations[asset.content_type.to_s]
24
+ asset.content.dup.tap do |input|
25
+ relevant_registrations.each do |compiler|
26
+ input.replace compiler.new(assembly).compile(input)
27
+ end
28
+ end
29
+ else
30
+ asset.content
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Propshaft::Helper
2
+ def compute_asset_path(path, options = {})
3
+ Rails.application.assets.resolver.resolve(path)
4
+ end
5
+ end
@@ -0,0 +1,42 @@
1
+ require "propshaft/asset"
2
+
3
+ class Propshaft::LoadPath
4
+ attr_reader :paths
5
+
6
+ def initialize(paths = [])
7
+ @paths = Array(paths).collect { |path| Pathname.new(path) }
8
+ end
9
+
10
+ def find(asset_name)
11
+ assets_by_path[asset_name]
12
+ end
13
+
14
+ def assets
15
+ assets_by_path.values
16
+ end
17
+
18
+ def manifest
19
+ Hash.new.tap do |manifest|
20
+ assets.each do |asset|
21
+ manifest[asset.logical_path.to_s] = asset.digested_path.to_s
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+ def assets_by_path
28
+ Hash.new.tap do |mapped|
29
+ paths.each do |path|
30
+ all_files_from_tree(path).each do |file|
31
+ logical_path = file.relative_path_from(path)
32
+
33
+ mapped[logical_path.to_s] ||= Propshaft::Asset.new(file, logical_path: logical_path)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def all_files_from_tree(path)
40
+ path.children.flat_map { |child| child.directory? ? all_files_from_tree(child) : child }
41
+ end
42
+ end
@@ -0,0 +1,74 @@
1
+ class Propshaft::Processor
2
+ MANIFEST_FILENAME = ".manifest.json"
3
+
4
+ attr_reader :load_path, :output_path, :compilers
5
+
6
+ def initialize(load_path:, output_path:, compilers:)
7
+ @load_path, @output_path = load_path, output_path
8
+ @compilers = compilers
9
+ end
10
+
11
+ def process
12
+ ensure_output_path_exists
13
+ write_manifest
14
+ output_assets
15
+ compress_assets
16
+ end
17
+
18
+ private
19
+ def ensure_output_path_exists
20
+ FileUtils.mkdir_p output_path
21
+ end
22
+
23
+
24
+ def write_manifest
25
+ File.open(output_path.join(MANIFEST_FILENAME), "wb+") do |manifest|
26
+ manifest.write load_path.manifest.to_json
27
+ end
28
+ end
29
+
30
+
31
+ def output_assets
32
+ load_path.assets.each do |asset|
33
+ unless output_path.join(asset.digested_path).exist?
34
+ FileUtils.mkdir_p output_path.join(asset.digested_path.parent)
35
+ output_asset(asset)
36
+ end
37
+ end
38
+ end
39
+
40
+ def output_asset(asset)
41
+ compile_asset(asset) || copy_asset(asset)
42
+ end
43
+
44
+ def copy_asset(asset)
45
+ FileUtils.copy asset.path, output_path.join(asset.digested_path)
46
+ end
47
+
48
+ def compile_asset(asset)
49
+ File.open(output_path.join(asset.digested_path), "w+") do |file|
50
+ begin
51
+ file.write compilers.compile(asset)
52
+ rescue Encoding::UndefinedConversionError
53
+ # FIXME: Not sure if there's a better way here?
54
+ file.write compilers.compile(asset).force_encoding("UTF-8")
55
+ end
56
+ end if compilers.compilable?(asset)
57
+ end
58
+
59
+
60
+ def compress_assets
61
+ # FIXME: Only try to compress text assets with brotli
62
+ load_path.assets.each do |asset|
63
+ compress_asset output_path.join(asset.digested_path)
64
+ end if compressor_available?
65
+ end
66
+
67
+ def compress_asset(path)
68
+ `brotli #{path} -o #{path}.br` unless Pathname.new(path.to_s + ".br").exist?
69
+ end
70
+
71
+ def compressor_available?
72
+ `which brotli`.present?
73
+ end
74
+ end
@@ -0,0 +1,54 @@
1
+ require "rails"
2
+ require "rails/railtie"
3
+ require "active_support/ordered_options"
4
+
5
+ # FIXME: There's gotta be a better way than this hack?
6
+ class Rails::Engine < Rails::Railtie
7
+ initializer :append_assets_path, group: :all do |app|
8
+ app.config.assets.paths.unshift(*paths["vendor/assets"].existent_directories)
9
+ app.config.assets.paths.unshift(*paths["lib/assets"].existent_directories)
10
+ app.config.assets.paths.unshift(*paths["app/assets"].existent_directories)
11
+ end
12
+ end
13
+
14
+ module Propshaft
15
+ class Railtie < ::Rails::Railtie
16
+ config.assets = ActiveSupport::OrderedOptions.new
17
+ config.assets.paths = []
18
+ config.assets.prefix = "/assets"
19
+ config.assets.compilers = [ [ "text/css", Propshaft::Compilers::CssAssetUrls ] ]
20
+
21
+ # Compatibility shiming (need to provide log warnings when used)
22
+ config.assets.precompile = []
23
+ config.assets.debug = nil
24
+ config.assets.quiet = nil
25
+ config.assets.compile = nil
26
+ config.assets.version = nil
27
+ config.assets.css_compressor = nil
28
+ config.assets.js_compressor = nil
29
+
30
+ config.after_initialize do |app|
31
+ config.assets.output_path ||=
32
+ Pathname.new(File.join(app.config.paths["public"].first, app.config.assets.prefix))
33
+
34
+ app.assets = Propshaft::Assembly.new(app.config.assets)
35
+
36
+ app.routes.prepend do
37
+ mount app.assets.server => app.assets.config.prefix
38
+ end
39
+
40
+ ActiveSupport.on_load(:action_view) do
41
+ include Propshaft::Helper
42
+ end
43
+ end
44
+
45
+ rake_tasks do |app|
46
+ namespace :assets do
47
+ desc "Compile all the assets from config.assets.paths"
48
+ task precompile: :environment do
49
+ Rails.application.assets.processor.process
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,15 @@
1
+ module Propshaft::Resolver
2
+ class Dynamic
3
+ attr_reader :load_path, :prefix
4
+
5
+ def initialize(load_path:, prefix:)
6
+ @load_path, @prefix = load_path, prefix
7
+ end
8
+
9
+ def resolve(logical_path)
10
+ if asset = load_path.find(logical_path)
11
+ File.join prefix, asset.logical_path
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module Propshaft::Resolver
2
+ class Static
3
+ attr_reader :manifest_path, :prefix
4
+
5
+ def initialize(manifest_path:, prefix:)
6
+ @manifest_path, @prefix = manifest_path, prefix
7
+ end
8
+
9
+ def resolve(logical_path)
10
+ if asset_path = parsed_manifest[logical_path]
11
+ File.join prefix, asset_path
12
+ end
13
+ end
14
+
15
+ private
16
+ def parsed_manifest
17
+ @parsed_manifest ||= JSON.parse(manifest_path.read)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ require "rack/utils"
2
+
3
+ class Propshaft::Server
4
+ def initialize(assembly)
5
+ @assembly = assembly
6
+ end
7
+
8
+ def call(env)
9
+ if asset = @assembly.load_path.find(requested_path(env))
10
+ compiled_content = @assembly.compilers.compile(asset)
11
+
12
+ [
13
+ 200,
14
+ {
15
+ "Content-Length" => compiled_content.length.to_s,
16
+ "Content-Type" => asset.content_type,
17
+ "ETag" => asset.digest,
18
+ "Cache-Control" => "public, must-revalidate"
19
+ },
20
+ [ compiled_content ]
21
+ ]
22
+ else
23
+ [ 404, { "Content-Type" => "text/plain", "Content-Length" => "9" }, [ "Not found" ] ]
24
+ end
25
+ end
26
+
27
+ private
28
+ def requested_path(env)
29
+ Rack::Utils.unescape(env["PATH_INFO"].to_s.sub(/^\//, ""))
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module Propshaft
2
+ VERSION = "0.1.0"
3
+ end
data/lib/propshaft.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "propshaft/assembly"
2
+ require "propshaft/helper"
3
+ require "propshaft/railtie"
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: propshaft
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - David Heinemeier Hansson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-09-19 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: 7.0.0.alpha2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.0.alpha2
27
+ description:
28
+ email: dhh@hey.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - MIT-LICENSE
34
+ - README.md
35
+ - Rakefile
36
+ - lib/propshaft.rb
37
+ - lib/propshaft/assembly.rb
38
+ - lib/propshaft/asset.rb
39
+ - lib/propshaft/compilers.rb
40
+ - lib/propshaft/compilers/css_asset_urls.rb
41
+ - lib/propshaft/helper.rb
42
+ - lib/propshaft/load_path.rb
43
+ - lib/propshaft/processor.rb
44
+ - lib/propshaft/railtie.rb
45
+ - lib/propshaft/resolver/dynamic.rb
46
+ - lib/propshaft/resolver/static.rb
47
+ - lib/propshaft/server.rb
48
+ - lib/propshaft/version.rb
49
+ homepage: https://github.com/rails/propshaft
50
+ licenses:
51
+ - MIT
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.7.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.1.4
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: Deliver assets for Rails.
72
+ test_files: []