condenser 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +1 -0
  4. data/lib/condenser.rb +108 -0
  5. data/lib/condenser/asset.rb +221 -0
  6. data/lib/condenser/cache/memory_store.rb +92 -0
  7. data/lib/condenser/cache/null_store.rb +37 -0
  8. data/lib/condenser/context.rb +272 -0
  9. data/lib/condenser/encoding_utils.rb +155 -0
  10. data/lib/condenser/environment.rb +50 -0
  11. data/lib/condenser/errors.rb +11 -0
  12. data/lib/condenser/export.rb +68 -0
  13. data/lib/condenser/manifest.rb +89 -0
  14. data/lib/condenser/pipeline.rb +82 -0
  15. data/lib/condenser/processors/babel.min.js +25 -0
  16. data/lib/condenser/processors/babel_processor.rb +87 -0
  17. data/lib/condenser/processors/node_processor.rb +38 -0
  18. data/lib/condenser/processors/rollup.js +24083 -0
  19. data/lib/condenser/processors/rollup_processor.rb +164 -0
  20. data/lib/condenser/processors/sass_importer.rb +81 -0
  21. data/lib/condenser/processors/sass_processor.rb +300 -0
  22. data/lib/condenser/resolve.rb +202 -0
  23. data/lib/condenser/server.rb +307 -0
  24. data/lib/condenser/templating_engine/erb.rb +21 -0
  25. data/lib/condenser/utils.rb +32 -0
  26. data/lib/condenser/version.rb +3 -0
  27. data/lib/condenser/writers/file_writer.rb +28 -0
  28. data/lib/condenser/writers/zlib_writer.rb +42 -0
  29. data/test/cache_test.rb +24 -0
  30. data/test/environment_test.rb +49 -0
  31. data/test/manifest_test.rb +513 -0
  32. data/test/pipeline_test.rb +31 -0
  33. data/test/preprocessor/babel_test.rb +21 -0
  34. data/test/processors/rollup_test.rb +71 -0
  35. data/test/resolve_test.rb +105 -0
  36. data/test/server_test.rb +361 -0
  37. data/test/templates/erb_test.rb +18 -0
  38. data/test/test_helper.rb +68 -0
  39. data/test/transformers/scss_test.rb +49 -0
  40. metadata +193 -0
@@ -0,0 +1,31 @@
1
+ require 'test_helper'
2
+
3
+ class PipelineTest < ActiveSupport::TestCase
4
+
5
+ def setup
6
+ super
7
+ @env.clear_pipeline
8
+ end
9
+
10
+ test 'clear_pipeline' do
11
+ @env.register_mime_type 'application/javascript', extension: 'js', charset: :unicode
12
+ @env.register_template 'application/erb', PipelineTest
13
+ @env.register_preprocessor 'application/javascript', PipelineTest
14
+ @env.register_transformer 'application/scss', 'application/css', PipelineTest
15
+ @env.register_postprocessor 'application/javascript', PipelineTest
16
+ @env.register_minifier 'application/javascript', PipelineTest
17
+ @env.register_writer '*/*', PipelineTest
18
+
19
+ vars = %w(templates preprocessors transformers postprocessors minifiers writers)
20
+ vars.each do |var|
21
+ assert_not_empty @env.send(var)
22
+ end
23
+
24
+ @env.clear_pipeline
25
+
26
+ vars.each do |var|
27
+ assert_empty @env.send(var)
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,21 @@
1
+ require 'test_helper'
2
+
3
+ class CondenserBabelTest < ActiveSupport::TestCase
4
+
5
+ test 'find' do
6
+ file 'name.js', <<~JS
7
+ var t = { 'var': () => { return 2; } };
8
+
9
+ export {t as name1};
10
+ JS
11
+
12
+ assert_file 'name.js', 'application/javascript', <<~JS
13
+ var t = { 'var': function _var() {
14
+ return 2;
15
+ } };
16
+
17
+ export { t as name1 };
18
+ JS
19
+ end
20
+
21
+ end
@@ -0,0 +1,71 @@
1
+ require 'test_helper'
2
+
3
+ class RollupTest < ActiveSupport::TestCase
4
+
5
+ def setup
6
+ super
7
+ @env.unregister_preprocessor('application/javascript', Condenser::BabelProcessor)
8
+ end
9
+
10
+ test 'import file' do
11
+ file 'main.js', <<~JS
12
+ import { cube } from './math.js';
13
+
14
+ console.log( cube( 5 ) ); // 125
15
+ JS
16
+ file 'math.js', <<~JS
17
+
18
+ // This function isn't used anywhere, so
19
+ // Rollup excludes it from the bundle...
20
+ export function square ( x ) {
21
+ return x * x;
22
+ }
23
+
24
+ // This function gets included
25
+ export function cube ( x ) {
26
+ return x * x * x;
27
+ }
28
+ JS
29
+
30
+ assert_exported_file 'main.js', 'application/javascript', <<~FILE
31
+ (function () {
32
+ 'use strict';
33
+
34
+ // This function gets included
35
+ function cube ( x ) {
36
+ return x * x * x;
37
+ }
38
+
39
+ console.log( cube( 5 ) ); // 125
40
+
41
+ }());
42
+ FILE
43
+ end
44
+
45
+ test 'import an erb file' do
46
+ file 'main.js', <<~JS
47
+ import { cube } from './math.js';
48
+
49
+ console.log( cube( 5 ) ); // 125
50
+ JS
51
+ file 'math.js.erb', <<~JS
52
+ export function cube ( x ) {
53
+ return <%= 2 %> * x * x;
54
+ }
55
+ JS
56
+
57
+ assert_exported_file 'main.js', 'application/javascript', <<~FILE
58
+ (function () {
59
+ 'use strict';
60
+
61
+ function cube ( x ) {
62
+ return 2 * x * x;
63
+ }
64
+
65
+ console.log( cube( 5 ) ); // 125
66
+
67
+ }());
68
+ FILE
69
+ end
70
+
71
+ end
@@ -0,0 +1,105 @@
1
+ require 'test_helper'
2
+
3
+ class ResolveTest < ActiveSupport::TestCase
4
+
5
+ def setup
6
+ super
7
+ @env.clear_pipeline
8
+ @env.register_template 'application/erb', Condenser::Erubi
9
+ @env.register_transformer 'text/scss', 'text/css', Condenser::Erubi
10
+ @env.register_exporter 'application/javascript', Condenser::RollupProcessor
11
+ @env.register_writer 'application/javascript', Condenser::Erubi, 'application/gzip'
12
+ end
13
+
14
+ test 'decompose_path' do
15
+ assert_equal [nil, 'test', ['.text'], ['text/plain']], @env.decompose_path('test.text')
16
+
17
+ assert_equal ['dir', 'test', ['.text'], ['text/plain']], @env.decompose_path('dir/test.text')
18
+
19
+ assert_equal ['dir', 'test.unkownmime', ['.text'], ['text/plain']], @env.decompose_path('dir/test.unkownmime.text')
20
+
21
+ assert_equal [nil, '*', nil, []], @env.decompose_path('*')
22
+ assert_equal ['*', '*', nil, []], @env.decompose_path('*/*')
23
+ assert_equal ['**', '*', nil, []], @env.decompose_path('**/*')
24
+
25
+ assert_equal ['test', '*', nil, []], @env.decompose_path('test/*')
26
+ assert_equal ['test/*', '*', nil, []], @env.decompose_path('test/*/*')
27
+ assert_equal ['test/**', '*', nil, []], @env.decompose_path('test/**/*')
28
+
29
+ assert_equal [nil, '*', ['.js'], ['application/javascript']], @env.decompose_path('*.js')
30
+ assert_equal ['*', '*', ['.js'], ['application/javascript']], @env.decompose_path('*/*.js')
31
+ assert_equal ['**', '*', ['.js'], ['application/javascript']], @env.decompose_path('**/*.js')
32
+
33
+ assert_equal ['test', '*', ['.js'], ['application/javascript']], @env.decompose_path('test/*.js')
34
+ assert_equal ['test/*', '*', ['.js'], ['application/javascript']], @env.decompose_path('test/*/*.js')
35
+ assert_equal ['test/**', '*', ['.js'], ['application/javascript']], @env.decompose_path('test/**/*.js')
36
+ end
37
+
38
+ test 'resolve' do
39
+ file 'file.js', 'test'
40
+ file 'test/file.scss', 'test'
41
+ file 'test/z/file.js', 'test'
42
+
43
+ assert_file('file.js', 'application/javascript')
44
+ assert_file('test/file.css', 'text/css')
45
+
46
+ assert_equal %w(file.js test/file.css test/z/file.js), @env.resolve('**/*').map(&:filename)
47
+ assert_equal %w(file.js test/file.css test/z/file.js), @env.resolve('**/*', accept: ['text/css', 'application/javascript']).map(&:filename)
48
+ assert_equal %w(file.js), @env.resolve('*', accept: ['application/javascript']).map(&:filename)
49
+ assert_equal %w(file.js test/z/file.js), @env.resolve('**/*', accept: ['application/javascript']).map(&:filename)
50
+ assert_equal %w(file.js), @env.resolve('*.js').map(&:filename)
51
+ assert_equal %w(file.js test/z/file.js), @env.resolve('**/*.js').map(&:filename)
52
+
53
+
54
+ assert_equal %w(test/file.css), @env.resolve('test/*').map(&:filename)
55
+ assert_equal %w(test/file.css), @env.resolve('test/*', accept: ['text/css', 'application/javascript']).map(&:filename)
56
+ assert_equal %w(test/file.css test/z/file.js), @env.resolve('test/**/*').map(&:filename)
57
+ assert_equal %w(test/file.css test/z/file.js), @env.resolve('test/**/*', accept: ['text/css', 'application/javascript']).map(&:filename)
58
+ assert_equal %w(test/z/file.js), @env.resolve('test/z/*', accept: ['application/javascript']).map(&:filename)
59
+ assert_equal %w(test/z/file.js), @env.resolve('test/**/*', accept: ['application/javascript']).map(&:filename)
60
+ assert_equal %w(test/z/file.js), @env.resolve('test/z/*.js').map(&:filename)
61
+ assert_equal %w(test/z/file.js), @env.resolve('test/**/*.js').map(&:filename)
62
+ end
63
+
64
+ test 'relative require' do
65
+ file 'a/main.js', <<~JS
66
+ import { cube } from './math.js';
67
+ console.log( cube( 5 ) ); // 125
68
+ JS
69
+ file 'a/math.js', <<~JS
70
+ export function cube ( x ) {
71
+ return x * x * x;
72
+ }
73
+ JS
74
+
75
+ file 'b/math.js', <<~JS
76
+ export function square ( x ) {
77
+ return x * x;
78
+ }
79
+ JS
80
+
81
+ @env.append_path File.join(@path, 'b')
82
+ @env.append_path File.join(@path, 'a')
83
+
84
+
85
+ assert_exported_file('main.js', 'application/javascript', <<~JS)
86
+ (function () {
87
+ 'use strict';
88
+
89
+ function cube ( x ) {
90
+ return x * x * x;
91
+ }
92
+
93
+ console.log( cube( 5 ) ); // 125
94
+
95
+ }());
96
+ JS
97
+ end
98
+
99
+ test 'resolve! raises NotFound' do
100
+ assert_raises Condenser::FileNotFound do
101
+ @env.resolve!('**/*')
102
+ end
103
+ end
104
+
105
+ end
@@ -0,0 +1,361 @@
1
+ require 'test_helper'
2
+ require 'rack/builder'
3
+ require 'rack/test'
4
+
5
+ class ServerTest < ActiveSupport::TestCase
6
+ include Rack::Test::Methods
7
+
8
+ def setup
9
+ super
10
+ server = Condenser::Server.new(@env)
11
+ @app = nil
12
+ @condenser_server = Rack::Builder.new do
13
+ map "/assets" do
14
+ run server
15
+ end
16
+ end
17
+
18
+ file 'foo.js', <<~JS
19
+ console.log(1);
20
+ JS
21
+ end
22
+
23
+ def app
24
+ @app ||= Rack::Lint.new(@condenser_server)
25
+ end
26
+
27
+ test "serve single source file" do
28
+ get "/assets/foo.js"
29
+ assert_equal 200, last_response.status
30
+ assert_equal "53", last_response.headers['Content-Length']
31
+ assert_equal "Accept-Encoding", last_response.headers['Vary']
32
+ assert_equal <<~JS, last_response.body
33
+ (function () {
34
+ 'use strict';
35
+
36
+ console.log(1);
37
+
38
+ }());
39
+ JS
40
+ end
41
+
42
+ # TODO:
43
+ # test "serve single source file from cached environment" do
44
+ # get "/cached/javascripts/foo.js"
45
+ # assert_equal "var foo;\n", last_response.body
46
+ # end
47
+
48
+ test "serve source with dependencies" do
49
+ file 'main.js', <<~JS
50
+ import { cube } from './math.js';
51
+
52
+ console.log( cube( 5 ) ); // 125
53
+ JS
54
+ file 'math.js', <<~JS
55
+ export function cube ( x ) {
56
+ return x * x * x;
57
+ }
58
+ JS
59
+
60
+ get "/assets/main.js"
61
+ assert_equal <<~JS, last_response.body
62
+ (function () {
63
+ 'use strict';
64
+
65
+ function cube(x) {
66
+ return x * x * x;
67
+ }
68
+
69
+ console.log(cube(5)); // 125
70
+
71
+ }());
72
+ JS
73
+ end
74
+
75
+ test "serve source with content type headers" do
76
+ file 'main.css', <<~CSS
77
+ body { background: green ; }
78
+ CSS
79
+
80
+ get "/assets/foo.js"
81
+ assert_equal "application/javascript", last_response.headers['Content-Type']
82
+
83
+ get "/assets/main.css"
84
+ assert_equal "text/css; charset=utf-8", last_response.headers['Content-Type']
85
+ end
86
+
87
+ test "serve source with etag headers" do
88
+ get "/assets/foo.js"
89
+
90
+ digest = '85b4185243c9cf4935e686f910a410762c2632a044118ac7ef030094be635c18'
91
+ assert_equal "\"#{digest}\"", last_response.headers['ETag']
92
+ end
93
+
94
+ test "not modified partial response when if-none-match etags match" do
95
+ get "/assets/foo.js"
96
+ assert_equal 200, last_response.status
97
+ etag, cache_control, expires, vary = last_response.headers.values_at(
98
+ 'ETag', 'Cache-Control', 'Expires', 'Vary'
99
+ )
100
+
101
+ assert_nil expires
102
+ get "/assets/foo.js", {},
103
+ 'HTTP_IF_NONE_MATCH' => etag
104
+
105
+ assert_equal 304, last_response.status
106
+
107
+ # Allow 304 headers
108
+ assert_equal cache_control, last_response.headers['Cache-Control']
109
+ assert_equal etag, last_response.headers['ETag']
110
+ assert_nil last_response.headers['Expires']
111
+ assert_equal vary, last_response.headers['Vary']
112
+
113
+ # Disallowed 304 headers
114
+ refute last_response.headers['Content-Type']
115
+ refute last_response.headers['Content-Length']
116
+ refute last_response.headers['Content-Encoding']
117
+ end
118
+
119
+ test "response when if-none-match etags don't match" do
120
+ get "/assets/foo.js", {},
121
+ 'HTTP_IF_NONE_MATCH' => "nope"
122
+
123
+ assert_equal 200, last_response.status
124
+ assert_equal '"85b4185243c9cf4935e686f910a410762c2632a044118ac7ef030094be635c18"', last_response.headers['ETag']
125
+ assert_equal '53', last_response.headers['Content-Length']
126
+ end
127
+
128
+ test "not modified partial response with fingerprint and if-none-match etags match" do
129
+ get "/assets/foo.js"
130
+ assert_equal 200, last_response.status
131
+
132
+ etag = last_response.headers['ETag']
133
+ digest = etag[/"(.+)"/, 1]
134
+
135
+ get "/assets/foo-#{digest}.js", {},
136
+ 'HTTP_IF_NONE_MATCH' => etag
137
+ assert_equal 304, last_response.status
138
+ end
139
+
140
+ test "ok response with fingerprint and if-nonematch etags don't match" do
141
+ get "/assets/foo.js"
142
+ assert_equal 200, last_response.status
143
+
144
+ etag = last_response.headers['ETag']
145
+ digest = etag[/"(.+)"/, 1]
146
+
147
+ get "/assets/foo-#{digest}.js", {},
148
+ 'HTTP_IF_NONE_MATCH' => "nope"
149
+ assert_equal 200, last_response.status
150
+ end
151
+
152
+ test "not found with if-none-match" do
153
+ get "/assets/missing.js", {},
154
+ 'HTTP_IF_NONE_MATCH' => '"000"'
155
+ assert_equal 404, last_response.status
156
+ end
157
+
158
+ test "not found fingerprint with if-none-match" do
159
+ get "/assets/missing-b452c9ae1d5c8d9246653e0d93bc83abce0ee09ef725c0f0a29a41269c217b83.js", {},
160
+ 'HTTP_IF_NONE_MATCH' => '"b452c9ae1d5c8d9246653e0d93bc83abce0ee09ef725c0f0a29a41269c217b83"'
161
+ assert_equal 404, last_response.status
162
+ end
163
+
164
+ test "not found with response with incorrect fingerprint and matching if-none-match etags" do
165
+ get "/assets/foo.js"
166
+ assert_equal 200, last_response.status
167
+
168
+ etag = last_response.headers['ETag']
169
+
170
+ get "/assets/foo-0000000000000000000000000000000000000000.js", {},
171
+ 'HTTP_IF_NONE_MATCH' => etag
172
+ assert_equal 404, last_response.status
173
+ end
174
+
175
+ test "ok partial response when if-match etags match" do
176
+ get "/assets/foo.js"
177
+ assert_equal 200, last_response.status
178
+ etag = last_response.headers['ETag']
179
+
180
+ get "/assets/foo.js", {},
181
+ 'HTTP_IF_MATCH' => etag
182
+
183
+ assert_equal 200, last_response.status
184
+ assert_equal '"85b4185243c9cf4935e686f910a410762c2632a044118ac7ef030094be635c18"', last_response.headers['ETag']
185
+ assert_equal '53', last_response.headers['Content-Length']
186
+ end
187
+
188
+ test "precondition failed with if-match is a mismatch" do
189
+ get "/assets/foo.js", {},
190
+ 'HTTP_IF_MATCH' => '"000"'
191
+ assert_equal 412, last_response.status
192
+
193
+ refute last_response.headers['ETag']
194
+ end
195
+
196
+ test "not found with if-match" do
197
+ get "/assets/missing.js", {},
198
+ 'HTTP_IF_MATCH' => '"000"'
199
+ assert_equal 404, last_response.status
200
+ end
201
+
202
+ # TODO:
203
+ # test "if sources didnt change the server shouldnt rebundle" do
204
+ # get "/assets/application.js"
205
+ # asset_before = @env["application.js"]
206
+ # assert asset_before
207
+ #
208
+ # get "/assets/application.js"
209
+ # asset_after = @env["application.js"]
210
+ # assert asset_after
211
+ #
212
+ # assert asset_before.eql?(asset_after)
213
+ # end
214
+ #
215
+ test "fingerprint digest sets expiration to the future" do
216
+ get "/assets/foo.js"
217
+ digest = last_response.headers['ETag'][/"(.+)"/, 1]
218
+
219
+ get "/assets/foo-#{digest}.js"
220
+ assert_equal 200, last_response.status
221
+ assert_match %r{max-age}, last_response.headers['Cache-Control']
222
+ assert_match %r{immutable}, last_response.headers['Cache-Control']
223
+ end
224
+
225
+ test "bad fingerprint digest returns a 404" do
226
+ get "/assets/foo-0000000000000000000000000000000000000000.js"
227
+ assert_equal 404, last_response.status
228
+
229
+ head "/assets/foo-0000000000000000000000000000000000000000.js"
230
+ assert_equal 404, last_response.status
231
+ assert_equal "0", last_response.headers['Content-Length']
232
+ assert_equal "", last_response.body
233
+ end
234
+
235
+ test "missing source" do
236
+ get "/assets/none.js"
237
+ assert_equal 404, last_response.status
238
+ assert_equal "pass", last_response.headers['X-Cascade']
239
+ end
240
+
241
+ test "re-throw JS exceptions in the browser" do
242
+ file 'error.js', "var error = {;"
243
+
244
+ get "/assets/error.js"
245
+ assert_equal 200, last_response.status
246
+ assert_match(/SyntaxError: error\.js: Unexpected token/, last_response.body)
247
+ end
248
+
249
+ test "display CSS exceptions in the browser" do
250
+ file 'error.scss', "* { color: $test; }"
251
+
252
+ get "/assets/error.css"
253
+ assert_equal 200, last_response.status
254
+ assert_match %r{content: ".*?Sass::SyntaxError}, last_response.body
255
+ end
256
+
257
+ test "serve encoded utf-8 filename" do
258
+ file '日本語.js', <<~JS
259
+ var japanese = "日本語";
260
+
261
+ console.log(japanese);
262
+ JS
263
+ get "/assets/%E6%97%A5%E6%9C%AC%E8%AA%9E.js"
264
+ assert_equal <<~JS, last_response.body
265
+ (function () {
266
+ 'use strict';
267
+
268
+ var japanese = "日本語";
269
+
270
+ console.log(japanese);
271
+
272
+ }());
273
+ JS
274
+ end
275
+
276
+ test "illegal require outside load path" do
277
+ get "/assets//etc/passwd"
278
+ assert_equal 403, last_response.status
279
+
280
+ get "/assets/%2fetc/passwd"
281
+ assert_equal 403, last_response.status
282
+
283
+ get "/assets//%2fetc/passwd"
284
+ assert_equal 403, last_response.status
285
+
286
+ get "/assets/%2f/etc/passwd"
287
+ assert_equal 403, last_response.status
288
+
289
+ get "/assets/../etc/passwd"
290
+ assert_equal 403, last_response.status
291
+
292
+ get "/assets/%2e%2e/etc/passwd"
293
+ assert_equal 403, last_response.status
294
+
295
+ get "/assets/.-0000000./etc/passwd"
296
+ assert_equal 403, last_response.status
297
+
298
+ head "/assets/.-0000000./etc/passwd"
299
+ assert_equal 403, last_response.status
300
+ assert_equal "0", last_response.headers['Content-Length']
301
+ assert_equal "", last_response.body
302
+ end
303
+
304
+ # TODO:
305
+ # test "add new source to tree" do
306
+ # filename = fixture_path("server/app/javascripts/baz.js")
307
+ #
308
+ # sandbox filename do
309
+ # get "/assets/tree.js"
310
+ # assert_equal "var foo;\n\n(function () {\n application.boot();\n})();\nvar bar;\nvar japanese = \"日本語\";\n", last_response.body
311
+ #
312
+ # File.open(filename, "w") do |f|
313
+ # f.write "var baz;\n"
314
+ # end
315
+ #
316
+ # path = fixture_path "server/app/javascripts"
317
+ # mtime = Time.now + 60
318
+ # File.utime(mtime, mtime, path)
319
+ #
320
+ # get "/assets/tree.js"
321
+ # assert_equal "var foo;\n\n(function () {\n application.boot();\n})();\nvar bar;\nvar baz;\nvar japanese = \"日本語\";\n", last_response.body
322
+ # end
323
+ # end
324
+
325
+ test "serving static assets" do
326
+ bytes = Random.new.bytes(128)
327
+ file 'logo.png', bytes
328
+
329
+ get "/assets/logo.png"
330
+ assert_equal 200, last_response.status
331
+ assert_equal "image/png", last_response.headers['Content-Type']
332
+ refute last_response.headers['Content-Encoding']
333
+ assert_equal bytes, last_response.body
334
+ end
335
+
336
+ test "disallow non-get methods" do
337
+ get "/assets/foo.js"
338
+ assert_equal 200, last_response.status
339
+
340
+ head "/assets/foo.js"
341
+ assert_equal 200, last_response.status
342
+ assert_equal "application/javascript", last_response.headers['Content-Type']
343
+ assert_equal "0", last_response.headers['Content-Length']
344
+ assert_equal "", last_response.body
345
+
346
+ post "/assets/foo.js"
347
+ assert_equal 405, last_response.status
348
+
349
+ put "/assets/foo.js"
350
+ assert_equal 405, last_response.status
351
+
352
+ delete "/assets/foo.js"
353
+ assert_equal 405, last_response.status
354
+ end
355
+
356
+ test "invalid URLs" do
357
+ get "/assets/%E2%EF%BF%BD%A6.js"
358
+ assert_equal 400, last_response.status
359
+ end
360
+
361
+ end