torba 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +9 -0
- data/.yardopts +1 -0
- data/CONTRIBUTING.md +47 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +7 -0
- data/Readme.md +211 -0
- data/bin/torba +5 -0
- data/lib/torba/cli.rb +18 -0
- data/lib/torba/css_url_to_erb_asset_path.rb +51 -0
- data/lib/torba/import_list.rb +38 -0
- data/lib/torba/manifest.rb +65 -0
- data/lib/torba/package.rb +173 -0
- data/lib/torba/rails.rb +7 -0
- data/lib/torba/remote_sources/common.rb +33 -0
- data/lib/torba/remote_sources/github_release.rb +26 -0
- data/lib/torba/remote_sources/zip.rb +65 -0
- data/lib/torba/ui.rb +26 -0
- data/lib/torba/verify.rb +2 -0
- data/lib/torba.rb +80 -0
- data/test/Torbafile +5 -0
- data/test/acceptance_test.rb +26 -0
- data/test/css_url_to_erb_asset_path_test.rb +75 -0
- data/test/import_list_test.rb +39 -0
- data/test/manifest_test.rb +54 -0
- data/test/package/import_list_test.rb +143 -0
- data/test/remote_sources/github_release_test.rb +21 -0
- data/test/remote_sources/zip_test.rb +21 -0
- data/test/test_helper.rb +37 -0
- data/torba.gemspec +22 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c693882d1f37d1cdf2ea156a9d6fd90aaa42409e
|
4
|
+
data.tar.gz: d57f3ec19cf7274292a3bee7440f9102ddd2e3a6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3191c0a6b2610b78e148fe7f3cbf936f8fd3a82707e62d914c01378ea464bee951b3d6412530089792d10188fba62faa633b52a0ec81e72f1506f548c9418af5
|
7
|
+
data.tar.gz: aa2e6571108e54d3fc76d35e99e5b8d90f23e47ee70c8dd456d7e3aa6cfb06831d6ad5e821b72c0c746f3f80c4b463a0d19e0c2a61c7acb8968b731b0a1c929f
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
## Adding a feature
|
4
|
+
|
5
|
+
1. Open an issue and explain what you're planning to do. It is better to discuss new idea first,
|
6
|
+
rather when diving into code.
|
7
|
+
2. Add some tests.
|
8
|
+
3. Write the code.
|
9
|
+
4. Make sure all tests pass.
|
10
|
+
5. Commit with detailed explanation what you've done in a message.
|
11
|
+
6. Open pull request.
|
12
|
+
|
13
|
+
## Breaking/removing a feature
|
14
|
+
|
15
|
+
1. Add deprecation warning and fallback to old behaivour if possible.
|
16
|
+
2. Explain how to migrate to the new code in CHANGELOG.
|
17
|
+
3. Update/remove tests.
|
18
|
+
4. Update the code.
|
19
|
+
5. Make sure all tests pass.
|
20
|
+
6. Commit with detailed explanation what you've done in a message.
|
21
|
+
7. Open pull request.
|
22
|
+
|
23
|
+
## Fixing a bug
|
24
|
+
|
25
|
+
1. Add failing test.
|
26
|
+
2. Fix the bug.
|
27
|
+
3. Make sure all tests pass.
|
28
|
+
4. Commit with detailed explanation what you've done in a message.
|
29
|
+
5. Open pull request.
|
30
|
+
|
31
|
+
## Fixing a typo
|
32
|
+
|
33
|
+
1. Commit with a message that include "[ci skip]" remark.
|
34
|
+
2. Open pull request.
|
35
|
+
|
36
|
+
## Running the tests
|
37
|
+
|
38
|
+
```
|
39
|
+
rake test
|
40
|
+
```
|
41
|
+
|
42
|
+
## Working with documentation
|
43
|
+
|
44
|
+
```
|
45
|
+
yard server -dr
|
46
|
+
open http://localhost:8808
|
47
|
+
```
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Andrii Malyshko
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/Readme.md
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
# Torba
|
2
|
+
|
3
|
+
[![Build Status](https://img.shields.io/travis/torba-rb/torba.svg)](https://travis-ci.org/torba-rb/torba)
|
4
|
+
[![Gem version](https://img.shields.io/gem/v/torba.svg)](https://rubygems.org/gems/torba)
|
5
|
+
|
6
|
+
**Torba** is a [Bower][bower]-less asset manager for [Sprockets][sprockets]. It makes a local copy
|
7
|
+
of a JS/CSS library and puts it under Sprockets' [load path][sprockets-load-path].
|
8
|
+
|
9
|
+
## Name origin
|
10
|
+
|
11
|
+
"Торба" [[tǒːrba][torba-pronounce]] in Ukrainian and "torba" in Polish, Turkic languages can mean
|
12
|
+
"duffel bag", "gunny sack" or, more generally, any flexible container.
|
13
|
+
|
14
|
+
## Status
|
15
|
+
|
16
|
+
Not tested in production.
|
17
|
+
|
18
|
+
## Documentation
|
19
|
+
|
20
|
+
http://rubydoc.info/github/torba-rb/torba/
|
21
|
+
|
22
|
+
## Why
|
23
|
+
|
24
|
+
De facto approach, i.e. wrapping JS and CSS libraries in a gem, requires from a
|
25
|
+
maintainer to constantly track changes in upstream repository. Even more, if the
|
26
|
+
maintainer stops using this gem itself, it will eventually become abandoned.
|
27
|
+
Many libraries still have no gem wrappers.
|
28
|
+
|
29
|
+
Among other alternatives:
|
30
|
+
|
31
|
+
* [rails-assets][rails-assets] project relies on Bower *and* it is quite complex,
|
32
|
+
* [bower-rails][bower-rails] project relies on Bower.
|
33
|
+
|
34
|
+
Problems with the Bower:
|
35
|
+
|
36
|
+
* it is not a part of Ruby ecosystem,
|
37
|
+
* frontend JS libraries are usually standalone (except for jQuery dependency), so there's
|
38
|
+
no need for complex Bundler-like solution with tree-dependency resolution,
|
39
|
+
* often we can't use optimistic version constraints, because JS community still doesn't
|
40
|
+
fully grasp the idea of [Semver][semver]. By specifying strict versions we use Bower
|
41
|
+
as a complex facade for functionality that could be done by curl.
|
42
|
+
|
43
|
+
## External dependencies
|
44
|
+
|
45
|
+
* curl
|
46
|
+
* unzip
|
47
|
+
|
48
|
+
## Design limitations
|
49
|
+
|
50
|
+
* Torba doesn't do any version dependency resolution, it's up to you to specify correct version of
|
51
|
+
each asset package,
|
52
|
+
* Torba doesn't do any builds, use remote sources with pre-built assets.
|
53
|
+
|
54
|
+
## Installation
|
55
|
+
|
56
|
+
Add this line to your application's Gemfile and run `bundle`:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
gem 'torba'
|
60
|
+
```
|
61
|
+
|
62
|
+
### Rails
|
63
|
+
|
64
|
+
in boot.rb
|
65
|
+
|
66
|
+
```diff
|
67
|
+
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
68
|
+
+require 'torba/verify'
|
69
|
+
```
|
70
|
+
|
71
|
+
it config/application.rb
|
72
|
+
|
73
|
+
```diff
|
74
|
+
# Require the gems listed in Gemfile, including any gems
|
75
|
+
# you've limited to :test, :development, or :production.
|
76
|
+
Bundler.require(*Rails.groups)
|
77
|
+
+
|
78
|
+
+require 'torba/rails'
|
79
|
+
```
|
80
|
+
|
81
|
+
## Usage
|
82
|
+
|
83
|
+
1. Create Torbafile at the project root and commit it.
|
84
|
+
|
85
|
+
2. Run `bundle exec torba pack`.
|
86
|
+
|
87
|
+
3. Add "require" [Sprockets directives][sprockets-directives] to your "application.js"
|
88
|
+
and/or "@import" [Sass directives][sass-import] to "application.css".
|
89
|
+
|
90
|
+
If any changes made to the Torbafile, run `bundle exec torba pack` again.
|
91
|
+
|
92
|
+
### Torbafile
|
93
|
+
|
94
|
+
Torbafile is an assets specification. It is a plain text file that contains one or more
|
95
|
+
sections, each of them describes one remote source of assets.
|
96
|
+
|
97
|
+
Currently only zip archives and [Github releases][github-releases] are supported.
|
98
|
+
|
99
|
+
#### Zip archive package
|
100
|
+
|
101
|
+
Allows to download and unpack asset package from any source accessible by curl.
|
102
|
+
|
103
|
+
The syntax is:
|
104
|
+
|
105
|
+
```
|
106
|
+
zip "name", url: "..." [, import: %w(...)]
|
107
|
+
```
|
108
|
+
|
109
|
+
where "name" is an arbitrary name for the package, more on "import" below. For example,
|
110
|
+
|
111
|
+
```
|
112
|
+
zip "scroll_magic", url: "https://github.com/janpaepke/ScrollMagic/archive/v2.0.0.zip"
|
113
|
+
```
|
114
|
+
|
115
|
+
#### Github release package
|
116
|
+
|
117
|
+
This is a more readable version/shortcut for "https://github.com/.../archive/..." URLs.
|
118
|
+
|
119
|
+
The syntax is:
|
120
|
+
|
121
|
+
```
|
122
|
+
gh_release "name", source: "...", tag: "..." [, import: %w(...)]
|
123
|
+
```
|
124
|
+
|
125
|
+
where "source" is the user + repository and "tag" is the repository tag (exactly as on Github,
|
126
|
+
i.e. with "v" prefix if present), more on "import" below. For example,
|
127
|
+
|
128
|
+
```
|
129
|
+
gh_release "scroll_magic", source: "janpaepke/ScrollMagic", tag: "v.2.0.0"
|
130
|
+
```
|
131
|
+
|
132
|
+
### "Packing the torba" process
|
133
|
+
|
134
|
+
When you run `torba pack` the following happens:
|
135
|
+
|
136
|
+
1. All remote sources are cached locally.
|
137
|
+
|
138
|
+
2. Archives are unpacked with top level directory removed. This is done for good cause it
|
139
|
+
usually contains package version, e.g. "react-0.13.2", and you don't want to reference versions
|
140
|
+
inside your application code (except Torbafile).
|
141
|
+
|
142
|
+
3. Remote source's content is copied as is to the `Torba.home_path` location with **package name used
|
143
|
+
as a namespace**.
|
144
|
+
|
145
|
+
This is also done for good in order to avoid name collisions (since many JS projects can have
|
146
|
+
assets with the same names and all packages are placed into shared Sprockets' virtual filesystem).
|
147
|
+
The downside is that you have to use namespace in each require directive, which can lead to
|
148
|
+
duplication:
|
149
|
+
|
150
|
+
```javascript
|
151
|
+
// application.js
|
152
|
+
//= require 'underscore/underscore'
|
153
|
+
```
|
154
|
+
|
155
|
+
Hint: use "require_directory" if you strongly against such duplication:
|
156
|
+
|
157
|
+
```javascript
|
158
|
+
//= require_directory 'underscore'
|
159
|
+
```
|
160
|
+
|
161
|
+
4. Stylesheets (if any) are converted to ".css.erb" with "asset_path" helpers used in "url(...)"
|
162
|
+
statements.
|
163
|
+
|
164
|
+
### :import option
|
165
|
+
|
166
|
+
Copying whole remote source's content has one disadvantage of using remote source specific paths in your
|
167
|
+
require/import directives. For example, if an archive contains file in "dist/css" directory, you'll have
|
168
|
+
to mention it:
|
169
|
+
|
170
|
+
```css
|
171
|
+
/* application.css */
|
172
|
+
@import 'lightslider/dist/css/lightslider';
|
173
|
+
```
|
174
|
+
|
175
|
+
To mitigate this you can cherry-pick files from the source via "import" option, for example:
|
176
|
+
|
177
|
+
```
|
178
|
+
gh_release "lightslider", source: "sachinchoolur/lightslider", tag: "1.1.2", import: %w[
|
179
|
+
dist/css/lightslider.css
|
180
|
+
]
|
181
|
+
```
|
182
|
+
|
183
|
+
Such files will be copied directly to the package root (i.e. file tree becomes flatten), thus you
|
184
|
+
can omit unnesseccary paths:
|
185
|
+
|
186
|
+
```css
|
187
|
+
@import 'lightslider/lightslider';
|
188
|
+
```
|
189
|
+
|
190
|
+
You can use any Dir.glob pattern:
|
191
|
+
|
192
|
+
```
|
193
|
+
gh_release "lightslider", source: "sachinchoolur/lightslider", tag: "1.1.2", import: %w[
|
194
|
+
dist/css/lightslider.css
|
195
|
+
dist/img/*.png
|
196
|
+
]
|
197
|
+
```
|
198
|
+
|
199
|
+
In addition to this "path/" is treated as a shortcut for "path/**/*" glob pattern.
|
200
|
+
|
201
|
+
|
202
|
+
[bower]: http://bower.io/
|
203
|
+
[sprockets]: https://github.com/sstephenson/sprockets/
|
204
|
+
[sprockets-load-path]: https://github.com/sstephenson/sprockets#the-load-path
|
205
|
+
[torba-pronounce]: http://upload.wikimedia.org/wikipedia/commons/2/28/Uk-%D1%82%D0%BE%D1%80%D0%B1%D0%B0.ogg
|
206
|
+
[github-releases]: https://help.github.com/articles/about-releases/
|
207
|
+
[sprockets-directives]: https://github.com/sstephenson/sprockets#the-directive-processor
|
208
|
+
[sass-import]: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#import
|
209
|
+
[rails-assets]: https://rails-assets.org/
|
210
|
+
[bower-rails]: https://github.com/rharriso/bower-rails
|
211
|
+
[semver]: http://semver.org/
|
data/bin/torba
ADDED
data/lib/torba/cli.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "torba"
|
3
|
+
|
4
|
+
module Torba
|
5
|
+
class Cli < Thor
|
6
|
+
desc "pack", "download and prepare all packages defined in Torbafile"
|
7
|
+
def pack
|
8
|
+
Torba.pretty_errors { Torba.pack }
|
9
|
+
Torba.ui.confirm "Torba has been packed!"
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "verify", "check if all packages are prepared"
|
13
|
+
def verify
|
14
|
+
Torba.pretty_errors { Torba.verify }
|
15
|
+
Torba.ui.confirm "Torba is prepared!"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Torba
|
2
|
+
# Parses content of CSS file and converts its image assets paths into Sprockets'
|
3
|
+
# {https://github.com/sstephenson/sprockets#logical-paths logical paths}.
|
4
|
+
class CssUrlToErbAssetPath
|
5
|
+
URL_RE =
|
6
|
+
/
|
7
|
+
url\( # url(
|
8
|
+
\s* # optional space
|
9
|
+
(?!data) # no data URIs
|
10
|
+
['"]? # optional quote
|
11
|
+
(?!\/) # only relative location
|
12
|
+
([^'"]+?) # location
|
13
|
+
['"]? # optional quote
|
14
|
+
\s* # optional space
|
15
|
+
\) # )
|
16
|
+
/xm
|
17
|
+
|
18
|
+
# @return [String] CSS file content where image "url(...)" paths are replaced by ERB
|
19
|
+
# interpolations "url(<%= asset_path(...) %>)".
|
20
|
+
# @param content [String] content of original CSS file
|
21
|
+
# @param file_path [String] absolute path to original CSS file
|
22
|
+
# @yield [image_file_path]
|
23
|
+
# @yieldparam image_file_path [String] absolute path to original image file which is mentioned
|
24
|
+
# within CSS file
|
25
|
+
# @yieldreturn [String] logical path to image file within Sprockets' virtual filesystem.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# content = \
|
29
|
+
# ".react-toolbar {
|
30
|
+
# width: 100%;
|
31
|
+
# background: url('./images/toolbar.png');
|
32
|
+
# }"
|
33
|
+
#
|
34
|
+
# new_content = CssUrlToErbAssetPath.call(content, "/var/downloads/react_unzipped/styles.css") do |url|
|
35
|
+
# url.sub("/var/downloads/react_unzipped/images", "react-toolbar-js"
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# new_content #=>
|
39
|
+
# ".react-toolbar {
|
40
|
+
# width: 100%;
|
41
|
+
# background: url('<%= asset_path('react-toolbar-js/toolbar.png') %>');
|
42
|
+
# }"
|
43
|
+
def self.call(content, file_path)
|
44
|
+
content.gsub(URL_RE) do
|
45
|
+
absolute_image_file_path = File.expand_path($1, File.dirname(file_path))
|
46
|
+
sprockets_file_path = yield absolute_image_file_path
|
47
|
+
"url('<%= asset_path('#{sprockets_file_path}') %>')"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Torba
|
2
|
+
module Errors
|
3
|
+
AssetNotFound = Class.new(StandardError)
|
4
|
+
end
|
5
|
+
|
6
|
+
# Represents a list of assets to be imported from a remote source.
|
7
|
+
class ImportList
|
8
|
+
class Asset < Struct.new(:absolute_path, :subpath)
|
9
|
+
def css?
|
10
|
+
absolute_path.end_with?(".css")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Array<Asset>] full list of assets to be imported.
|
15
|
+
attr_reader :assets
|
16
|
+
|
17
|
+
def initialize(assets)
|
18
|
+
@assets = assets
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Asset] asset with given path.
|
22
|
+
# @param path [String] absolute path of an asset.
|
23
|
+
# @raise [Errors::AssetNotFound] if nothing found
|
24
|
+
def find_by_absolute_path(path)
|
25
|
+
assets.find { |asset| asset.absolute_path == path } || raise(Errors::AssetNotFound.new(path))
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Array<Asset>] list of stylesheets to be imported.
|
29
|
+
def css_assets
|
30
|
+
assets.find_all { |asset| asset.css? }
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Array<Asset>] list of assets to be imported except stylesheets.
|
34
|
+
def non_css_assets
|
35
|
+
assets.find_all { |asset| !asset.css? }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "torba/package"
|
2
|
+
require "torba/remote_sources/zip"
|
3
|
+
require "torba/remote_sources/github_release"
|
4
|
+
|
5
|
+
module Torba
|
6
|
+
# Represents Torbafile.
|
7
|
+
class Manifest
|
8
|
+
# all packages defined in Torbafile
|
9
|
+
attr_reader :packages
|
10
|
+
|
11
|
+
# Reads Torbafile and evaluates it.
|
12
|
+
# @return [Manifest]
|
13
|
+
#
|
14
|
+
# @overload self.build(file_path)
|
15
|
+
# @param file_path [String] absolute path to Torbafile
|
16
|
+
#
|
17
|
+
# @overload self.build
|
18
|
+
# Reads Torbafile from current directory
|
19
|
+
def self.build(file_path = nil)
|
20
|
+
file_path ||= File.join(Dir.pwd, "Torbafile")
|
21
|
+
|
22
|
+
manifest = new
|
23
|
+
content = File.read(file_path)
|
24
|
+
manifest.instance_eval(content, file_path)
|
25
|
+
manifest
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@packages = []
|
30
|
+
end
|
31
|
+
|
32
|
+
# Adds {Package} with {RemoteSources::Zip} to {#packages}
|
33
|
+
def zip(name, options = {})
|
34
|
+
url = options.fetch(:url)
|
35
|
+
remote_source = RemoteSources::Zip.new(url)
|
36
|
+
packages << Package.new(name, remote_source, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Adds {Package} with {RemoteSources::GithubRelease} to {#packages}
|
40
|
+
def gh_release(name, options = {})
|
41
|
+
source = options.fetch(:source)
|
42
|
+
tag = options.fetch(:tag)
|
43
|
+
remote_source = RemoteSources::GithubRelease.new(source, tag)
|
44
|
+
packages << Package.new(name, remote_source, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Builds all {#packages}
|
48
|
+
# @return [void]
|
49
|
+
def pack
|
50
|
+
packages.each(&:build)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Array<String>] list of paths to each prepared asset package.
|
54
|
+
# It should be appended to the Sprockets' load_path.
|
55
|
+
def load_path
|
56
|
+
packages.map(&:load_path)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Verifies all {#packages}
|
60
|
+
# @return [void]
|
61
|
+
def verify
|
62
|
+
packages.each(&:verify)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
require "torba/css_url_to_erb_asset_path"
|
4
|
+
require "torba/import_list"
|
5
|
+
|
6
|
+
module Torba
|
7
|
+
module Errors
|
8
|
+
UnbuiltPackage = Class.new(StandardError)
|
9
|
+
|
10
|
+
class NothingToImport < StandardError
|
11
|
+
attr_reader :package, :path
|
12
|
+
|
13
|
+
def initialize(options)
|
14
|
+
@package = options.fetch(:package)
|
15
|
+
@path = options.fetch(:path)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Represents remote source with explicit paths/files to be imported (i.e.
|
22
|
+
# copied from an archive, git repository etc).
|
23
|
+
# Stylesheets (if any) are treated specially because static "url(...)"
|
24
|
+
# definitions should be replaced with Sprockets-aware "asset_path" helpers.
|
25
|
+
class Package
|
26
|
+
# @return [String] short package name, acts as as namespace within Sprockets' load path.
|
27
|
+
# Doesn't need to be equal to remote package name.
|
28
|
+
attr_reader :name
|
29
|
+
|
30
|
+
# @return instance that implements {RemoteSources::Common}
|
31
|
+
attr_reader :remote_source
|
32
|
+
|
33
|
+
# @return [Array<String>] list of file paths to import (relative to remote source root).
|
34
|
+
# @example Direct path to a file
|
35
|
+
# ["build/underscore.js"]
|
36
|
+
# @example {http://www.rubydoc.info/stdlib/core/Dir#glob-class_method Dir.glob} pattern
|
37
|
+
# ["build/*.js", "**/*.css"]
|
38
|
+
# @example Any file within directory (including subdirectories)
|
39
|
+
# ["build/"] # same as ["build/**/*"]
|
40
|
+
attr_reader :import_paths
|
41
|
+
|
42
|
+
# @param name [String] see {#name}
|
43
|
+
# @param remote_source [#[]] see {#remote_source}
|
44
|
+
# @param options [Hash]
|
45
|
+
# @option options [Array<String>] :import list assigned to {#import_paths}
|
46
|
+
def initialize(name, remote_source, options = {})
|
47
|
+
@name = name
|
48
|
+
@remote_source = remote_source
|
49
|
+
@import_paths = (options[:import] || ["**/*"]).sort.map do |path|
|
50
|
+
if path.end_with?("/")
|
51
|
+
File.join(path, "**/*")
|
52
|
+
else
|
53
|
+
path
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# @raise [Errors::UnbuiltPackage] if package is not build.
|
59
|
+
def verify
|
60
|
+
raise Errors::UnbuiltPackage.new(name) unless built?
|
61
|
+
end
|
62
|
+
|
63
|
+
# Cache remote source and import specified assets to {#load_path}.
|
64
|
+
# @return [void]
|
65
|
+
# @note Directories explicitly specified in {#import_paths} are not preserved after importing,
|
66
|
+
# i.e. resulted file tree becomes flatten. This way you can omit build specific directories
|
67
|
+
# when requiring assets in your project. If you want to preserve remote source file tree,
|
68
|
+
# use glob patterns without mentioning subdirectories in them.
|
69
|
+
#
|
70
|
+
# In addition {#name} is used as a namespace folder within {#load_path} to protect file names
|
71
|
+
# clashing across packages.
|
72
|
+
#
|
73
|
+
# package.name #=> "datepicker"
|
74
|
+
# package.import_paths #=> ["css/stylesheet.css", "js/*.js"]
|
75
|
+
# Dir[package.load_path + "/**/*"] #=> ["datepicker/stylesheet.css", "datepicker/script.js"]
|
76
|
+
#
|
77
|
+
# package.name #=> "datepicker"
|
78
|
+
# package.import_paths #=> ["**/*.{js,css}"]
|
79
|
+
# Dir[package.load_path + "/**/*"] #=> ["datepicker/css/stylesheet.css", "datepicker/js/script.js"]
|
80
|
+
def build
|
81
|
+
return if built?
|
82
|
+
process_stylesheets
|
83
|
+
process_other_assets
|
84
|
+
rescue
|
85
|
+
remove
|
86
|
+
raise
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [String] path where processed files of the package reside. It's located within
|
90
|
+
# {Torba.home_path} directory.
|
91
|
+
def load_path
|
92
|
+
@load_path ||= File.join(Torba.home_path, folder_name)
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [ImportList]
|
96
|
+
def import_list
|
97
|
+
@import_list ||= build_import_list
|
98
|
+
end
|
99
|
+
|
100
|
+
# Remove self from filesystem.
|
101
|
+
# @return [void]
|
102
|
+
def remove
|
103
|
+
FileUtils.rm_rf(load_path)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def built?
|
109
|
+
Dir.exist?(load_path)
|
110
|
+
end
|
111
|
+
|
112
|
+
def folder_name
|
113
|
+
digest = Torba.digest(import_paths.join << remote_source.digest)
|
114
|
+
"#{name}-#{digest}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def build_import_list
|
118
|
+
assets = import_paths.flat_map do |import_path|
|
119
|
+
path_wo_glob_metacharacters = import_path.sub(/\*.+$/, "")
|
120
|
+
|
121
|
+
assets = remote_source[import_path].map do |absolute_path, relative_path|
|
122
|
+
subpath =
|
123
|
+
if relative_path == import_path
|
124
|
+
File.basename(relative_path)
|
125
|
+
else
|
126
|
+
relative_path.sub(path_wo_glob_metacharacters, "")
|
127
|
+
end
|
128
|
+
|
129
|
+
ImportList::Asset.new(absolute_path, subpath)
|
130
|
+
end
|
131
|
+
|
132
|
+
if assets.empty?
|
133
|
+
raise Errors::NothingToImport.new(package: name, path: import_path)
|
134
|
+
end
|
135
|
+
|
136
|
+
assets
|
137
|
+
end
|
138
|
+
|
139
|
+
ImportList.new(assets)
|
140
|
+
end
|
141
|
+
|
142
|
+
def process_stylesheets
|
143
|
+
import_list.css_assets.each do |asset|
|
144
|
+
content = File.read(asset.absolute_path)
|
145
|
+
|
146
|
+
new_content = CssUrlToErbAssetPath.call(content, asset.absolute_path) do |image_file_path|
|
147
|
+
image_asset = import_list.find_by_absolute_path(image_file_path)
|
148
|
+
with_namespace(image_asset.subpath)
|
149
|
+
end
|
150
|
+
|
151
|
+
new_absolute_path = File.join(load_path, with_namespace(asset.subpath + ".erb"))
|
152
|
+
ensure_directory(new_absolute_path)
|
153
|
+
File.write(new_absolute_path, new_content)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def process_other_assets
|
158
|
+
import_list.non_css_assets.each do |asset|
|
159
|
+
new_absolute_path = File.join(load_path, with_namespace(asset.subpath))
|
160
|
+
ensure_directory(new_absolute_path)
|
161
|
+
FileUtils.cp(asset.absolute_path, new_absolute_path)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def with_namespace(file_name)
|
166
|
+
File.join(name, file_name)
|
167
|
+
end
|
168
|
+
|
169
|
+
def ensure_directory(file)
|
170
|
+
FileUtils.mkdir_p(File.dirname(file))
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|