palimpsest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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