condenser 1.3 → 1.5

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/lib/condenser/asset.rb +103 -25
  3. data/lib/condenser/build_cache.rb +23 -8
  4. data/lib/condenser/cache/file_store.rb +1 -0
  5. data/lib/condenser/cache/memory_store.rb +1 -0
  6. data/lib/condenser/cache/null_store.rb +1 -0
  7. data/lib/condenser/cache_store.rb +1 -0
  8. data/lib/condenser/context.rb +1 -0
  9. data/lib/condenser/encoding_utils.rb +2 -0
  10. data/lib/condenser/environment.rb +2 -0
  11. data/lib/condenser/errors.rb +2 -0
  12. data/lib/condenser/export.rb +11 -6
  13. data/lib/condenser/helpers/parse_helpers.rb +23 -1
  14. data/lib/condenser/manifest.rb +3 -1
  15. data/lib/condenser/minifiers/sass_minifier.rb +2 -0
  16. data/lib/condenser/minifiers/terser_minifier.rb +2 -0
  17. data/lib/condenser/minifiers/uglify_minifier.rb +2 -0
  18. data/lib/condenser/pipeline.rb +2 -0
  19. data/lib/condenser/processors/babel_processor.rb +19 -16
  20. data/lib/condenser/processors/css_media_combiner_processor.rb +7 -5
  21. data/lib/condenser/processors/js_analyzer.rb +149 -42
  22. data/lib/condenser/processors/node_processor.rb +3 -0
  23. data/lib/condenser/processors/purgecss_processor.rb +2 -0
  24. data/lib/condenser/processors/rollup_processor.rb +289 -136
  25. data/lib/condenser/resolve.rb +41 -10
  26. data/lib/condenser/server.rb +22 -20
  27. data/lib/condenser/templating_engine/ejs.rb +2 -0
  28. data/lib/condenser/templating_engine/erb.rb +2 -0
  29. data/lib/condenser/transformers/dart_sass_transformer.rb +5 -3
  30. data/lib/condenser/transformers/jst_transformer.rb +2 -0
  31. data/lib/condenser/transformers/sass/functions.rb +2 -0
  32. data/lib/condenser/transformers/sass/importer.rb +2 -0
  33. data/lib/condenser/transformers/sass.rb +2 -0
  34. data/lib/condenser/transformers/sass_transformer.rb +2 -0
  35. data/lib/condenser/transformers/svg_transformer/base.rb +2 -0
  36. data/lib/condenser/transformers/svg_transformer/tag.rb +2 -0
  37. data/lib/condenser/transformers/svg_transformer/template.rb +3 -1
  38. data/lib/condenser/transformers/svg_transformer/template_error.rb +2 -0
  39. data/lib/condenser/transformers/svg_transformer/value.rb +2 -0
  40. data/lib/condenser/transformers/svg_transformer/var_generator.rb +2 -0
  41. data/lib/condenser/transformers/svg_transformer.rb +2 -0
  42. data/lib/condenser/utils.rb +2 -0
  43. data/lib/condenser/version.rb +3 -1
  44. data/lib/condenser/writers/brotli_writer.rb +2 -0
  45. data/lib/condenser/writers/file_writer.rb +2 -0
  46. data/lib/condenser/writers/zlib_writer.rb +2 -0
  47. data/lib/condenser.rb +2 -0
  48. data/lib/rake/condensertask.rb +2 -0
  49. data/test/cache_test.rb +115 -20
  50. data/test/dependency_test.rb +51 -2
  51. data/test/manifest_test.rb +17 -2
  52. data/test/postprocessors/css_media_combiner_test.rb +9 -12
  53. data/test/preprocessor/babel_test.rb +876 -349
  54. data/test/preprocessor/js_analyzer_test.rb +208 -4
  55. data/test/processors/rollup/dynamic_import_test.rb +358 -0
  56. data/test/processors/rollup_test.rb +37 -56
  57. data/test/resolve_test.rb +14 -9
  58. data/test/server_test.rb +10 -9
  59. data/test/test_helper.rb +6 -3
  60. data/test/transformers/dart_scss_test.rb +2 -2
  61. data/test/transformers/scss_test.rb +2 -2
  62. metadata +6 -11
  63. data/lib/condenser/minifiers/package-lock.json +0 -25
@@ -1,38 +1,99 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'tmpdir'
3
5
 
4
- class Condenser::RollupProcessor < Condenser::NodeProcessor
6
+ class Condenser::RollupProcessor
7
+
8
+ @@setup = false
5
9
 
6
- attr_accessor :options
10
+ def self.setup(environment)
11
+ install_npm_packages(environment.npm_path)
12
+ end
7
13
 
8
- def initialize(dir = nil, options = {})
9
- super(dir)
10
- npm_install('rollup', '@rollup/plugin-commonjs', '@rollup/plugin-node-resolve')
14
+ def self.install_npm_packages(npm_path)
15
+ return if @@setup
11
16
 
12
- @options = options.freeze
17
+ ::Condenser::NodeProcessor.new(npm_path).npm_install(
18
+ 'rollup',
19
+ '@rollup/plugin-commonjs',
20
+ '@rollup/plugin-node-resolve',
21
+ '@rollup/plugin-replace'
22
+ )
23
+ @@setup = true
24
+ end
25
+
26
+ def self.call(environment, input)
27
+ new(environment.npm_path).call(environment, input)
28
+ end
29
+
30
+ def name
31
+ self.class.name
32
+ end
33
+
34
+ def options
35
+ {prefix: @prefix, dynamic_imports: @dynamic_imports}
36
+ end
37
+
38
+ # @param prefix [string] string to prefix to the url of dynamic imports
39
+ # @param imports [symbol] :inline will inline all the imports in the
40
+ # output file if possible.
41
+ # false will keep all the imports as imports.
42
+ # :local will not inline URLS.
43
+ # @param dynamic_imports [symbol] Default is :local
44
+ # :inline - Inline dynamic imports into the output file if possible.
45
+ # :local - Same as :inline except for URLs are kept as URLs
46
+ # :keep - Keep the dynamic imports but rewrite the import as a URL
47
+ def initialize(dir = nil, prefix: nil, dynamic_imports: :inline)
48
+ self.class.install_npm_packages(dir)
49
+ @npm_dir = dir
50
+ @prefix = prefix
51
+ @dynamic_imports = dynamic_imports
13
52
  end
14
53
 
15
54
  def call(environment, input)
16
- @environment = environment
17
- @input = input
55
+ Runner.new(@npm_dir, prefix: @prefix, dynamic_imports: @dynamic_imports).call(environment, input)
56
+ end
57
+
58
+
59
+ class Runner < Condenser::NodeProcessor
60
+ def initialize(dir = nil, prefix: nil, dynamic_imports: :inline)
61
+ super(dir)
62
+ @prefix = prefix
63
+ @dynamic_imports = dynamic_imports
64
+ end
65
+
66
+ def call(environment, input)
67
+ @environment = environment
68
+ @input = input
69
+
70
+ @input_dir = File.realdirpath(Dir.mktmpdir)
71
+ @output_dir = File.realdirpath(Dir.mktmpdir)
72
+
73
+ @entry = File.join(@input_dir, 'entry.js')
18
74
 
19
- Dir.mktmpdir do |output_dir|
20
- @entry = File.join(output_dir, 'entry.js')
21
75
  input_options = {
22
76
  input: @entry
23
77
  }
78
+
24
79
  output_options = {
25
- file: File.join(output_dir, 'result.js'),
26
- format: 'iife',
27
- # output: { sourcemap: true, format: 'iife' },
80
+ dir: @output_dir,
81
+ format: 'es',
28
82
  globals: [],
29
- sourcemap: true
83
+ sourcemap: false,
84
+ inlineDynamicImports: true,
85
+ generatedCode: {
86
+ arrowFunctions: true,
87
+ constBindings: true,
88
+ objectShorthand: true,
89
+ preset: 'es2015',
90
+ }
30
91
  }
92
+
31
93
  if input[:source] =~ /export\s+{[^}]+};?\z/i
32
94
  output_options[:name] = File.basename(input[:filename], ".*").capitalize
33
- # output_options[:output][:name] = File.basename(input[:filename], ".*").capitalize
34
95
  end
35
-
96
+
36
97
  exec_runtime(<<-JS, @entry)
37
98
  const fs = require('fs');
38
99
  const path = require('path');
@@ -73,14 +134,19 @@ class Condenser::RollupProcessor < Condenser::NodeProcessor
73
134
  buffer += chunk;
74
135
  buffer = emitMessages(buffer);
75
136
  });
76
-
137
+
77
138
  const rollup = require("#{npm_module_path('rollup')}");
78
139
  const commonjs = require("#{npm_module_path('@rollup/plugin-commonjs')}");
79
140
  const nodeResolve = require("#{npm_module_path('@rollup/plugin-node-resolve')}").nodeResolve;
141
+ const asyncWalk = require("#{npm_module_path('estree-walker')}").asyncWalk;
142
+ const MagicString = require("#{npm_module_path('magic-string')}");
143
+ const replace = require("#{npm_module_path('@rollup/plugin-replace')}");
144
+
80
145
  var rid = 0;
81
146
  var renderStack = {};
147
+ var dynamicImports = {};
82
148
  var nodeResolver = null;
83
-
149
+
84
150
  function request(method, args) {
85
151
  var trid = rid;
86
152
  rid += 1;
@@ -107,8 +173,58 @@ class Condenser::RollupProcessor < Condenser::NodeProcessor
107
173
 
108
174
  const inputOptions = #{JSON.generate(input_options)};
109
175
  inputOptions.plugins = [];
110
- inputOptions.plugins.push({
176
+
177
+ inputOptions.plugins.push(replace({
178
+ preventAssignment: true,
179
+ values: {
180
+ "process.env.NODE_ENV": JSON.stringify("production")
181
+ }
182
+ }), {
183
+ name: 'transform',
184
+ transform: async function(code, id) {
185
+ const magicString = new MagicString(code);
186
+ const ast = this.parse(code, { sourceType: 'module' });
187
+
188
+ await asyncWalk(ast, {
189
+ async enter(node) {
190
+ if(['ImportExpression'].includes(node.type) && node.source.type == 'Literal') {
191
+ const asset = await request('resolveDynamicImport', [node.source.value, id]);
192
+
193
+ if (asset.export) {
194
+ dynamicImports[asset.source] = asset;
195
+ magicString.update(node.source.start, node.source.end, `'${asset.source}'`);
196
+ } else {
197
+ dynamicImports[node.source.value] = asset;
198
+ }
199
+ } else if (node.type == 'ImportDeclaration' ) {
200
+ if (node.value == '@arcgis/lumina/controllers') {
201
+ magicString.update(node.start, node.end, `'@arcgis/components-controllers'`);
202
+ }
203
+ }
204
+ }
205
+ });
206
+
207
+ return {
208
+ code: magicString.toString(),
209
+ map: null
210
+ };
211
+ }
212
+ }, {
111
213
  name: 'condenser',
214
+ resolveDynamicImport: function (importee, importer, options) {
215
+ const asset = dynamicImports[importee];
216
+ request('resolve!!!!', [importee, dynamicImports]);
217
+
218
+ if (!asset) {
219
+ return;
220
+ }
221
+
222
+ if (asset.export) {
223
+ return false;
224
+ }
225
+
226
+ return asset.source;
227
+ },
112
228
  resolveId: function (importee, importer, options) {
113
229
  if (importee.startsWith('\\0') || (importer && importer.startsWith('\\0'))) {
114
230
  return null;
@@ -125,36 +241,32 @@ class Condenser::RollupProcessor < Condenser::NodeProcessor
125
241
  return value;
126
242
  });
127
243
  },
244
+
128
245
  load: function(id) {
129
246
  if (id.startsWith('\\0')) {
130
247
  return null;
131
248
  }
132
249
 
133
- return request('load', [id]).then(function(value) {
134
- return value;
135
- });
250
+ return request('load', [id]);
136
251
  }
137
252
  });
138
-
253
+
139
254
  inputOptions.plugins.push(nodeResolver);
140
255
  inputOptions.plugins.push(commonjs());
141
-
142
- // inputOptions.plugins.push({
143
- // name: 'nullHanlder',
144
- // resolveId: function (importee, importer, options) {
145
- // request('log', [importee, importer, options])
146
- // // request('error', ["AssetNotFound", importee, importer, renderStack]).then(function(value) {
147
- // // process.exit(1);
148
- // // });
149
- // }
150
- // });
151
-
256
+
152
257
  const outputOptions = #{JSON.generate(output_options)};
258
+ outputOptions.paths = (id) => {
259
+ const asset = dynamicImports[id];
260
+
261
+ if (asset) {
262
+ return asset.path;
263
+ }
264
+ }
153
265
 
154
266
  async function build() {
155
267
  try {
156
268
  // inputOptions.cache = await JSON.parse(request('get_cache', []));
157
-
269
+
158
270
  const bundle = await rollup.rollup(inputOptions);
159
271
  await bundle.write(outputOptions);
160
272
  // await request('set_cache', [JSON.stringify(bundle)]);
@@ -167,134 +279,175 @@ class Condenser::RollupProcessor < Condenser::NodeProcessor
167
279
 
168
280
  build();
169
281
  JS
170
-
171
- input[:source] = File.read(File.join(output_dir, 'result.js'))
282
+
283
+ input[:source] = File.read(File.join(@output_dir, 'entry.js'))
172
284
  input[:source].delete_suffix!("//# sourceMappingURL=result.js.map\n")
285
+ input[:type] = 'module'
173
286
  # asset.map = File.read(File.join(output_dir, 'result.js.map'))
287
+ input
288
+ ensure
289
+ begin
290
+ [@input_dir, @output_dir].each { |dir| FileUtils.remove_entry(dir) }
291
+ rescue Errno::ENOENT
292
+ end
174
293
  end
175
- end
176
294
 
177
- def exec_runtime(script, input)
178
- io = IO.popen([binary, '-e', script], 'r+')
179
- buffer = ''
295
+ def exec_runtime(script, input)
296
+ io = IO.popen([binary, '--max_old_space_size=5120', '-e', script], 'r+')
297
+ buffer = String.new
180
298
 
181
- begin
299
+ begin
182
300
 
183
- while IO.select([io]) && io_read = io.read_nonblock(1_024)
184
- buffer << io_read
185
- messages = buffer.split("\n\n")
301
+ while IO.select([io]) && io_read = io.read_nonblock(1_024)
302
+ buffer << io_read
303
+ messages = buffer.split("\n\n")
186
304
 
187
- buffer = if buffer.end_with?("\n\n")
188
- ''
189
- else
190
- messages.pop
191
- end
305
+ buffer = if buffer.end_with?("\n\n")
306
+ String.new
307
+ else
308
+ messages.pop
309
+ end
192
310
 
193
- messages.each do |message|
194
- message = JSON.parse(message)
195
-
196
- ret = case message['method']
197
- when 'resolve'
198
- importee, importer = message['args']
311
+ messages.each do |message|
312
+ message = JSON.parse(message)
313
+ # puts message.inspect
314
+ ret = case message['method']
315
+ when 'log'
316
+ puts message.inspect
317
+ when 'resolveDynamicImport'
318
+ importee, importer = message['args']
199
319
 
200
- if importer.nil? && importee == @entry
201
- @entry
202
- elsif importee.start_with?('@babel/runtime') || importee.start_with?('core-js-pure') || importee.start_with?('regenerator-runtime')
203
- x = File.join(npm_module_path, importee.gsub(/^\.\//, File.dirname(importer) + '/')).sub('/node_modules/regenerator-runtime', '/node_modules/regenerator-runtime/runtime.js')
204
- x = "#{x}.js" if !x.end_with?('.js', '.mjs')
205
- File.file?(x) ? x : (x.delete_suffix('.js') + "/index.js")
206
- elsif npm_module_path && importee&.start_with?(npm_module_path)
207
- x = importee.end_with?('.js', '.mjs') ? importee : "#{importee}.js"
208
- x = (x.delete_suffix('.js') + "/index.js") if !File.file?(x)
209
- x
210
- elsif importee.start_with?('.') && importer.start_with?(npm_module_path)
211
- x = File.expand_path(importee, File.dirname(importer))
212
- x = "#{x}.js" if !x.end_with?('.js', '.mjs')
213
- File.file?(x) ? x : (x.delete_suffix('.js') + "/index.js")
214
- elsif importee.end_with?('*')
215
- File.join(File.dirname(importee), '*')
216
- else
217
- @environment.find(importee, importer ? File.dirname(@entry == importer ? @input[:source_file] : importer) : nil, accept: @input[:content_types].last)&.source_file
218
- end
219
- when 'load'
220
- importee = message['args'].first
221
- if importee == @entry
222
- { code: @input[:source], map: @input[:map] }
223
- elsif importee.end_with?('*')
224
- importees = @environment.resolve(importee, importer ? File.dirname(@entry == importer ? @input[:source_file] : importer) : nil, accept: @input[:content_types].last)
225
- code = ""
226
- code_imports = [];
227
- importees.each_with_index.map do |f, i|
228
- if f.has_default_export?
229
- code << "import _#{i} from '#{f.source_file}';\n"
230
- code_imports << "_#{i}"
231
- elsif f.has_exports?
232
- code << "import * as _#{i} from '#{f.source_file}';\n"
233
- code_imports << "_#{i}"
320
+ asset = @environment.find(importee, importer ? (@entry == importer ? @input[:source_file] : importer) : nil, accept: @input[:content_types].last, npm: true)
321
+ asset ||= @environment.find(importee.delete_suffix('.js') + "/index.js", importer ? (@entry == importer ? @input[:source_file] : importer) : nil, accept: @input[:content_types].last, npm: true)
322
+ asset ||= @environment.find(importee.gsub(/\/[^\/]+$/, '')+ "/dist/index.js", importer ? (@entry == importer ? @input[:source_file] : importer) : nil, accept: @input[:content_types].last, npm: true)
323
+
324
+ if asset.source_file == @input[:source_file]
325
+ {
326
+ path: File.join("/", *[@prefix, asset.path].compact),
327
+ # source: File.join(@input_dir, asset.filename),
328
+ source: @entry,
329
+ export: false,
330
+ # filename: asset.filename
331
+ }
332
+ else
333
+ if @dynamic_imports != :inline
334
+ {
335
+ path: File.join("/", *[@prefix, asset.path].compact),
336
+ source: File.join(@input_dir, asset.filename),
337
+ export: true,
338
+ filename: asset.filename
339
+ }
234
340
  else
235
- code << "import '#{f.source_file}';\n"
341
+ {
342
+ export: false,
343
+ source: asset.source_file,
344
+ filename: asset.filename
345
+ }
236
346
  end
237
347
  end
348
+
349
+ when 'resolve'
350
+ importee, importer = message['args']
351
+
352
+ if importer.nil? && importee == @entry
353
+ @entry
354
+ elsif importee.start_with?('@babel/runtime') || importee.start_with?('core-js-pure') || importee.start_with?('regenerator-runtime')
355
+ x = File.join(npm_module_path, importee.gsub(/^\.\//, File.dirname(importer) + '/')).sub('/node_modules/regenerator-runtime', '/node_modules/regenerator-runtime/runtime.js')
356
+ x = "#{x}.js" if !x.end_with?('.js', '.mjs')
357
+ File.file?(x) ? x : (x.delete_suffix('.js') + "/index.js")
358
+ elsif npm_module_path && importee&.start_with?(npm_module_path)
359
+ x = importee.end_with?('.js', '.mjs') ? importee : "#{importee}.js"
360
+ x = (x.delete_suffix('.js') + "/index.js") if !File.file?(x)
361
+ x
362
+ elsif importee.start_with?('.') && importer.start_with?(npm_module_path)
363
+ x = File.expand_path(importee, File.dirname(importer))
364
+ x = "#{x}.js" if !x.end_with?('.js', '.mjs')
365
+ File.file?(x) ? x : (x.delete_suffix('.js') + "/index.js")
366
+ elsif importee.end_with?('*')
367
+ File.join(File.dirname(importee), '*')
368
+ else
369
+ asset = @environment.find(importee, importer ? (@entry == importer ? @input[:source_file] : importer) : nil, accept: @input[:content_types].last, npm: true)&.source_file
370
+ end
371
+ when 'load'
372
+ importee = message['args'].first
373
+ if importee == @entry
374
+ { code: @input[:source], map: @input[:map] }
375
+ elsif importee.end_with?('*')
376
+ importees = @environment.resolve(importee, importer ? File.dirname(@entry == importer ? @input[:source_file] : importer) : nil, accept: @input[:content_types].last, npm: true)
377
+ code = String.new
378
+ code_imports = [];
379
+ importees.each_with_index.map do |f, i|
380
+ if f.has_default_export?
381
+ code << "import _#{i} from '#{f.source_file}';\n"
382
+ code_imports << "_#{i}"
383
+ elsif f.has_exports?
384
+ code << "import * as _#{i} from '#{f.source_file}';\n"
385
+ code_imports << "_#{i}"
386
+ else
387
+ code << "import '#{f.source_file}';\n"
388
+ end
389
+ end
238
390
 
239
- code += "export default [#{code_imports.join(', ')}];"
391
+ code += "export default [#{code_imports.join(', ')}];"
240
392
 
241
- { code: code, map: nil }
242
- else
243
- asset = @environment.find(importee, accept: @input[:content_types].last)
244
- if asset
245
- { code: asset.source, map: asset.sourcemap }
393
+ { code: code, map: nil }
246
394
  else
247
- nil
395
+ asset = @environment.find(importee, accept: @input[:content_types].last)
396
+ if asset
397
+ { code: asset.source, map: asset.sourcemap }
398
+ else
399
+ nil
400
+ end
248
401
  end
249
- end
250
- when 'error'
251
- io.write(JSON.generate({rid: message['rid'], return: nil}))
402
+ when 'error'
403
+ io.write(JSON.generate({rid: message['rid'], return: nil}))
252
404
 
253
- case message['args'][0]
254
- when 'AssetNotFound'
255
- error_message = "Could not find import \"#{message['args'][1]}\" for \"#{message['args'][2]}\".\n\n"
256
- error_message << build_tree(message['args'][3], input, message['args'][2])
257
- raise exec_runtime_error(error_message)
258
- else
259
- raise exec_runtime_error(message['args'][0] + ': ' + message['args'][1])
405
+ case message['args'][0]
406
+ when 'AssetNotFound'
407
+ error_message = "Could not find import \"#{message['args'][1]}\" for \"#{message['args'][2]}\".\n\n"
408
+ error_message << build_tree(message['args'][3], input, message['args'][2])
409
+ raise exec_runtime_error(error_message)
410
+ else
411
+ raise exec_runtime_error(message['args'][0] + ': ' + message['args'][1])
412
+ end
413
+ # when 'set_cache'
414
+ # @environment.cache.set('rollup', message['args'][0])
415
+ # io.write(JSON.generate({rid: message['rid'], return: true}))
416
+ # when 'get_cache'
417
+ # io.write(JSON.generate({rid: message['rid'], return: [(@environment.cache.get('rollup') || '{}')] }))
260
418
  end
261
- # when 'set_cache'
262
- # @environment.cache.set('rollup', message['args'][0])
263
- # io.write(JSON.generate({rid: message['rid'], return: true}))
264
- # when 'get_cache'
265
- # io.write(JSON.generate({rid: message['rid'], return: [(@environment.cache.get('rollup') || '{}')] }))
419
+ # puts JSON.generate({rid: message['rid'], return: ret})
420
+ io.write(JSON.generate({rid: message['rid'], return: ret}))
266
421
  end
267
- io.write(JSON.generate({rid: message['rid'], return: ret}))
268
422
  end
423
+ rescue Errno::EPIPE, EOFError
269
424
  end
270
- rescue Errno::EPIPE, EOFError
271
- end
272
425
 
273
- io.close
274
- if $?.success?
275
- true
276
- else
277
- raise exec_runtime_error(buffer)
426
+ io.close
427
+ if $?.success?
428
+ true
429
+ else
430
+ raise exec_runtime_error(buffer)
431
+ end
278
432
  end
279
- end
280
433
 
281
- def build_tree(renderStack, from, to, visited: nil)
282
- visited ||= []
283
- return if visited.include?(from)
284
- visited << from
434
+ def build_tree(renderStack, from, to, visited: nil)
435
+ visited ||= []
436
+ return if visited.include?(from)
437
+ visited << from
285
438
 
286
- if renderStack[from].nil? || renderStack[from].empty?
287
- nil
288
- elsif renderStack[from].include?(to)
289
- from
290
- else
291
- renderStack[from].each do |dep|
292
- if tree = build_tree(renderStack, dep, to, visited: visited)
293
- return "#{from}\n└ #{tree.lines.each_with_index.map{|l, i| "#{i == 0 ? '' : ' '}#{l}"}.join("")}"
439
+ if renderStack[from].nil? || renderStack[from].empty?
440
+ nil
441
+ elsif renderStack[from].include?(to)
442
+ from
443
+ else
444
+ renderStack[from].each do |dep|
445
+ if tree = build_tree(renderStack, dep, to, visited: visited)
446
+ return "#{from}\n└ #{tree.lines.each_with_index.map{|l, i| "#{i == 0 ? '' : ' '}#{l}"}.join("")}"
447
+ end
294
448
  end
449
+ nil
295
450
  end
296
- nil
297
451
  end
298
452
  end
299
-
300
453
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Condenser
2
4
  module Resolve
3
5
 
@@ -12,8 +14,14 @@ class Condenser
12
14
  @build_cache = BuildCache.new(path, logger: logger)
13
15
  end
14
16
 
15
- def resolve(filename, base=nil, accept: nil)
16
- filename = filename.delete_prefix("/") if path.none? { |p| filename.start_with?(p) }
17
+ def resolve(filename, base=nil, accept: nil, npm: false)
18
+ search_path = if npm
19
+ path + [File.join(npm_path, 'node_modules')]
20
+ else
21
+ path
22
+ end
23
+
24
+ filename = filename.delete_prefix("/") if search_path.none? { |p| filename.start_with?(p) }
17
25
  dirname, basename, extensions, mime_types = decompose_path(filename, base)
18
26
  accept ||= mime_types.empty? ? ['*/*'] : mime_types
19
27
  accept = Array(accept)
@@ -26,7 +34,7 @@ class Condenser
26
34
  results = []
27
35
 
28
36
  paths = if dirname&.start_with?('/')
29
- if pat = path.find { |pa| dirname.start_with?(pa) }
37
+ if pat = search_path.find { |pa| dirname.start_with?(pa) }
30
38
  dirname.delete_prefix!(pat)
31
39
  dirname.delete_prefix!('/')
32
40
  [pat]
@@ -34,7 +42,7 @@ class Condenser
34
42
  []
35
43
  end
36
44
  else
37
- path
45
+ search_path
38
46
  end
39
47
 
40
48
  paths.each do |path|
@@ -48,7 +56,7 @@ class Condenser
48
56
  if (basename == '*' || basename == f_basename)
49
57
  if accept == ['*/*'] || mime_type_match_accept?(f_mime_types, accept)
50
58
  asset_dir = f_dirname.delete_prefix(path).delete_prefix('/')
51
- asset_basename = f_basename + f_extensions.join('')
59
+ asset_basename = f_basename + f_extensions&.join('').to_s
52
60
  asset_filename = asset_dir.empty? ? asset_basename : File.join(asset_dir, asset_basename)
53
61
  results << build_cache.map("#{asset_filename}@#{f_mime_types.join('')}") do
54
62
  Asset.new(self, {
@@ -105,9 +113,9 @@ class Condenser
105
113
  end
106
114
  end
107
115
 
108
- def find(filename, base=nil, accept: nil)
116
+ def find(filename, base=nil, **kargs)
109
117
  build do
110
- resolve(filename, base, accept: accept).first
118
+ resolve(filename, base, **kargs).first
111
119
  end
112
120
  end
113
121
 
@@ -148,11 +156,34 @@ class Condenser
148
156
  end
149
157
  end
150
158
 
159
+ def has_dir?(path)
160
+ path.count('/') > (path.start_with?('/') ? 1 : 0)
161
+ end
162
+
163
+ def expand_path(path)
164
+ dir = if path.start_with?('/')
165
+ File.expand_path(path)
166
+ else
167
+ File.expand_path("/#{path}").delete_prefix('/')
168
+ end
169
+ dir.empty? ? nil : dir
170
+ end
171
+
151
172
  def decompose_path(path, base=nil)
152
- dirname = path.index('/') ? File.dirname(path) : nil
153
- if base && path&.start_with?('.')
154
- dirname = File.expand_path(dirname, base)
173
+ dirname = if base && path.start_with?('.')
174
+ if has_dir?(base)
175
+ if has_dir?(path)
176
+ expand_path(File.join(File.dirname(base), File.dirname(path)))
177
+ else
178
+ expand_path(File.dirname(base))
179
+ end
180
+ else
181
+ expand_path(File.dirname(path))
182
+ end
183
+ else
184
+ path.index('/') ? File.dirname(path) : nil
155
185
  end
186
+
156
187
 
157
188
  _, star, basename, extensions = path.match(/(([^\.\/]+)(\.[^\/]+)|\*|[^\/]+)$/).to_a
158
189
  if extensions == '.*'