palimpsest 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ require 'archive/tar/minitar'
2
+
3
+ module Palimpsest
4
+
5
+ # Utility functions for Palimpsest.
6
+ class Utility
7
+
8
+ # Make a random directory.
9
+ # @param [String] root directory to place random directory
10
+ # @param [String] prefix prepended to random directory name
11
+ # @param [String, nil] dir the random directory name (used recursively)
12
+ # @return [String] path to created random directory
13
+ def self.make_random_directory root, prefix, dir = nil
14
+ path = "#{root}/#{prefix}#{dir}" unless dir.nil?
15
+ if path.nil? or File.exists? path
16
+ make_random_directory root, prefix, Random.rand(10000000)
17
+ else
18
+ FileUtils.mkdir(path).first
19
+ end
20
+ end
21
+
22
+ # Forbids use of `../` and `~/` in path.
23
+ # Forbids absolute paths.
24
+ # @param [String] path
25
+ # @return [Boolean]
26
+ def self.safe_path? path
27
+ case
28
+ when path[/(\.\.\/|~\/)/] then return false
29
+ when path[/^\//] then return false
30
+ else return true
31
+ end
32
+ end
33
+
34
+ # Checks that a path is really rooted under a given root directory.
35
+ # Forbids use of `../` and `~/` in path.
36
+ # @param [String] path
37
+ # @param [String] root directory where path should be rooted under
38
+ # @return [String] input path if valid
39
+ def self.validate_path path, root=''
40
+ case
41
+ when path[/(\.\.\/|~\/)/] then raise RuntimeError
42
+ when File.expand_path(path, root)[/^#{root}/].nil? then raise RuntimeError
43
+ else path
44
+ end
45
+ end
46
+
47
+ # Extracts a git repo to a directory.
48
+ # @param [Grit::Repo] repo
49
+ # @param [String] treeish
50
+ # @param [String] directory
51
+ def self.extract_repo repo, treeish, directory, files: nil
52
+ input = Archive::Tar::Minitar::Input.new StringIO.new(repo.archive_tar treeish)
53
+ input.each do |entry|
54
+ if files.nil?
55
+ input.extract_entry directory, entry
56
+ else
57
+ input.extract_entry directory, entry if files.include? entry.name
58
+ end
59
+ end
60
+ end
61
+
62
+ # Write contents to file.
63
+ # @param contents [String]
64
+ # @param file [String]
65
+ def self.write contents, file
66
+ File.open(file, 'w') { |f| f.write contents }
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,4 @@
1
+ module Palimpsest
2
+ # Palimpsest version.
3
+ VERSION = '0.0.1'
4
+ end
data/lib/palimpsest.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'palimpsest/version'
2
+ require 'palimpsest/assets'
3
+ require 'palimpsest/environment'
4
+ require 'palimpsest/utility'
5
+
6
+ # No web framework, no problem:
7
+ # Palimpsest gives any custom or legacy project # a modern workflow and toolset.
8
+ module Palimpsest
9
+ end
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'palimpsest/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'palimpsest'
8
+ spec.version = Palimpsest::VERSION
9
+ spec.authors = ['Evan Boyd Sosenko']
10
+ spec.email = ['razorx@evansosenko.com']
11
+ spec.description = %q{No web framework, no problem: Palimpsest gives any custom or legacy project a modern workflow and toolset.}
12
+ spec.summary = %q{Built flexible, simple, and customizable. Palimpsest runs on top of any project and acts as a post processor for your code. Features a Sprockets asset pipeline and easy integration with Kit.}
13
+ spec.homepage = 'https://github.com/razor-x/palimpsest'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'activesupport', '~> 4.0.0'
22
+ spec.add_dependency 'archive-tar-minitar', '~> 0.5.2'
23
+ spec.add_dependency 'grit', '~> 2.5.0'
24
+ spec.add_dependency 'sprockets', '~> 2.10.0'
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.3'
27
+ spec.add_development_dependency 'rake', '~> 10.1'
28
+ spec.add_development_dependency 'bump', '~> 0.4'
29
+
30
+ spec.add_development_dependency 'yard', '0.8.7.2'
31
+ spec.add_development_dependency 'redcarpet', '3.0.0'
32
+ spec.add_development_dependency 'github-markup', '0.7.5'
33
+
34
+ spec.add_development_dependency 'rspec', '~> 2.14.1'
35
+ spec.add_development_dependency 'simplecov', '~> 0.7.1'
36
+ spec.add_development_dependency 'coveralls', '~> 0.7.0'
37
+ spec.add_development_dependency 'fuubar', '~> 1.2.1'
38
+ spec.add_development_dependency 'guard-rspec', '~> 3.1.0'
39
+ spec.add_development_dependency 'guard-yard', '~> 2.1.0'
40
+ end
@@ -0,0 +1,360 @@
1
+ require 'spec_helper'
2
+
3
+ describe Palimpsest::Assets do
4
+
5
+ let(:config) do
6
+ YAML.load <<-EOF
7
+ :options:
8
+ :js_compressor: :uglifier
9
+ :not_a_good_setting: :some_value
10
+ :paths:
11
+ - assets/javascripts
12
+ - other/javascripts
13
+ EOF
14
+ end
15
+
16
+ subject(:assets) { Palimpsest::Assets.new }
17
+
18
+ describe ".new" do
19
+
20
+ it "sets default options" do
21
+ expect(assets.options).to eq Palimpsest::Assets::DEFAULT_OPTIONS
22
+ end
23
+
24
+ it "merges default options" do
25
+ assets = Palimpsest::Assets.new options: { src_pre: '{{' }
26
+ expect(assets.options).to eq Palimpsest::Assets::DEFAULT_OPTIONS.merge(src_pre: '{{')
27
+ end
28
+ end
29
+
30
+ describe "#options" do
31
+
32
+ it "merges with default options" do
33
+ assets.options[:src_pre] = '{{'
34
+ expect(assets.options).to eq Palimpsest::Assets::DEFAULT_OPTIONS.merge(src_pre: '{{')
35
+ end
36
+
37
+ it "can be called twice and merge options" do
38
+ assets.options[:src_pre] = '{{'
39
+ assets.options[:src_post] = '}}'
40
+ expect(assets.options).to eq Palimpsest::Assets::DEFAULT_OPTIONS.merge(src_pre: '{{', src_post: '}}')
41
+ end
42
+ end
43
+
44
+ describe "#sprockets" do
45
+
46
+ it "returns a new sprockets environment" do
47
+ expect(assets.sprockets).to be_a Sprockets::Environment
48
+ end
49
+ end
50
+
51
+ describe "#load_options" do
52
+
53
+ subject(:assets) { Palimpsest::Assets.new options: config[:options] }
54
+
55
+ it "returns itself" do
56
+ expect(assets.load_options).to be assets
57
+ end
58
+
59
+ it "sets the options for sprockets" do
60
+ expect(assets.sprockets).to receive(:js_compressor=).with(:uglifier)
61
+ assets.load_options
62
+ end
63
+
64
+ it "does not load an unset setting" do
65
+ expect(assets.sprockets).to_not receive(:css_compressor)
66
+ assets.load_options
67
+ end
68
+
69
+ it "does not load an invalid setting" do
70
+ expect(assets.sprockets).to_not receive(:not_a_good_setting)
71
+ assets.load_options
72
+ end
73
+
74
+ context "no options" do
75
+
76
+ it "does not fail when options not set" do
77
+ expect { assets.load_options }.to_not raise_error
78
+ end
79
+ end
80
+ end
81
+
82
+ describe "#load_paths" do
83
+
84
+ subject(:assets) { Palimpsest::Assets.new paths: config[:paths] }
85
+
86
+ it "returns itself" do
87
+ expect(assets.load_paths).to be assets
88
+ end
89
+
90
+ context "when directory set" do
91
+
92
+ it "loads the paths for the given set into the sprockets environment" do
93
+ assets.directory = '/tmp/root_dir'
94
+ expect(assets.sprockets).to receive(:append_path).with('/tmp/root_dir/assets/javascripts')
95
+ expect(assets.sprockets).to receive(:append_path).with('/tmp/root_dir/other/javascripts')
96
+ assets.load_paths
97
+ end
98
+ end
99
+
100
+ context "when no directory set" do
101
+
102
+ it "loads the paths for the given set into the sprockets environment" do
103
+ expect(assets.sprockets).to receive(:append_path).with('assets/javascripts')
104
+ expect(assets.sprockets).to receive(:append_path).with('other/javascripts')
105
+ assets.load_paths
106
+ end
107
+ end
108
+
109
+ context "when no paths set" do
110
+
111
+ it "should not fail" do
112
+ assets.paths = {}
113
+ expect { assets.load_paths }.to_not raise_error
114
+ end
115
+ end
116
+ end
117
+
118
+ describe "#assets" do
119
+
120
+ subject(:assets) { Palimpsest::Assets.new paths: config[:paths] }
121
+
122
+ it "should load options" do
123
+ expect(assets).to receive :load_options
124
+ assets.assets
125
+ end
126
+
127
+ it "should load paths" do
128
+ expect(assets).to receive :load_paths
129
+ assets.assets
130
+ end
131
+
132
+ it "should not load options and paths twice" do
133
+ expect(assets).to receive(:load_options).once
134
+ expect(assets).to receive(:load_paths).once
135
+ assets.assets
136
+ assets.assets
137
+ end
138
+
139
+ it "should return compiled assets" do
140
+ expect(assets.assets).to equal assets.sprockets
141
+ end
142
+ end
143
+
144
+ describe "#write" do
145
+
146
+ let(:asset) { double Sprockets::Asset }
147
+ let(:name) { 'lib/app.js' }
148
+
149
+ before :each do
150
+ assets.options hash: false
151
+ allow(assets.assets).to receive(:[]).with('lib/app').and_return(asset)
152
+ allow(asset).to receive(:logical_path).and_return(name)
153
+ end
154
+
155
+ context "asset not found" do
156
+ it "returns nil" do
157
+ allow(assets.assets).to receive(:[]).with('not_here').and_return(nil)
158
+ expect(assets.write 'not_here').to be nil
159
+ end
160
+ end
161
+
162
+ context "output is set with no directory set" do
163
+
164
+ it "writes to relative path and returns the relative path" do
165
+ assets.options output: 'compiled'
166
+ expect(asset).to receive(:write_to).with("compiled/#{name}")
167
+ expect(assets.write 'lib/app').to eq "compiled/#{name}"
168
+ end
169
+ end
170
+
171
+ context "output is set with directory set" do
172
+
173
+ it "writes to relative path under directory and returns the relative path" do
174
+ assets.options output: 'compiled'
175
+ assets.directory = '/tmp/dir'
176
+ expect(asset).to receive(:write_to).with("/tmp/dir/compiled/#{name}")
177
+ expect(assets.write 'lib/app').to eq "compiled/#{name}"
178
+ end
179
+ end
180
+
181
+ context "no output is set with directory set and returns the relative path" do
182
+
183
+ it "writes to relative path under directory and returns the relative path" do
184
+ assets.directory = '/tmp/dir'
185
+ expect(asset).to receive(:write_to).with("/tmp/dir/#{name}")
186
+ expect(assets.write 'lib/app').to eq name
187
+ end
188
+ end
189
+
190
+ context "no output is set with no directory set" do
191
+
192
+ it "writes to relative path and returns the relative path" do
193
+ expect(asset).to receive(:write_to).with(name)
194
+ expect(assets.write 'lib/app').to eq name
195
+ end
196
+ end
197
+
198
+ context "when gzip true" do
199
+
200
+ it "still returns the non-gzipped asset name" do
201
+ allow(asset).to receive(:write_to)
202
+ expect(assets.write 'lib/app', gzip: true).to eq name
203
+ end
204
+
205
+ it "it gzips the assets as well" do
206
+ expect(asset).to receive(:write_to).at_most(:once).with("#{name}.gz", compress: true)
207
+ expect(asset).to receive(:write_to).at_most(:once).with(name)
208
+ assets.write 'lib/app', gzip: true
209
+ end
210
+ end
211
+
212
+ context "when hash true" do
213
+
214
+ it "hashes the file name" do
215
+ allow(asset).to receive(:digest_path).and_return('app-cb5a921a4e7663347223c41cd2fa9e11.js')
216
+ expect(asset).to receive(:write_to).with('app-cb5a921a4e7663347223c41cd2fa9e11.js')
217
+ assets.write 'lib/app', hash: true
218
+ end
219
+ end
220
+ end
221
+
222
+ describe ".find_tags and #find_tags" do
223
+
224
+ let(:grep) { [ 'grep', '-l', '-I', '-r', '-E' ] }
225
+ let(:matches) { [ "first/match_1\nsecond/match_2" ] }
226
+
227
+ describe ".find_tags" do
228
+
229
+ let(:regex) { '\[%(.*?)%\]' }
230
+
231
+ it "fails if path is empty" do
232
+ expect { assets.find_tags '' }.to raise_error ArgumentError
233
+ end
234
+
235
+ it "greps in the path" do
236
+ expect(Open3).to receive(:capture2).with(*grep, regex, '/the/path').and_return(matches)
237
+ Palimpsest::Assets.find_tags '/the/path'
238
+ end
239
+
240
+ it "greps for only the asset tag for the given type" do
241
+ expect(Open3).to receive(:capture2).with(*grep, '\[%\s+javascript\s+(.*?)%\]', '/the/path').and_return(matches)
242
+ Palimpsest::Assets.find_tags '/the/path', :javascript
243
+ end
244
+
245
+ it "merges options" do
246
+ expect(Open3).to receive(:capture2).with(*grep, '\(%\s+javascript\s+(.*?)%\]', '/the/path').and_return(matches)
247
+ Palimpsest::Assets.find_tags '/the/path', :javascript, src_pre: '(%'
248
+ end
249
+
250
+ it "returns an array of results" do
251
+ allow(Open3).to receive(:capture2).with(*grep, regex, '/the/path').and_return(matches)
252
+ expect(Palimpsest::Assets.find_tags '/the/path').to eq [ 'first/match_1', 'second/match_2' ]
253
+ end
254
+ end
255
+
256
+ describe "#find_tags" do
257
+
258
+ it "uses the type as the type" do
259
+ assets.type = :javascript
260
+ expect(Palimpsest::Assets).to receive(:find_tags).with(anything, :javascript, anything)
261
+ assets.find_tags path: '/the/path'
262
+ end
263
+
264
+ it "uses the options as the options" do
265
+ expect(Palimpsest::Assets).to receive(:find_tags).with(anything, anything, assets.options)
266
+ assets.find_tags path: '/the/path'
267
+ end
268
+
269
+ it "uses the directory as the path" do
270
+ assets.directory = '/the/directory'
271
+ expect(Palimpsest::Assets).to receive(:find_tags).with('/the/directory', anything, anything)
272
+ assets.find_tags
273
+ end
274
+
275
+ it "can use an alternative path" do
276
+ assets.directory = '/the/directory'
277
+ expect(Palimpsest::Assets).to receive(:find_tags).with('/the/path', anything, anything)
278
+ assets.find_tags path: '/the/path'
279
+ end
280
+ end
281
+ end
282
+
283
+ describe "#update_source and #update_source!" do
284
+
285
+ let(:asset) { double Sprockets::Asset }
286
+
287
+ let(:source) do
288
+ <<-EOF
289
+ <head>
290
+ <script src="[% javascript app %]"></script>
291
+ <script src="[% javascript vendor/modernizr %]"></script>
292
+ <script>
293
+ [% javascript inline vendor/tracking %]
294
+ </script>
295
+ </head>
296
+ EOF
297
+ end
298
+
299
+ let(:result) do
300
+ <<-EOF
301
+ <head>
302
+ <script src="app-1234.js"></script>
303
+ <script src="vendor/modernizr-5678.js"></script>
304
+ <script>
305
+ alert('track');
306
+ </script>
307
+ </head>
308
+ EOF
309
+ end
310
+
311
+ let(:result_with_cdn) do
312
+ <<-EOF
313
+ <head>
314
+ <script src="https://cdn.example.com/app-1234.js"></script>
315
+ <script src="https://cdn.example.com/vendor/modernizr-5678.js"></script>
316
+ <script>
317
+ alert('track');
318
+ </script>
319
+ </head>
320
+ EOF
321
+ end
322
+
323
+ before :each do
324
+ assets.type = :javascripts
325
+ allow(assets).to receive(:write).with('app').and_return('app-1234.js')
326
+ allow(assets).to receive(:write).with('vendor/modernizr').and_return('vendor/modernizr-5678.js')
327
+ allow(assets.assets).to receive(:[]).with('vendor/tracking').and_return(asset)
328
+ allow(asset).to receive(:to_s).and_return(%q{alert('track');})
329
+ end
330
+
331
+ describe "#update_source!" do
332
+
333
+ it "replaces asset tags in sources" do
334
+ assets.update_source! source
335
+ expect(source).to eq result
336
+ end
337
+
338
+ context "cdn option set" do
339
+
340
+ it "uses cdn when it replaces asset tags in sources" do
341
+ assets.options cdn: 'https://cdn.example.com/'
342
+ assets.update_source! source
343
+ expect(source).to eq result_with_cdn
344
+ end
345
+ end
346
+ end
347
+
348
+ describe "#update_source" do
349
+
350
+ it "replaces asset tags in sources" do
351
+ expect(assets.update_source source).to eq result
352
+ end
353
+
354
+ it "does not change input string" do
355
+ assets.update_source source
356
+ expect(source).to eq source
357
+ end
358
+ end
359
+ end
360
+ end