condenser 1.2 → 1.3

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.
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