condenser 1.2 → 1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/condenser/asset.rb +19 -16
  3. data/lib/condenser/build_cache.rb +1 -1
  4. data/lib/condenser/context.rb +9 -25
  5. data/lib/condenser/helpers/parse_helpers.rb +1 -1
  6. data/lib/condenser/manifest.rb +3 -1
  7. data/lib/condenser/pipeline.rb +8 -3
  8. data/lib/condenser/processors/babel_processor.rb +1 -1
  9. data/lib/condenser/processors/css_media_combiner_processor.rb +81 -0
  10. data/lib/condenser/processors/js_analyzer.rb +0 -2
  11. data/lib/condenser/processors/purgecss_processor.rb +6 -4
  12. data/lib/condenser/processors/rollup_processor.rb +37 -35
  13. data/lib/condenser/resolve.rb +1 -3
  14. data/lib/condenser/templating_engine/ejs.rb +1 -1
  15. data/lib/condenser/transformers/dart_sass_transformer.rb +285 -0
  16. data/lib/condenser/transformers/jst_transformer.rb +67 -17
  17. data/lib/condenser/transformers/sass/functions.rb +133 -0
  18. data/lib/condenser/transformers/sass/importer.rb +48 -0
  19. data/lib/condenser/transformers/sass.rb +4 -0
  20. data/lib/condenser/transformers/sass_transformer.rb +124 -281
  21. data/lib/condenser/transformers/svg_transformer/base.rb +26 -0
  22. data/lib/condenser/transformers/svg_transformer/tag.rb +54 -0
  23. data/lib/condenser/transformers/svg_transformer/template.rb +151 -0
  24. data/lib/condenser/transformers/svg_transformer/template_error.rb +2 -0
  25. data/lib/condenser/transformers/svg_transformer/value.rb +13 -0
  26. data/lib/condenser/transformers/svg_transformer/var_generator.rb +10 -0
  27. data/lib/condenser/transformers/svg_transformer.rb +19 -0
  28. data/lib/condenser/version.rb +1 -1
  29. data/lib/condenser.rb +17 -5
  30. data/test/cache_test.rb +46 -2
  31. data/test/dependency_test.rb +2 -2
  32. data/test/manifest_test.rb +34 -0
  33. data/test/minifiers/terser_minifier_test.rb +0 -1
  34. data/test/minifiers/uglify_minifier_test.rb +0 -1
  35. data/test/postprocessors/css_media_combiner_test.rb +107 -0
  36. data/test/postprocessors/purgecss_test.rb +62 -0
  37. data/test/preprocessor/babel_test.rb +693 -299
  38. data/test/preprocessor/js_analyzer_test.rb +0 -2
  39. data/test/processors/rollup_test.rb +50 -20
  40. data/test/resolve_test.rb +8 -9
  41. data/test/server_test.rb +6 -1
  42. data/test/templates/ejs_test.rb +2 -11
  43. data/test/templates/erb_test.rb +0 -5
  44. data/test/test_helper.rb +3 -1
  45. data/test/transformers/dart_scss_test.rb +139 -0
  46. data/test/transformers/jst_test.rb +165 -21
  47. data/test/transformers/scss_test.rb +14 -0
  48. data/test/transformers/svg_test.rb +40 -0
  49. metadata +23 -6
  50. data/lib/condenser/transformers/sass_transformer/importer.rb +0 -50
@@ -0,0 +1,285 @@
1
+ class Condenser::DartSassTransformer < Condenser::NodeProcessor
2
+
3
+ @@helper_methods = Set.new
4
+
5
+ ACCEPT = ['text/css', 'text/scss', 'text/sass']
6
+
7
+ attr_accessor :options
8
+
9
+ # Use this function to append Modules that contain functions to expose
10
+ # to dart-sass
11
+ def self.add_helper_methods(module_to_add = nil, &block)
12
+ old_methods = self.instance_methods
13
+
14
+ self.include(module_to_add) if module_to_add
15
+ self.class_eval(&block) if block_given?
16
+
17
+ @@helper_methods.merge(self.instance_methods - old_methods)
18
+ end
19
+
20
+ add_helper_methods(Condenser::Sass::Functions)
21
+
22
+ def self.syntax
23
+ :sass
24
+ end
25
+
26
+ def initialize(dir, options = {})
27
+ super(dir)
28
+ npm_install('sass')
29
+
30
+ @options = options.merge({
31
+ indentedSyntax: self.class.syntax == :sass
32
+ }).freeze
33
+ end
34
+
35
+ def helper_method_signatures
36
+ x = @@helper_methods.map do |name|
37
+ arity = self.method(name).arity
38
+ signature = []
39
+ types = []
40
+ if respond_to?(:"#{name}_signature")
41
+ send(:"#{name}_signature").each do |arg, type|
42
+ signature << arg
43
+ types << type
44
+ end
45
+ elsif arity >= 0
46
+ arity.times.with_index do |a|
47
+ signature << "$arg#{a}"
48
+ types << 'String'
49
+ end
50
+ end
51
+
52
+ ["#{name}(#{signature.join(', ')})", types]
53
+ end
54
+ x
55
+ end
56
+
57
+ def call(environment, input)
58
+ @context = environment.new_context_class#.new(environment)
59
+
60
+ options = {
61
+ verbose: true,
62
+ file: File.join('/', input[:filename])
63
+ }.merge(@options)
64
+ @environment = environment
65
+ @input = input
66
+
67
+ result = exec_runtime(<<-JS)
68
+ const fs = require('fs');
69
+ const sass = require("#{npm_module_path('sass')}");
70
+ const stdin = process.stdin;
71
+ stdin.resume();
72
+ stdin.setEncoding('utf8');
73
+
74
+ // SET STDOUT to sync so that we don't get in an infinte looping waiting
75
+ // for Ruby to response when we haven't even sent the entire request.
76
+ if (process.stdout._handle) process.stdout._handle.setBlocking(true)
77
+
78
+ const source = #{JSON.generate(input[:source])};
79
+ const options = #{JSON.generate(options)};
80
+
81
+ var rid = 0;
82
+ function request(method, ...args) {
83
+ var trid = rid;
84
+ rid += 1;
85
+ console.log(JSON.stringify({ rid: trid, method: method, args: args }) + "\\n");
86
+
87
+
88
+ var readBuffer = '';
89
+ var response = null;
90
+ let chunk = new Buffer(1024);
91
+ let bytesRead;
92
+
93
+ while (response === null) {
94
+ try {
95
+ bytesRead = fs.readSync(stdin.fd, chunk, 0, 1024);
96
+ if (bytesRead === null) { exit(1); }
97
+ readBuffer += chunk.toString('utf8', 0, bytesRead);
98
+ [readBuffer, response] = readResponse(readBuffer);
99
+ } catch (e) {
100
+ if (e.code !== 'EAGAIN') { throw e; }
101
+ }
102
+ }
103
+ return response['return'];
104
+ }
105
+
106
+ function readResponse(buffer) {
107
+ try {
108
+ var message = JSON.parse(buffer);
109
+ return ['', message];
110
+ } catch(e) {
111
+ if (e.name === "SyntaxError") {
112
+ if (e.message.startsWith('Unexpected non-whitespace character after JSON at position ')) {
113
+ let pos = parseInt(e.message.slice(59));
114
+ let [b, r] = readResponse(buffer.slice(0,pos));
115
+ return [b + buffer.slice(pos), r];
116
+ } else if (e.message.startsWith('Unexpected token { in JSON at position ')) {
117
+ // This can be removed, once dropping support for node <= v18
118
+ var pos = parseInt(e.message.slice(39));
119
+ let [b, r] = readResponse(buffer.slice(0,pos));
120
+ return [b + buffer.slice(pos), r];
121
+ } else {
122
+ return [buffer, null];
123
+ }
124
+ } else {
125
+ console.log(JSON.stringify({method: 'error', args: [e.name, e.message]}) + "\\n");
126
+ process.exit(1);
127
+ }
128
+ }
129
+ }
130
+
131
+
132
+ options.importer = function(url, prev) { return request('load', url, prev); };
133
+
134
+ const call_fn = function(name, types, args) {
135
+ const transformedArgs = [];
136
+ request('log', '==================')
137
+ args.forEach((a, i) => {
138
+ if (types[i] === 'Map') {
139
+ // Don't know how to go from SassMap to hash yet
140
+ transformedArgs.push({});
141
+ } else if (types[i] === 'List') {
142
+ // Don't know how to go from SassList to hash yet
143
+ transformedArgs.push([]);
144
+ } else if (!(a instanceof sass.types[types[i]])) {
145
+ throw "$url: Expected a string.";
146
+ } else {
147
+ transformedArgs.push(a.getValue());
148
+ }
149
+
150
+
151
+ // if (types[i] === 'List') { a = a.contents(); }
152
+ });
153
+ request('log', name, transformedArgs, types)
154
+ return new sass.types.String(request('call', name, transformedArgs));
155
+ }
156
+ options.functions = {};
157
+ #{JSON.generate(helper_method_signatures)}.forEach( (f) => {
158
+ let name = f[0].replace(/-/g, '_').replace(/(^[^\\(]+)\\(.*\\)$/, '$1');
159
+ options.functions[f[0]] = (...a) => call_fn(name, f[1], a);
160
+ })
161
+
162
+ try {
163
+ options.data = source;
164
+ const result = sass.renderSync(options);
165
+ request('result', result.css.toString());
166
+ process.exit(0);
167
+ } catch(e) {
168
+ request('error', e.name, e.message);
169
+ process.exit(1);
170
+ }
171
+ JS
172
+
173
+ input[:source] = result
174
+ # input[:map] = map.to_json({})
175
+ input[:linked_assets] += @context.links
176
+ input[:process_dependencies] += @context.dependencies
177
+ end
178
+
179
+ def find(importee, importer = nil)
180
+ # importer ||= @input[:source_file]
181
+ @environment.find(expand_path(importee, importer), nil, accept: ACCEPT)
182
+ end
183
+
184
+ def resolve(importee, importer = nil)
185
+ # importer ||= @input[:source_file]
186
+ @environment.resolve(expand_path(importee, importer), accept: ACCEPT)
187
+ end
188
+
189
+ def expand_path(path, base=nil)
190
+ if path.start_with?('.')
191
+ File.expand_path(path, File.dirname(base)).delete_prefix(File.expand_path('.') + '/')
192
+ else
193
+ File.expand_path(path).delete_prefix(File.expand_path('.') + '/')
194
+ end
195
+ end
196
+
197
+ def exec_runtime(script)
198
+ io = IO.popen([binary, '-e', script], 'r+')
199
+ buffer = ''
200
+ result = nil
201
+
202
+ begin
203
+ while IO.select([io]) && io_read = io.read_nonblock(1_024)
204
+ buffer << io_read
205
+ messages = buffer.split("\n\n")
206
+ buffer = buffer.end_with?("\n\n") ? '' : messages.pop
207
+
208
+ messages.each do |message|
209
+ message = JSON.parse(message)
210
+
211
+ ret = case message['method']
212
+ when 'result'
213
+ result = message['args'][0]
214
+ nil
215
+ when 'load'
216
+ importee = message['args'][0]
217
+ importer = message['args'][1]
218
+
219
+ if importee.end_with?('*')
220
+ @context.depend_on(importee)
221
+ code = ""
222
+ resolve(importee, importer).each do |f, i|
223
+ code << "@import '#{f.filename}';\n"
224
+ end
225
+ { contents: code, map: nil }
226
+ else
227
+ if asset = find(importee)
228
+ @context.depend_on(asset.filename)
229
+ { contents: asset.source, map: asset.sourcemap }
230
+ else
231
+ @context.depend_on(importee)
232
+ nil
233
+ end
234
+ end
235
+ when 'error'
236
+ io.write(JSON.generate({rid: message['rid'], return: nil}))
237
+
238
+ case message['args'][0]
239
+ when 'AssetNotFound'
240
+ error_message = "Could not find import \"#{message['args'][1]}\" for \"#{message['args'][2]}\".\n\n"
241
+ error_message << build_tree(message['args'][3], input, message['args'][2])
242
+ raise exec_runtime_error(error_message)
243
+ else
244
+ raise exec_runtime_error(message['args'][0] + ': ' + message['args'][1])
245
+ end
246
+ when 'call'
247
+ if respond_to?(message['args'][0])
248
+ send(message['args'][0], *message['args'][1])
249
+ else
250
+ puts '!!!'
251
+ end
252
+ end
253
+
254
+ io.write(JSON.generate({rid: message['rid'], return: ret}))
255
+ end
256
+ end
257
+ rescue Errno::EPIPE, EOFError
258
+ end
259
+
260
+ io.close
261
+ if $?.success?
262
+ result
263
+ else
264
+ raise exec_runtime_error(buffer)
265
+ end
266
+ end
267
+
268
+ protected
269
+
270
+ def condenser_context
271
+ @context
272
+ end
273
+
274
+ def condenser_environment
275
+ @environment
276
+ end
277
+
278
+
279
+ end
280
+
281
+ class Condenser::DartScssTransformer < Condenser::DartSassTransformer
282
+ def self.syntax
283
+ :scss
284
+ end
285
+ end
@@ -23,32 +23,32 @@ class Condenser::JstTransformer < Condenser::NodeProcessor
23
23
  const source = #{JSON.generate(input[:source])};
24
24
  const options = #{JSON.generate(opts).gsub(/"@?babel[\/-][^"]+"/) { |m| "require(#{m})"}};
25
25
 
26
- function globalVar(scope, name) {
27
- if (name in scope.globals) {
28
- return true;
29
- } else if (scope.parent === null || scope.parent === undefined) {
30
- return false;
31
- } else {
32
- return globalVar(scope.parent, name);
33
- }
34
- }
35
-
26
+ let scope = [['document', 'window']];
27
+
36
28
  options['plugins'].unshift(function({ types: t }) {
37
29
  return {
38
30
  visitor: {
39
31
  Identifier(path, state) {
40
- if ( path.parent.type == 'MemberExpression' && path.parent.object != path.node) {
32
+ if ( path.parent.type === 'MemberExpression' && path.parent.object !== path.node) {
41
33
  return;
42
34
  }
43
- if ( path.parent.type == 'ImportSpecifier' || path.parent.type == 'ImportDefaultSpecifier' || path.parent.type =='FunctionDeclaration') {
35
+
36
+ if ( path.parent.type === 'ImportSpecifier' ||
37
+ path.parent.type === 'ImportDefaultSpecifier' ||
38
+ path.parent.type === 'FunctionDeclaration' ||
39
+ path.parent.type === 'FunctionExpression' ||
40
+ path.parent.type === 'ArrowFunctionExpression' ||
41
+ path.parent.type === 'SpreadElement' ||
42
+ path.parent.type === 'CatchClause' ) {
43
+ return;
44
+ }
45
+
46
+ if ( path.parent.type === 'ObjectProperty' && path.parent.key === path.node ) {
44
47
  return;
45
48
  }
46
49
 
47
- if (
48
- path.node.name !== 'document' &&
49
- path.node.name !== 'window' &&
50
- !(path.node.name in global) &&
51
- globalVar(path.scope, path.node.name)
50
+ if ( !(path.node.name in global) &&
51
+ !scope.find((s) => s.find(v => v === path.node.name))
52
52
  ) {
53
53
  path.replaceWith(
54
54
  t.memberExpression(t.identifier("locals"), path.node)
@@ -58,6 +58,56 @@ class Condenser::JstTransformer < Condenser::NodeProcessor
58
58
  }
59
59
  };
60
60
  });
61
+
62
+
63
+ options['plugins'].unshift(function({ types: t }) {
64
+ return {
65
+ visitor: {
66
+ "FunctionDeclaration|FunctionExpression|ArrowFunctionExpression": {
67
+ enter(path, state) {
68
+ if (path.node.id) { scope[scope.length-1].push(path.node.id.name); }
69
+ scope.push(path.node.params.map((n) => n.type === 'RestElement' ? n.argument.name : n.name));
70
+ }
71
+ },
72
+ CatchClause: {
73
+ enter(path, state) {
74
+ scope.push([]);
75
+ if (path.node.param.name) { scope[scope.length-1].push(path.node.param.name); }
76
+ }
77
+ },
78
+ Scopable: {
79
+ enter(path, state) {
80
+ if (path.node.type !== 'Program' &&
81
+ path.node.type !== 'CatchClause' &&
82
+ path.parent.type !== 'FunctionDeclaration' &&
83
+ path.parent.type !== 'FunctionExpression' &&
84
+ path.parent.type !== 'ArrowFunctionExpression' &&
85
+ path.parent.type !== 'ExportDefaultDeclaration') {
86
+ scope.push([]);
87
+ }
88
+ },
89
+ exit(path, state) {
90
+ if (path.node.type !== 'Program' &&
91
+ path.parent.type !== 'ExportDefaultDeclaration') {
92
+ scope.pop();
93
+ }
94
+ }
95
+ },
96
+ ImportDeclaration(path, state) {
97
+ path.node.specifiers.forEach((s) => scope[scope.length-1].push(s.local.name));
98
+ },
99
+ ClassDeclaration(path, state) {
100
+ if (path.node.id) {
101
+ scope[scope.length-1].push(path.node.id.name)
102
+ }
103
+ },
104
+ VariableDeclaration(path, state) {
105
+ path.node.declarations.forEach((s) => scope[scope.length-1].push(s.id.name));
106
+ }
107
+ }
108
+ };
109
+ });
110
+
61
111
 
62
112
  try {
63
113
  const result = babel.transform(source, options);
@@ -0,0 +1,133 @@
1
+ # Public: Functions injected into Sass context during Condenser evaluation.
2
+ #
3
+ # This module may be extended to add global functionality to all Condenser
4
+ # Sass environments. Though, scoping your functions to just your environment
5
+ # is preferred.
6
+ #
7
+ # module Condenser::SassProcessor::Functions
8
+ # def asset_path(path, options = {})
9
+ # end
10
+ # end
11
+ module Condenser::Sass
12
+ module Functions
13
+
14
+ # Public: Generate a url for asset path.
15
+ #
16
+ # Defaults to Context#asset_path.
17
+ def asset_path(path, options = {})
18
+ condenser_context.link_asset(path)
19
+
20
+ path = condenser_context.asset_path(path, options)
21
+ query = "?#{query}" if query
22
+ fragment = "##{fragment}" if fragment
23
+ "#{path}#{query}#{fragment}"
24
+ end
25
+
26
+ def asset_path_signature
27
+ {
28
+ "$path" => "String",
29
+ "$options: ()" => 'Map'
30
+ }
31
+ end
32
+
33
+ # Public: Generate a asset url() link.
34
+ #
35
+ # path - String
36
+ def asset_url(path, options = {})
37
+ "url(#{asset_path(path, options)})"
38
+ end
39
+
40
+ def asset_url_signature
41
+ {
42
+ "$path" => "String",
43
+ "$options: ()" => 'Map'
44
+ }
45
+ end
46
+
47
+ # Public: Generate url for image path.
48
+ def image_path(path)
49
+ asset_path(path, type: :image)
50
+ end
51
+
52
+ # Public: Generate a image url() link.
53
+ def image_url(path)
54
+ asset_url(path, type: :image)
55
+ end
56
+
57
+ # Public: Generate url for video path.
58
+ def video_path(path)
59
+ asset_path(path, type: :video)
60
+ end
61
+
62
+ # Public: Generate a video url() link.
63
+ def video_url(path)
64
+ asset_url(path, type: :video)
65
+ end
66
+
67
+ # Public: Generate url for audio path.
68
+ def audio_path(path)
69
+ asset_path(path, type: :audio)
70
+ end
71
+
72
+ # Public: Generate a audio url() link.
73
+ def audio_url(path)
74
+ asset_url(path, type: :audio)
75
+ end
76
+
77
+ # Public: Generate url for font path.
78
+ def font_path(path)
79
+ asset_path(path, type: :font)
80
+ end
81
+
82
+ # Public: Generate a font url() link.
83
+ def font_url(path)
84
+ asset_url(path, type: :font)
85
+ end
86
+
87
+ # Public: Generate url for javascript path.
88
+ def javascript_path(path)
89
+ asset_path(path, type: :javascript)
90
+ end
91
+
92
+ # Public: Generate a javascript url() link.
93
+ def javascript_url(path)
94
+ asset_url(path, type: :javascript)
95
+ end
96
+
97
+ # Public: Generate url for stylesheet path.
98
+ def stylesheet_path(path)
99
+ asset_path(path, type: :stylesheet)
100
+ end
101
+
102
+ # Public: Generate a stylesheet url() link.
103
+ def stylesheet_url(path)
104
+ asset_url(path, type: :stylesheet)
105
+ end
106
+
107
+ # Public: Generate a data URI for asset path.
108
+ def asset_data_url(path)
109
+ url = condenser_environment.asset_data_uri(path.value)
110
+ Sass::Script::String.new("url(" + url + ")")
111
+ end
112
+
113
+ protected
114
+ # Public: The Environment.
115
+ #
116
+ # Returns Condenser::Environment.
117
+ def condenser_context
118
+ options[:condenser][:context]
119
+ end
120
+
121
+ def condenser_environment
122
+ options[:condenser][:environment]
123
+ end
124
+
125
+ # Public: Mutatable set of dependencies.
126
+ #
127
+ # Returns a Set.
128
+ def condenser_dependencies
129
+ options[:asset][:process_dependencies]
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,48 @@
1
+ require "sassc"
2
+
3
+ class Condenser::Sass::Importer < SassC::Importer
4
+
5
+ def imports(name, base)
6
+ name = expand_path(name, base)
7
+ env = options[:condenser][:environment]
8
+ accept = extensions.keys.map { |x| options[:condenser][:environment].extensions[x] }
9
+
10
+ options[:asset][:process_dependencies] << [name, accept.map{ |i| [i] }]
11
+
12
+ imports = []
13
+ env.resolve(name, accept: accept).sort_by(&:filename).each do |asset|
14
+ next if asset.filename == options[:filename]
15
+ imports << Import.new(asset.filename, source: asset.source, source_map_path: nil)
16
+ end
17
+
18
+ if imports.empty? && env.npm_path
19
+ package = File.join(env.npm_path, name, 'package.json')
20
+ if File.exist?(package)
21
+ package = JSON.parse(File.read(package))
22
+ if package['style']
23
+ imports << Import.new(name, source: File.read(File.join(env.npm_path, name, package['style'])), source_map_path: nil)
24
+ end
25
+ end
26
+ end
27
+
28
+ raise Condenser::FileNotFound, "couldn't find file '#{name}'" if imports.empty?
29
+
30
+ imports
31
+ end
32
+
33
+ # Allow .css files to be @imported
34
+ def extensions
35
+ { '.sass' => :sass, '.scss' => :scss, '.css' => :scss }
36
+ end
37
+
38
+ private
39
+
40
+ def expand_path(path, base=nil)
41
+ if path.start_with?('.')
42
+ File.expand_path(path, File.dirname(base)).delete_prefix(File.expand_path('.') + '/')
43
+ else
44
+ File.expand_path(path).delete_prefix(File.expand_path('.') + '/')
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,4 @@
1
+ module Condenser::Sass
2
+ autoload :Functions, 'condenser/transformers/sass/functions'
3
+ autoload :Importer, 'condenser/transformers/sass/importer'
4
+ end