torba 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ module Torba
2
+ class Engine < Rails::Engine
3
+ initializer "torba.assets" do |app|
4
+ Rails.application.config.assets.paths.concat(Torba.load_path)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ module Torba
2
+ module RemoteSources
3
+ module Common
4
+ # Expands pattern inside content of the remote source.
5
+ # @return [Array<Array(String, String)>] where first element of the tuple is an absolute
6
+ # path to cached file and second element is the same path relative to cache directory.
7
+ # @param pattern [String] see {http://www.rubydoc.info/stdlib/core/Dir#glob-class_method Dir.glob}
8
+ # for pattern examples
9
+ # @note Unlike Dir.glob doesn't include directories.
10
+ def [](pattern)
11
+ ensure_cached
12
+
13
+ Dir.glob(File.join(cache_path, pattern)).reject{ |path| File.directory?(path) }.map do |path|
14
+ [path, path.sub(/#{cache_path}\/?/, "")]
15
+ end
16
+ end
17
+
18
+ def digest
19
+ raise NotImplementedError
20
+ end
21
+
22
+ private
23
+
24
+ def cache_path
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def ensure_cached
29
+ raise NotImplementedError
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ require "torba/remote_sources/zip"
2
+
3
+ module Torba
4
+ module RemoteSources
5
+ # Represents {https://help.github.com/articles/about-releases/ Github release}.
6
+ class GithubRelease < Zip
7
+ # @return [String] repository user and name.
8
+ # @example
9
+ # "jashkenas/underscore"
10
+ attr_reader :source
11
+
12
+ # @return [String] repository tag.
13
+ # @example
14
+ # "v1.8.3"
15
+ attr_reader :tag
16
+
17
+ # @param source see {#source}
18
+ # @param tag see {#tag}
19
+ def initialize(source, tag)
20
+ @source = source
21
+ @tag = tag
22
+ super("https://github.com/#{source}/archive/#{tag}.zip")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,65 @@
1
+ require "tempfile"
2
+ require "fileutils"
3
+
4
+ require "torba/remote_sources/common"
5
+
6
+ module Torba
7
+ module Errors
8
+ ShellCommandFailed = Class.new(StandardError)
9
+ end
10
+
11
+ module RemoteSources
12
+ # Represents remote zip archive.
13
+ class Zip
14
+ include Common
15
+
16
+ attr_reader :url
17
+
18
+ def initialize(url)
19
+ @url = url
20
+ end
21
+
22
+ def digest
23
+ Torba.digest(url)
24
+ end
25
+
26
+ private
27
+
28
+ def cache_path
29
+ @cache_path ||= File.join(Torba.cache_path, digest)
30
+ end
31
+
32
+ def ensure_cached
33
+ unless Dir.exist?(cache_path)
34
+ FileUtils.mkdir_p(cache_path)
35
+
36
+ tempfile = Tempfile.new("torba")
37
+ tempfile.close
38
+
39
+ Torba.ui.info "downloading '#{url}'"
40
+
41
+ [
42
+ "curl -L -o #{tempfile.path} #{url}",
43
+ "unzip -oqq -d #{cache_path} #{tempfile.path}",
44
+ ].each do |command|
45
+ system(command) || raise(Errors::ShellCommandFailed.new(command))
46
+ end
47
+
48
+ get_rid_of_top_level_directory
49
+ end
50
+ rescue
51
+ FileUtils.rm_rf(cache_path)
52
+ raise
53
+ end
54
+
55
+ def get_rid_of_top_level_directory
56
+ top_level_content = Dir.glob("#{cache_path}/*")
57
+ if top_level_content.size == 1 && File.directory?(top_level_content.first)
58
+ top_level_dir = top_level_content.first
59
+ FileUtils.cp_r("#{top_level_dir}/.", cache_path)
60
+ FileUtils.rm_rf(top_level_dir)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
data/lib/torba/ui.rb ADDED
@@ -0,0 +1,26 @@
1
+ require "thor/shell"
2
+
3
+ module Torba
4
+ # Thin wrapper around Thor::Shell.
5
+ class Ui
6
+ def initialize
7
+ @shell = Thor::Base.shell.new
8
+ end
9
+
10
+ def info(message)
11
+ @shell.say(message)
12
+ end
13
+
14
+ def confirm(message)
15
+ @shell.say(message, :green)
16
+ end
17
+
18
+ def suggest(message)
19
+ @shell.say(message, :yellow)
20
+ end
21
+
22
+ def error(message)
23
+ @shell.say(message, :red)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,2 @@
1
+ require "torba"
2
+ Torba.pretty_errors { Torba.verify }
data/lib/torba.rb ADDED
@@ -0,0 +1,80 @@
1
+ require "digest"
2
+
3
+ require "torba/ui"
4
+ require "torba/manifest"
5
+
6
+ # @since 0.1.0
7
+ module Torba
8
+ # @return [String] root path to prepared asset packages. By default it's ".torba" within
9
+ # your OS home directory (i.e. packages are shared between projects).
10
+ # @note use "TORBA_HOME_PATH" env variable to override default value
11
+ def self.home_path
12
+ @home_path ||= ENV["TORBA_HOME_PATH"] || File.join(Dir.home, ".torba")
13
+ end
14
+
15
+ # Override home path with a new value
16
+ # @param val [String] new home path
17
+ # @return [void]
18
+ def self.home_path=(val)
19
+ @home_path = val
20
+ end
21
+
22
+ # @return [String] root path to downloaded yet unprocessed asset packages. By default
23
+ # it's "cache" within {.home_path}.
24
+ # @note use "TORBA_CACHE_PATH" env variable to override default value
25
+ def self.cache_path
26
+ @cache_path ||= ENV["TORBA_CACHE_PATH"] || File.join(home_path, "cache")
27
+ end
28
+
29
+ # @return [Ui]
30
+ def self.ui
31
+ @ui ||= Ui.new
32
+ end
33
+
34
+ # @return [Manifest]
35
+ def self.manifest
36
+ @manifest ||= Manifest.build
37
+ end
38
+
39
+ # @see Manifest#pack
40
+ def self.pack
41
+ manifest.pack
42
+ end
43
+
44
+ # @see Manifest#load_path
45
+ def self.load_path
46
+ manifest.load_path
47
+ end
48
+
49
+ # @see Manifest#verify
50
+ def self.verify
51
+ manifest.verify
52
+ end
53
+
54
+ # @return [String] unique short fingerprint/hash for given string
55
+ # @param [String] string to be hashed
56
+ #
57
+ # @example
58
+ # Torba.digest("path/to/hash") #=> "23e3e63c"
59
+ def self.digest(string)
60
+ Digest::SHA1.hexdigest(string)[0..7]
61
+ end
62
+
63
+ # @yield a block, converts common exceptions into useful messages
64
+ def self.pretty_errors
65
+ yield
66
+ rescue Torba::Errors::UnbuiltPackage
67
+ ui.error "Your Torba is not packed yet."
68
+ ui.suggest "Run `bundle exec torba pack` to install missing packages."
69
+ exit(false)
70
+ rescue Torba::Errors::NothingToImport => e
71
+ ui.error "Couldn't import an asset(-s) '#{e.path}' from import list in '#{e.package}'."
72
+ ui.suggest "Check for typos."
73
+ ui.suggest "Make sure that the path has trailing '/' if its a directory."
74
+ exit(false)
75
+ rescue Torba::Errors::AssetNotFound => e
76
+ ui.error "Couldn't found an asset with path '#{e.message}'."
77
+ ui.suggest "Make sure that you've imported all image assets mentioned in a stylesheet(-s)."
78
+ exit(false)
79
+ end
80
+ end
data/test/Torbafile ADDED
@@ -0,0 +1,5 @@
1
+ gh_release "trumbowyg", source: "torba-rb/Trumbowyg", tag: "1.1.7", import: %w[
2
+ dist/trumbowyg.js
3
+ dist/ui/*.css
4
+ dist/ui/images/
5
+ ]
@@ -0,0 +1,26 @@
1
+ require "test_helper"
2
+
3
+ module Torba
4
+ class AcceptanceTest < Minitest::Test
5
+ def manifest
6
+ @manifest ||= Manifest.build("test/Torbafile")
7
+ end
8
+
9
+ def assert_exists(file_path)
10
+ super File.join(manifest.load_path, file_path)
11
+ end
12
+
13
+ def test_pack
14
+ manifest.pack
15
+
16
+ assert_exists "trumbowyg/trumbowyg.js"
17
+ assert_exists "trumbowyg/trumbowyg.css.erb"
18
+ assert_exists "trumbowyg/icons.png"
19
+ assert_exists "trumbowyg/icons-2x.png"
20
+
21
+ css = File.read File.join(manifest.load_path, "trumbowyg/trumbowyg.css.erb")
22
+ assert_includes css, "background: transparent url('<%= asset_path('trumbowyg/icons.png') %>') no-repeat;"
23
+ assert_includes css, "background-image: url('<%= asset_path('trumbowyg/icons-2x.png') %>')"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,75 @@
1
+ require "test_helper"
2
+
3
+ module Torba
4
+ class CssUrlToErbAssetPathTest < Minitest::Test
5
+ def filter
6
+ CssUrlToErbAssetPath
7
+ end
8
+
9
+ def test_no_url
10
+ css = "p { margin: 0; }"
11
+ assert_equal css, filter.call(css, "/current/file")
12
+ end
13
+
14
+ def test_url_with_data
15
+ css = "background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaskip);"
16
+ assert_equal css, filter.call(css, "/current/file")
17
+ end
18
+
19
+ def test_absolute_url
20
+ css = "background-image: url('/icons.png');"
21
+ assert_equal "background-image: url('/icons.png');", filter.call(css, "/current_file")
22
+ end
23
+
24
+ def test_stylesheet_and_image_in_same_directory
25
+ current_file = "/home/package/dist/ui/stylesheet.css"
26
+ css = "background-image: url('icons.png');"
27
+
28
+ new_content = filter.call(css, current_file) do |image_full_url|
29
+ assert_equal "/home/package/dist/ui/icons.png", image_full_url
30
+ "ui/icons.png"
31
+ end
32
+ assert_equal "background-image: url('<%= asset_path('ui/icons.png') %>');", new_content
33
+ end
34
+
35
+ def test_url_double_quotes
36
+ current_file = "/home/package/dist/ui/stylesheet.css"
37
+ css = 'background-image: url("icons.png");'
38
+
39
+ filter.call(css, current_file) do |image_full_url|
40
+ assert_equal "/home/package/dist/ui/icons.png", image_full_url
41
+ end
42
+ end
43
+
44
+ def test_minified
45
+ current_file = "/home/package/dist/ui/stylesheet.css"
46
+ css = ".image{background-image:url(icons.png)}.border{border(1px solid)};"
47
+
48
+ filter.call(css, current_file) do |image_full_url|
49
+ assert_equal "/home/package/dist/ui/icons.png", image_full_url
50
+ end
51
+ end
52
+
53
+ def test_stylesheet_and_image_in_sibling_directories
54
+ current_file = "/home/package/dist/ui/stylesheet.css"
55
+ css = "background-image: url('../images/icons.png');"
56
+
57
+ new_content = filter.call(css, current_file) do |image_full_url|
58
+ assert_equal "/home/package/dist/images/icons.png", image_full_url
59
+ "images/icons.png"
60
+ end
61
+ assert_equal "background-image: url('<%= asset_path('images/icons.png') %>');", new_content
62
+ end
63
+
64
+ def test_image_in_child_directory_of_stylesheet
65
+ current_file = "/home/package/dist/ui/stylesheet.css"
66
+ css = "background-image: url('./images/icons.png');"
67
+
68
+ new_content = filter.call(css, current_file) do |image_full_url|
69
+ assert_equal "/home/package/dist/ui/images/icons.png", image_full_url
70
+ "ui/images/icons.png"
71
+ end
72
+ assert_equal "background-image: url('<%= asset_path('ui/images/icons.png') %>');", new_content
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,39 @@
1
+ require "test_helper"
2
+
3
+ module Torba
4
+ class ImportListTest < Minitest::Test
5
+ def test_find_by_absolute_path
6
+ item = ImportList::Asset.new("/dir/subdir/file.jpg", "file.jpg")
7
+ list = ImportList.new([item])
8
+
9
+ found = list.find_by_absolute_path("/dir/subdir/file.jpg")
10
+ assert_equal item, found
11
+ end
12
+
13
+ def test_find_by_absolute_path_missing
14
+ list = ImportList.new([])
15
+
16
+ assert_raises(Errors::AssetNotFound) do
17
+ list.find_by_absolute_path("file.jpg")
18
+ end
19
+ end
20
+
21
+ def js_asset
22
+ ImportList::Asset.new("/dir/script.js", "script.js")
23
+ end
24
+
25
+ def css_asset
26
+ ImportList::Asset.new("/dir/stylesheet.css", "stylesheet.css")
27
+ end
28
+
29
+ def test_css_assets
30
+ list = ImportList.new([js_asset, css_asset])
31
+ assert [css_asset], list.css_assets
32
+ end
33
+
34
+ def test_non_css_assets
35
+ list = ImportList.new([js_asset, css_asset])
36
+ assert [js_asset], list.non_css_assets
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,54 @@
1
+ require "test_helper"
2
+
3
+ module Torba
4
+ class ManifestTest < Minitest::Test
5
+ def manifest
6
+ @manifest ||= Manifest.new
7
+ end
8
+
9
+ def test_zip
10
+ manifest.zip "angular", url: "http://angularjs.com/angularjs.zip"
11
+
12
+ assert_equal 1, manifest.packages.size
13
+
14
+ package = manifest.packages.first
15
+ assert_equal "angular", package.name
16
+
17
+ remote = package.remote_source
18
+ assert_instance_of RemoteSources::Zip, remote
19
+ assert_equal "http://angularjs.com/angularjs.zip", remote.url
20
+ end
21
+
22
+ def test_zip_wo_url
23
+ assert_raises(KeyError) do
24
+ manifest.zip "angular"
25
+ end
26
+ end
27
+
28
+ def test_gh_release
29
+ manifest.gh_release "underscore", source: "jashkenas/underscore", tag: "1.8.3"
30
+
31
+ assert_equal 1, manifest.packages.size
32
+
33
+ package = manifest.packages.first
34
+ assert_equal "underscore", package.name
35
+
36
+ remote = package.remote_source
37
+ assert_instance_of RemoteSources::GithubRelease, remote
38
+ assert_equal "jashkenas/underscore", remote.source
39
+ assert_equal "1.8.3", remote.tag
40
+ end
41
+
42
+ def test_gh_release_wo_source
43
+ assert_raises(KeyError) do
44
+ manifest.gh_release "underscore", tag: "1.8.3"
45
+ end
46
+ end
47
+
48
+ def test_gh_release_wo_tag
49
+ assert_raises(KeyError) do
50
+ manifest.gh_release "underscore", source: "jashkenas/underscore"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,143 @@
1
+ require "test_helper"
2
+ require "fileutils"
3
+ require "torba/remote_sources/common"
4
+
5
+ module Torba
6
+ class PackageImportListTest < Minitest::Test
7
+ def source_dir
8
+ @source_dir ||= begin
9
+ dir = File.join(Torba.home_path, "source")
10
+ FileUtils.mkdir_p(dir)
11
+ dir
12
+ end
13
+ end
14
+
15
+ def touch(path)
16
+ absolute_path = File.join(source_dir, path)
17
+ FileUtils.mkdir_p(File.dirname(absolute_path))
18
+ FileUtils.touch(absolute_path)
19
+ end
20
+
21
+ class RemoteSource
22
+ include RemoteSources::Common
23
+
24
+ attr_reader :cache_path
25
+
26
+ def initialize(cache_path)
27
+ @cache_path = cache_path
28
+ end
29
+
30
+ def ensure_cached; end
31
+ end
32
+
33
+ def remote_source
34
+ @remote_source ||= RemoteSource.new(source_dir)
35
+ end
36
+
37
+ def test_single_file_path
38
+ touch("hello.js")
39
+
40
+ list = Package.new("package", remote_source, import: ["hello.js"]).import_list
41
+ assert_equal 1, list.assets.size
42
+ item = list.assets.first
43
+ assert_equal "hello.js", item.subpath
44
+ assert_equal File.join(source_dir, "hello.js"), item.absolute_path
45
+ end
46
+
47
+ def test_single_file_path_with_subdir
48
+ touch("build/hello.js")
49
+
50
+ list = Package.new("package", remote_source, import: ["build/hello.js"]).import_list
51
+ assert_equal 1, list.assets.size
52
+ item = list.assets.first
53
+ assert_equal "hello.js", item.subpath
54
+ assert_equal File.join(source_dir, "build/hello.js"), item.absolute_path
55
+
56
+ touch("build/standalone/hello.js")
57
+
58
+ list = Package.new("package", remote_source, import: ["build/standalone/hello.js"]).import_list
59
+ assert_equal 1, list.assets.size
60
+ item = list.assets.first
61
+ assert_equal "hello.js", item.subpath
62
+ assert_equal File.join(source_dir, "build/standalone/hello.js"), item.absolute_path
63
+ end
64
+
65
+ def test_directory
66
+ touch("build/standalone/hello.js")
67
+
68
+ list = Package.new("package", remote_source, import: ["build/"]).import_list
69
+ assert_equal 1, list.assets.size
70
+ item = list.assets.first
71
+ assert_equal "standalone/hello.js", item.subpath
72
+ assert_equal File.join(source_dir, "build/standalone/hello.js"), item.absolute_path
73
+
74
+
75
+ list = Package.new("package", remote_source, import: ["build/standalone/"]).import_list
76
+ assert_equal 1, list.assets.size
77
+ item = list.assets.first
78
+ assert_equal "hello.js", item.subpath
79
+ assert_equal File.join(source_dir, "build/standalone/hello.js"), item.absolute_path
80
+ end
81
+
82
+ def test_multiple_files
83
+ touch("build/images/first.png")
84
+ touch("build/images/second.png")
85
+
86
+ list = Package.new("package", remote_source, import: ["build/"]).import_list
87
+ assert_equal 2, list.assets.size
88
+
89
+ first_item = list.assets[0]
90
+ assert_equal "images/first.png", first_item.subpath
91
+ assert_equal File.join(source_dir, "build/images/first.png"), first_item.absolute_path
92
+
93
+ second_item = list.assets[1]
94
+ assert_equal "images/second.png", second_item.subpath
95
+ assert_equal File.join(source_dir, "build/images/second.png"), second_item.absolute_path
96
+ end
97
+
98
+ def test_multiple_import_paths
99
+ touch("images/one.jpg")
100
+ touch("js/script.js")
101
+
102
+ list = Package.new("package", remote_source, import: ["images/", "js/script.js"]).import_list
103
+ assert_equal 2, list.assets.size
104
+
105
+ first_item = list.assets[0]
106
+ assert_equal "one.jpg", first_item.subpath
107
+ assert_equal File.join(source_dir, "images/one.jpg"), first_item.absolute_path
108
+
109
+ second_item = list.assets[1]
110
+ assert_equal "script.js", second_item.subpath
111
+ assert_equal File.join(source_dir, "js/script.js"), second_item.absolute_path
112
+ end
113
+
114
+ def test_glob_pattern
115
+ touch("js/hello.js")
116
+ touch("build/css/bye.css")
117
+
118
+ list = Package.new("package", remote_source, import: ["**/*.{js,coffee}"]).import_list
119
+ assert_equal 1, list.assets.size
120
+ item = list.assets.first
121
+ assert_equal "js/hello.js", item.subpath
122
+ assert_equal File.join(source_dir, "js/hello.js"), item.absolute_path
123
+
124
+ list = Package.new("package", remote_source, import: ["build/**/*.css"]).import_list
125
+ assert_equal 1, list.assets.size
126
+ item = list.assets.first
127
+ assert_equal "css/bye.css", item.subpath
128
+ assert_equal File.join(source_dir, "build/css/bye.css"), item.absolute_path
129
+ end
130
+
131
+ def test_missing_file
132
+ assert_raises(Torba::Errors::NothingToImport) do
133
+ Package.new("package", remote_source, import: ["hello.js"]).import_list
134
+ end
135
+
136
+ touch("hello.js")
137
+
138
+ assert_raises(Torba::Errors::NothingToImport) do
139
+ Package.new("package", remote_source, import: ["hello.js", "another_missing.js"]).import_list
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,21 @@
1
+ require "test_helper"
2
+
3
+ module Torba
4
+ class GithubReleaseTest < Minitest::Test
5
+ def remote
6
+ RemoteSources::GithubRelease.new("repo/user", "v.2.0.0")
7
+ end
8
+
9
+ def test_source
10
+ assert_equal "repo/user", remote.source
11
+ end
12
+
13
+ def test_tag
14
+ assert_equal "v.2.0.0", remote.tag
15
+ end
16
+
17
+ def test_url
18
+ assert_equal "https://github.com/repo/user/archive/v.2.0.0.zip", remote.url
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ require "test_helper"
2
+
3
+ module Torba
4
+ class ZipTest < Minitest::Test
5
+ def test_url
6
+ remote = RemoteSources::Zip.new("http://jquery.com/jquery.zip")
7
+ assert_equal "http://jquery.com/jquery.zip", remote.url
8
+ end
9
+
10
+ def test_digest
11
+ remote = RemoteSources::Zip.new("http://jquery.com/jquery.zip")
12
+ same_remote = RemoteSources::Zip.new("http://jquery.com/jquery.zip")
13
+
14
+ assert_equal remote.digest, same_remote.digest
15
+
16
+ another_remote = RemoteSources::Zip.new("http://angularjs.com/angular.zip")
17
+
18
+ refute_equal remote.digest, another_remote.digest
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ require "bundler/setup"
2
+ require "torba"
3
+
4
+ require "minitest/autorun"
5
+ require "tmpdir"
6
+
7
+ module Torba
8
+ module Test
9
+ module TempHome
10
+ def before_setup
11
+ Torba.home_path = @_torba_tmp_dir = Dir.mktmpdir("torba")
12
+ super
13
+ end
14
+
15
+ def after_teardown
16
+ FileUtils.rm_rf(@_torba_tmp_dir)
17
+ Torba.home_path = nil
18
+ super
19
+ end
20
+ end
21
+
22
+ module AssertExists
23
+ def assert_exists(file_path)
24
+ assert File.exists?(file_path)
25
+ end
26
+
27
+ def refute_exists(file_path)
28
+ refute File.exists?(file_path)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ class Minitest::Test
35
+ include Torba::Test::TempHome
36
+ include Torba::Test::AssertExists
37
+ end