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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ea18e4983464493a490f76c2c31839d29e69de171033575b6db24649177189ac
4
+ data.tar.gz: e938a1ee91802fb1b197bb34be963d4b4d1f88663f60ee5494c5b9bd4ccdd15c
5
+ SHA512:
6
+ metadata.gz: b01a8d1ed588a216f9f802a0ef0065782092957c4ff8030321a62aa1229ad3a478eb6e08b11b4d747c3e85d9c9ce0d6320ab60fb7b8e6d1ed8ba9186cfcc4c6c
7
+ data.tar.gz: b7ddee06c969a9601f374e58fea6b7623d779f27241d75d685e88779349e252e28e72310a7ffc2411d28e090c815cc48773bec740fc39c96658e5779fe18f2f0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Jon Bracy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1 @@
1
+ # condenser
data/lib/condenser.rb ADDED
@@ -0,0 +1,108 @@
1
+ require 'logger'
2
+ require 'active_support'
3
+ require 'active_support/core_ext'
4
+
5
+ require 'condenser/version'
6
+ require 'condenser/errors'
7
+ require 'condenser/environment'
8
+ require 'condenser/pipeline'
9
+ require 'condenser/resolve'
10
+ require 'condenser/encoding_utils'
11
+ require 'condenser/asset'
12
+ require 'condenser/manifest'
13
+
14
+
15
+ class Condenser
16
+ include Environment, Pipeline, Resolve
17
+
18
+ def self.configure(&block)
19
+ instance_eval(&block)
20
+ end
21
+
22
+ attr_reader :logger
23
+ attr_accessor :digestor
24
+
25
+ def initialize(root='.', digestor: nil)
26
+ @logger = Logger.new($stderr, level: :warn)
27
+ self.digestor = digestor || Digest::SHA256
28
+ super(root)
29
+ end
30
+ end
31
+
32
+ Condenser.configure do
33
+ # Common asset text types
34
+ register_mime_type 'application/json', extension: '.json', charset: :unicode
35
+ register_mime_type 'application/ruby', extension: '.rb'
36
+ register_mime_type 'application/xml', extension: '.xml'
37
+ register_mime_type 'text/html', extensions: %w(.html .htm), charset: :html
38
+ register_mime_type 'text/plain', extensions: %w(.txt .text)
39
+ register_mime_type 'text/yaml', extensions: %w(.yml .yaml), charset: :unicode
40
+
41
+ # Common image types
42
+ register_mime_type 'image/x-icon', extension: '.ico'
43
+ register_mime_type 'image/bmp', extension: '.bmp'
44
+ register_mime_type 'image/gif', extension: '.gif'
45
+ register_mime_type 'image/webp', extension: '.webp'
46
+ register_mime_type 'image/png', extension: '.png'
47
+ register_mime_type 'image/jpeg', extensions: %w(.jpg .jpeg)
48
+ register_mime_type 'image/tiff', extensions: %w(.tiff .tif)
49
+ register_mime_type 'image/svg+xml', extension: '.svg'
50
+
51
+ # Common audio/video types
52
+ register_mime_type 'video/webm', extensions: ['.webm']
53
+ register_mime_type 'audio/basic', extensions: ['.snd', '.au']
54
+ register_mime_type 'audio/aiff', extensions: ['.aiff']
55
+ register_mime_type 'audio/mpeg', extensions: ['.mp3', '.mp2', '.m2a', '.m3a']
56
+ register_mime_type 'application/ogg', extensions: ['.ogx']
57
+ register_mime_type 'audio/ogg', extensions: ['.ogg', '.oga']
58
+ register_mime_type 'audio/midi', extensions: ['.midi', '.mid']
59
+ register_mime_type 'video/avi', extensions: ['.avi']
60
+ register_mime_type 'audio/wave', extensions: ['.wav', '.wave']
61
+ register_mime_type 'video/mp4', extensions: ['.mp4', '.m4v']
62
+ register_mime_type 'audio/aac', extensions: ['.aac']
63
+ register_mime_type 'audio/mp4', extensions: ['.m4a']
64
+ register_mime_type 'audio/flac', extensions: ['.flac']
65
+
66
+ # Common font types
67
+ register_mime_type 'application/vnd.ms-fontobject', extensions: ['.eot']
68
+ register_mime_type 'application/x-font-opentype', extensions: ['.otf']
69
+ register_mime_type 'application/x-font-ttf', extensions: ['.ttf']
70
+ register_mime_type 'application/font-woff', extensions: ['.woff']
71
+ register_mime_type 'application/font-woff2', extensions: ['.woff2']
72
+
73
+ # Sourmaps
74
+ register_mime_type 'application/sourcemap', extension: '.map', charset: :unicode
75
+
76
+ # ERB
77
+ require 'condenser/templating_engine/erb'
78
+ register_mime_type 'application/erb', extension: '.erb'
79
+ register_template 'application/erb', Condenser::Erubi
80
+
81
+ # CSS
82
+ register_mime_type 'text/css', extension: '.css', charset: :css
83
+
84
+ # SASS
85
+ require 'condenser/processors/sass_processor'
86
+ register_mime_type 'text/sass', extensions: %w(.sass .css.sass)
87
+ # register_transformer 'text/sass', 'text/css', SassProcessor
88
+
89
+ # SCSS
90
+ register_mime_type 'text/scss', extensions: %w(.scss .css.scss)
91
+ register_transformer 'text/scss', 'text/css', Condenser::ScssProcessor
92
+
93
+ # Javascript
94
+ require 'condenser/processors/rollup_processor'
95
+ require 'condenser/processors/babel_processor'
96
+ register_mime_type 'application/javascript', extension: '.js', charset: :unicode
97
+ register_preprocessor 'application/javascript', Condenser::BabelProcessor
98
+ register_exporter 'application/javascript', Condenser::RollupProcessor
99
+ # register_minifier 'application/javascript', Uglifier
100
+
101
+ # Writers
102
+ require 'condenser/writers/file_writer'
103
+ require 'condenser/writers/zlib_writer'
104
+ register_mime_type 'application/gzip', extensions: %w(.gz .gzip)
105
+ # register_compressor 'application/gzip', Condenser::Erubi
106
+ register_writer '*/*', Condenser::FileWriter.new
107
+ register_writer Condenser::ZlibWriter::COMPRESSALBE_TYPES, Condenser::ZlibWriter.new, 'application/gzip'
108
+ end
@@ -0,0 +1,221 @@
1
+ require 'set'
2
+ require 'digest/md5'
3
+ require 'digest/sha1'
4
+ require 'digest/sha2'
5
+ require 'condenser/export'
6
+
7
+ class Condenser
8
+ class Asset
9
+
10
+ include EncodingUtils
11
+
12
+ attr_reader :environment, :filename, :content_types, :source_file, :source_path
13
+ attr_reader :linked_assets, :dependencies
14
+ attr_writer :source, :sourcemap
15
+
16
+ attr_accessor :imports
17
+
18
+ def initialize(env, attributes={})
19
+ @environment = env
20
+
21
+ @filename = attributes[:filename]
22
+ @content_types = Array(attributes[:content_types] || attributes[:content_type])
23
+
24
+ @source_file = attributes[:source_file]
25
+ @source_path = attributes[:source_path]
26
+
27
+ @linked_assets = Set.new
28
+ @dependencies = Set.new
29
+
30
+ @processed = false
31
+ @exported = false
32
+ end
33
+
34
+ def path
35
+ filename.sub(/\.(\w+)$/) { |ext| "-#{etag}#{ext}" }
36
+ end
37
+
38
+ def content_type
39
+ @content_types.last
40
+ end
41
+
42
+ def process
43
+ process! if @processed == false
44
+ end
45
+
46
+ def basepath
47
+ dirname, basename, extensions, mime_types = @environment.decompose_path(filename)
48
+ [dirname, basename].compact.join('/')
49
+ end
50
+
51
+ def cache_key(data)
52
+ end
53
+
54
+ def process!(source=nil, source_digest=nil, content_digest=nil)
55
+ source ||= File.binread(@source_file)
56
+ source_digest ||= Digest::SHA1.base64digest(source)
57
+ content_digest ||= Digest::SHA1.base64digest(@content_types.join(':'))
58
+
59
+ result = @environment.cache.fetch("process/#{source_digest}/#{content_digest}") do
60
+ dirname, basename, extensions, mime_types = @environment.decompose_path(@source_file)
61
+ data = {
62
+ source: source,
63
+ source_file: @source_file,
64
+ source_digest: source_digest,
65
+
66
+ filename: @filename.dup,
67
+ content_type: mime_types,
68
+
69
+ map: nil,
70
+ linked_assets: [],
71
+ dependencies: []
72
+ }
73
+
74
+ while @environment.templates.has_key?(data[:content_type].last)
75
+ templator = @environment.templates[data[:content_type].pop]
76
+ templator.call(@environment, data)
77
+ data[:filename] = data[:filename].gsub(/\.#{extensions.last}$/, '')
78
+ end
79
+
80
+ case @environment.mime_types[data[:content_type].last][:charset]
81
+ when :unicode
82
+ detect_unicode(data[:source])
83
+ when :css
84
+ detect_css(data[:source])
85
+ when :html
86
+ detect_html(data[:source])
87
+ else
88
+ detect(data[:source]) if mime_types.last.start_with?('text/')
89
+ end
90
+
91
+ if @environment.preprocessors.has_key?(data[:content_type].last)
92
+ @environment.preprocessors[data[:content_type].last].each do |processor|
93
+ processor.call(@environment, data)
94
+ end
95
+ end
96
+
97
+ if data[:content_type].last != @content_types.last && @environment.transformers.has_key?(data[:content_type].last)
98
+ @environment.transformers[data[:content_type].pop].each do |to_mime_type, processor|
99
+ processor.call(@environment, data)
100
+ data[:content_type] << to_mime_type
101
+ end
102
+ end
103
+
104
+ if mime_types != @content_types
105
+ raise ContentTypeMismatch, "mime type(s) \"#{@content_types.join(', ')}\" does not match requested mime type(s) \"#{data[:mime_types].join(', ')}\""
106
+ end
107
+
108
+ data[:digest] = @environment.digestor.digest(data[:source])
109
+ data[:digest_name] = @environment.digestor.name.sub(/^.*::/, '').downcase
110
+ data
111
+ end
112
+ @processed = true
113
+
114
+ @source = result[:source]
115
+ @sourcemap = result[:map]
116
+ @filename = result[:filename]
117
+ @content_types = result[:content_type]
118
+ @linked_assets = result[:linked_assets]
119
+ @digest = result[:digest]
120
+ @digest_name = result[:digest_name]
121
+ @dependencies = result[:dependencies]
122
+ end
123
+
124
+ def export
125
+ source ||= File.binread(@source_file)
126
+ source_digest ||= Digest::SHA1.base64digest(source)
127
+ content_digest ||= Digest::SHA1.base64digest(@content_types.join(':'))
128
+
129
+ process!(source, source_digest, content_digest) if !@processed
130
+
131
+ result = @environment.cache.fetch("export/#{source_digest}/#{content_digest}") do
132
+ dirname, basename, extensions, mime_types = @environment.decompose_path(@filename)
133
+ data = {
134
+ source: @source,
135
+ source_file: @source_file,
136
+ source_digest: source_digest,
137
+
138
+ filename: @filename.dup,
139
+ content_types: @content_types,
140
+
141
+ sourcemap: nil,
142
+ linked_assets: [],
143
+ dependencies: []
144
+ }
145
+ @environment.exporters[content_type]&.call(@environment, data)
146
+ data[:digest] = @environment.digestor.digest(data[:source])
147
+ data[:digest_name] = @environment.digestor.name.sub(/^.*::/, '').downcase
148
+ data
149
+ end
150
+
151
+ Export.new(@environment, result)
152
+ end
153
+
154
+ def to_s
155
+ # process
156
+ @source
157
+ end
158
+
159
+ def source
160
+ process
161
+ @source
162
+ end
163
+
164
+ def sourcemap
165
+ process
166
+ @sourcemap
167
+ end
168
+
169
+ def length
170
+ # process
171
+ @source.bytesize
172
+ end
173
+ alias size length
174
+
175
+ def digest
176
+ # process
177
+ @digest
178
+ end
179
+
180
+ def charset
181
+ @source.encoding.name.downcase
182
+ end
183
+
184
+ # Public: Returns String hexdigest of source.
185
+ def hexdigest
186
+ # process
187
+ @digest.unpack('H*'.freeze).first
188
+ end
189
+ alias_method :etag, :hexdigest
190
+
191
+ def integrity
192
+ "#{@digest_name}-#{[@digest].pack('m0')}"
193
+ end
194
+
195
+ def to_json
196
+ { path: path, digest: hexdigest, size: size, integrity: integrity }
197
+ end
198
+
199
+ def write(output_directory)
200
+ files = @environment.writers_for_mime_type(content_type).map do |writer|
201
+ writer[0].call(output_directory, self)
202
+ end
203
+ files.flatten.compact
204
+ end
205
+
206
+ def ext
207
+ File.extname(filename)
208
+ end
209
+
210
+ # Public: Compare assets.
211
+ #
212
+ # Assets are equal if they share the same path and digest.
213
+ #
214
+ # Returns true or false.
215
+ def eql?(other)
216
+ self.class == other.class && self.filename == other.filename && self.content_types == other.content_types
217
+ end
218
+ alias_method :==, :eql?
219
+
220
+ end
221
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Condenser::Cache
4
+ # Public: Basic in memory LRU cache.
5
+ #
6
+ # Assign the instance to the Environment#cache.
7
+ #
8
+ # environment.cache = Sprockets::Cache::MemoryStore.new(1000)
9
+ #
10
+ # See Also
11
+ #
12
+ # ActiveSupport::Cache::MemoryStore
13
+ #
14
+ class MemoryStore
15
+ # Internal: Default key limit for store.
16
+ DEFAULT_MAX_SIZE = 33_554_432 # 32 Megabytes
17
+ PER_ENTRY_OVERHEAD = 240
18
+
19
+ def initialize(options = {})
20
+ @max_size = options[:size] || DEFAULT_MAX_SIZE
21
+ @cache = {}
22
+ @key_access = {}
23
+ @cache_size = 0
24
+ end
25
+
26
+ def clear
27
+ @cache.clear
28
+ @key_access.clear
29
+ @cache_size = 0
30
+ end
31
+
32
+ def fetch(key)
33
+ value = get(key)
34
+
35
+ if value.nil?
36
+ value = yield
37
+ set(key, Marshal.dump(value))
38
+ else
39
+ value = Marshal.load(value)
40
+ end
41
+ value
42
+ end
43
+
44
+ def get(key)
45
+ value = @cache[key]
46
+ @key_access[key] = Time.now.to_f if value
47
+ value
48
+ end
49
+
50
+ def cached_size(key, value)
51
+ key.to_s.bytesize + value.bytesize + PER_ENTRY_OVERHEAD
52
+ end
53
+
54
+ def set(key, value)
55
+ if old_value = @cache[key]
56
+ @cache_size -= (old_value.bytesize - value.bytesize)
57
+ else
58
+ @cache_size += cached_size(key, value)
59
+ end
60
+ @cache[key] = value
61
+ @key_access[key] = Time.now.to_f
62
+ prune if @cache_size > @max_size
63
+ value
64
+ end
65
+
66
+ def delete(key)
67
+ @key_access.delete(key)
68
+ if value = @cache.delete(key)
69
+ @cache_size -= cached_size(key, value)
70
+ true
71
+ else
72
+ false
73
+ end
74
+ end
75
+
76
+ def prune
77
+ keys = @key_access.keys.sort { |a, b| @key_access[a].to_f <=> @key_access[b].to_f }
78
+ keys.each do |key|
79
+ delete(key)
80
+ return if @cache_size <= @max_size
81
+ end
82
+ end
83
+
84
+ # Public: Pretty inspect
85
+ #
86
+ # Returns String.
87
+ def inspect
88
+ "#<#{self.class} size=#{@cache_size}/#{@max_size}>"
89
+ end
90
+
91
+ end
92
+ end