klenod-build 0.0.1

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +29 -0
  3. data/lib/klenod/build/asset.rb +235 -0
  4. data/lib/klenod/build/asset_compression.rb +58 -0
  5. data/lib/klenod/build/asset_generation_queue.rb +50 -0
  6. data/lib/klenod/build/cli/application.rb +108 -0
  7. data/lib/klenod/build/cli.rb +3 -0
  8. data/lib/klenod/build/config.rb +142 -0
  9. data/lib/klenod/build/context.rb +370 -0
  10. data/lib/klenod/build/dependency.rb +21 -0
  11. data/lib/klenod/build/errors.rb +175 -0
  12. data/lib/klenod/build/filesystem_resolver.rb +147 -0
  13. data/lib/klenod/build/graph/invalidator.rb +246 -0
  14. data/lib/klenod/build/graph.rb +864 -0
  15. data/lib/klenod/build/graphviz.rb +282 -0
  16. data/lib/klenod/build/hashing.rb +21 -0
  17. data/lib/klenod/build/invalidation_result.rb +55 -0
  18. data/lib/klenod/build/load_result.rb +7 -0
  19. data/lib/klenod/build/loaded_module.rb +38 -0
  20. data/lib/klenod/build/module_id.rb +100 -0
  21. data/lib/klenod/build/module_record.rb +22 -0
  22. data/lib/klenod/build/plugin.rb +35 -0
  23. data/lib/klenod/build/plugins/class_names_runtime.rb +125 -0
  24. data/lib/klenod/build/plugins/component_defaults.rb +12 -0
  25. data/lib/klenod/build/plugins/data_plugin.rb +117 -0
  26. data/lib/klenod/build/plugins/gem_import_plugin.rb +125 -0
  27. data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.json +17687 -0
  28. data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.rb +105 -0
  29. data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.txt +31 -0
  30. data/lib/klenod/build/plugins/google_fonts_plugin.rb +387 -0
  31. data/lib/klenod/build/plugins/haml_plugin/companions.rb +40 -0
  32. data/lib/klenod/build/plugins/haml_plugin/errors.rb +114 -0
  33. data/lib/klenod/build/plugins/haml_plugin/helper_source.rb +123 -0
  34. data/lib/klenod/build/plugins/haml_plugin/parser.rb +85 -0
  35. data/lib/klenod/build/plugins/haml_plugin/transformer/ruby_builder.rb +1039 -0
  36. data/lib/klenod/build/plugins/haml_plugin/transformer.rb +766 -0
  37. data/lib/klenod/build/plugins/haml_plugin.rb +369 -0
  38. data/lib/klenod/build/plugins/image_plugin.rb +401 -0
  39. data/lib/klenod/build/plugins/intl_plugin.rb +34 -0
  40. data/lib/klenod/build/plugins/markdown_compiler.rb +180 -0
  41. data/lib/klenod/build/plugins/markdown_plugin.rb +161 -0
  42. data/lib/klenod/build/plugins/router_plugin.rb +942 -0
  43. data/lib/klenod/build/plugins/ruby_plugin.rb +34 -0
  44. data/lib/klenod/build/plugins/svg_plugin.rb +197 -0
  45. data/lib/klenod/build/plugins.rb +22 -0
  46. data/lib/klenod/build/profiler.rb +79 -0
  47. data/lib/klenod/build/resolution_error_formatter.rb +42 -0
  48. data/lib/klenod/build/resolver.rb +95 -0
  49. data/lib/klenod/build/ruby_import_rewriter.rb +436 -0
  50. data/lib/klenod/build/source_map/editor.rb +110 -0
  51. data/lib/klenod/build/source_map/map.rb +168 -0
  52. data/lib/klenod/build/source_map/vlq.rb +68 -0
  53. data/lib/klenod/build/source_map.rb +12 -0
  54. data/lib/klenod/build/transform_result.rb +12 -0
  55. data/lib/klenod/build/version.rb +7 -0
  56. data/lib/klenod/build/watched_pattern.rb +11 -0
  57. data/lib/klenod/build/watcher.rb +141 -0
  58. data/lib/klenod/build.rb +15 -0
  59. metadata +310 -0
@@ -0,0 +1,369 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree"
4
+
5
+ require_relative "../plugin"
6
+ require_relative "../dependency"
7
+ require_relative "../module_id"
8
+ require_relative "../ruby_import_rewriter"
9
+ require_relative "../transform_result"
10
+ require_relative "../watched_pattern"
11
+ require_relative "intl_plugin"
12
+ require_relative "markdown_compiler"
13
+ require_relative "class_names_runtime"
14
+ require_relative "component_defaults"
15
+ require_relative "haml_plugin/errors"
16
+ require_relative "haml_plugin/parser"
17
+ require_relative "haml_plugin/transformer"
18
+ require_relative "haml_plugin/helper_source"
19
+ require_relative "haml_plugin/companions"
20
+
21
+ module Klenod
22
+ module Build
23
+ module Plugins
24
+ module HamlPlugin
25
+ def self.new(...)
26
+ Plugin.new(...)
27
+ end
28
+
29
+ DEFAULT_COMPONENT_BASE_CLASS = ComponentDefaults::DEFAULT_COMPONENT_BASE_CLASS
30
+ DEFAULT_FACTORY = ComponentDefaults::DEFAULT_FACTORY
31
+ HAML_HELPER_SPECIFIER = "virtual:klenod/haml_helper"
32
+ HAML_HELPER_MODULE_ID = ModuleId.new("#{HAML_HELPER_SPECIFIER}.rb", nil)
33
+ STATIC_CLASS_SOURCE_PATTERN = /^[ \t]*(?:%[-:\w]+)?(?:#[\w-]+)?\.[\w-]|[({][^)}\n]*\bclass\s*=/m
34
+ end
35
+
36
+ module HamlPlugin
37
+ class Plugin < Klenod::Build::Plugin
38
+ ParseError = HamlPlugin::ParseError
39
+ RubyParseError = HamlPlugin::RubyParseError
40
+ HamlTransformResult = HamlPlugin::HamlTransformResult
41
+ Transformer = HamlPlugin::Transformer
42
+ ParserWithMetadata = HamlPlugin::ParserWithMetadata
43
+
44
+ DEFAULT_COMPONENT_BASE_CLASS = HamlPlugin::DEFAULT_COMPONENT_BASE_CLASS
45
+ DEFAULT_FACTORY = HamlPlugin::DEFAULT_FACTORY
46
+ HAML_HELPER_SPECIFIER = HamlPlugin::HAML_HELPER_SPECIFIER
47
+ HAML_HELPER_MODULE_ID = HamlPlugin::HAML_HELPER_MODULE_ID
48
+ STATIC_CLASS_SOURCE_PATTERN = HamlPlugin::STATIC_CLASS_SOURCE_PATTERN
49
+
50
+ def self.parse_haml(...)
51
+ HamlPlugin.parse_haml(...)
52
+ end
53
+
54
+ include ClassNamesRuntime
55
+ include HelperSource
56
+ include Companions
57
+
58
+ def initialize(
59
+ component_base_class: DEFAULT_COMPONENT_BASE_CLASS,
60
+ factory: DEFAULT_FACTORY,
61
+ global_variables: nil,
62
+ i18n_class: nil,
63
+ i18n_constant: nil,
64
+ cache_static_subtrees: false
65
+ )
66
+ @component_base_class = component_base_class
67
+ @factory = factory
68
+ @global_variables = validate_global_variables(global_variables)
69
+ @i18n_class, @i18n_constant = validate_i18n_options(i18n_class, i18n_constant)
70
+ @cache_static_subtrees = cache_static_subtrees
71
+ @transformer = Transformer.new
72
+ end
73
+
74
+ def resolve(dependency, _context)
75
+ styles_dependency = resolve_class_names_runtime(dependency)
76
+ return styles_dependency if styles_dependency
77
+
78
+ return nil unless dependency.specifier == HAML_HELPER_SPECIFIER
79
+
80
+ ResolvedDependency.new(dependency, HAML_HELPER_MODULE_ID, {virtual: true})
81
+ end
82
+
83
+ def load(module_id, _context)
84
+ styles_load = load_class_names_runtime(module_id)
85
+ return styles_load if styles_load
86
+
87
+ return nil unless module_id.scheme == :virtual && module_id == HAML_HELPER_MODULE_ID
88
+
89
+ LoadResult.new(haml_helper_source, nil, TransformResult.new(haml_helper_source, [], nil, [], [], {}))
90
+ end
91
+
92
+ def transform(module_id, code, context)
93
+ return super unless module_id.extname == ".haml"
94
+
95
+ companion_css = companion_path(module_id, ".css")
96
+ dependencies = []
97
+ style_dependencies = []
98
+ import_dependencies = []
99
+ watched_patterns = []
100
+ markdown_filters = markdown_filter_nodes(code, module_id: module_id)
101
+ builder = Transformer::RubyBuilder.new(profiler: context.profiler)
102
+ context.unregister_virtual_modules(module_id)
103
+
104
+ if context.absolute_path(companion_css).file? && css_plugin_available?(context)
105
+ dependency =
106
+ Dependency
107
+ .create(
108
+ specifier: "./#{File.basename(companion_css.path)}",
109
+ importer_id: module_id,
110
+ kind: :companion_style,
111
+ metadata: {optional: true}
112
+ )
113
+ .with(id: "#{module_id}:companion_style")
114
+ dependencies << dependency
115
+ style_dependencies << dependency
116
+ end
117
+ markdown_components_dependency = nil
118
+ if markdown_filters.any?
119
+ markdown_components_id = ModuleId.new("markdown-components.rb", nil)
120
+ watched_patterns << WatchedPattern.new(module_id, markdown_components_id.path, :markdown_components, {})
121
+ if context.absolute_path(markdown_components_id).file?
122
+ markdown_components_dependency =
123
+ Dependency
124
+ .create(
125
+ specifier: "/markdown-components",
126
+ importer_id: module_id,
127
+ kind: :markdown_components,
128
+ metadata: {optional: true}
129
+ )
130
+ .with(id: "#{module_id}:markdown_components")
131
+ dependencies << markdown_components_dependency
132
+ end
133
+ end
134
+ inline_css_sources(code, module_id: module_id).each_with_index do |source, index|
135
+ virtual_module_id = ModuleId.new("#{module_id.path}.inline.#{index}.css", nil)
136
+ context.register_virtual_module(
137
+ virtual_module_id,
138
+ source.text,
139
+ owner_id: module_id,
140
+ metadata: {
141
+ inline_css_origin: {
142
+ module_id: module_id,
143
+ source: code,
144
+ line_offset: source.line_offset,
145
+ column_offset: source.column_offset
146
+ }
147
+ }
148
+ )
149
+ dependency =
150
+ Dependency
151
+ .create(
152
+ specifier: virtual_module_id.to_s,
153
+ importer_id: module_id,
154
+ kind: :inline_style,
155
+ metadata: {virtual_module_id: virtual_module_id}
156
+ )
157
+ .with(id: "#{module_id}:inline_style:#{index}")
158
+ dependencies << dependency
159
+ style_dependencies << dependency
160
+ end
161
+ class_names_runtime_dependency = class_names_runtime_dependency(module_id)
162
+ dependencies << class_names_runtime_dependency
163
+ styles_source = styles_source_for(builder, style_dependencies, class_names_runtime_dependency: class_names_runtime_dependency)
164
+ haml_helper_needed = @cache_static_subtrees || haml_helper_needed?(code, styleable: !style_dependencies.empty?)
165
+ haml_helper_dependency = haml_helper_dependency(module_id) if haml_helper_needed
166
+ dependencies << haml_helper_dependency if haml_helper_dependency
167
+ translations_source = builder.frozen_literal(translations_for(context, module_id)).source
168
+ component_class_name = component_class_name(module_id)
169
+ import_rewriter =
170
+ lambda do |source, source_line_offset: 0, source_column_offset: 0|
171
+ result =
172
+ RubyImportRewriter
173
+ .new(
174
+ module_id: module_id,
175
+ kind: :haml_import,
176
+ source_dir: context.source_dir,
177
+ profiler: context.profiler,
178
+ dependency_id_offset: import_dependencies.length,
179
+ source_line_offset: source_line_offset,
180
+ source_column_offset: source_column_offset
181
+ )
182
+ .rewrite(source)
183
+ import_dependencies.concat(result.dependencies)
184
+ watched_patterns.concat(result.watched_patterns)
185
+ result.code
186
+ end
187
+ haml_result =
188
+ @transformer.call(
189
+ source: code,
190
+ module_id: module_id,
191
+ component_class_name: component_class_name,
192
+ component_base_class: @component_base_class,
193
+ factory: @factory,
194
+ styles_source: styles_source,
195
+ translations_source: translations_source,
196
+ i18n_source: i18n_source_for(builder),
197
+ haml_helper_source: haml_helper_dependency && builder.constant_assignment("HamlHelper", "#{builder.import_call(haml_helper_dependency.id).source}::Default"),
198
+ styleable: !style_dependencies.empty?,
199
+ profiler: context.profiler,
200
+ import_rewriter: import_rewriter,
201
+ markdown_components_source: markdown_components_dependency ? "__klenod_import__(#{markdown_components_dependency.id.inspect})::Default" : "{}",
202
+ global_variables: @global_variables,
203
+ cache_static_subtrees: @cache_static_subtrees
204
+ )
205
+ import_rewrite =
206
+ if !haml_result.code.include?("import(") && !haml_result.code.include?("lazy_import(") && !haml_result.code.include?("import_glob(")
207
+ RubyImportRewriter::Result.new(haml_result.code, [], [])
208
+ else
209
+ context.profiler.measure(:haml_import_rewrite, module_id: module_id.to_s) do
210
+ RubyImportRewriter
211
+ .new(
212
+ module_id: module_id,
213
+ kind: :haml_import,
214
+ source_dir: context.source_dir,
215
+ profiler: context.profiler,
216
+ dependency_id_offset: import_dependencies.length
217
+ )
218
+ .rewrite(haml_result.code)
219
+ end
220
+ end
221
+
222
+ watched_patterns.concat(import_rewrite.watched_patterns)
223
+
224
+ TransformResult.new(
225
+ import_rewrite.code,
226
+ dependencies + import_dependencies + import_rewrite.dependencies,
227
+ haml_result.source_map,
228
+ [],
229
+ companion_patterns(module_id) + watched_patterns,
230
+ haml_result.metadata
231
+ )
232
+ end
233
+
234
+ def import_value(resolved_dependency, record, context)
235
+ styles_import = class_names_runtime_import_value(resolved_dependency, record, context)
236
+ return styles_import if styles_import
237
+ return nil unless record.id.extname == ".haml"
238
+
239
+ context.mods.fetch(record.id).const_get(:Exports)::Default
240
+ end
241
+
242
+ def runtime_import_value(resolved_dependency, record, _context)
243
+ styles_import = class_names_runtime_runtime_import_value(resolved_dependency, record)
244
+ return styles_import if styles_import
245
+ return Runtime::DefaultImport.new(:Default) if record.id.extname == ".haml"
246
+
247
+ super
248
+ end
249
+
250
+ def invalidate_module_ids(paths, context)
251
+ paths
252
+ .filter_map { |path| companion_owner_module_id(path, context) }
253
+ .uniq
254
+ end
255
+
256
+ private
257
+
258
+ InlineCssSource = Data.define(:text, :line_offset, :column_offset)
259
+
260
+ def validate_global_variables(global_variables)
261
+ return nil if global_variables.nil?
262
+
263
+ source = global_variables.to_s
264
+ parsed = SyntaxTree.parse(source)&.statements&.body
265
+ raise ArgumentError, "global_variables must be a Ruby expression" unless parsed&.length == 1
266
+
267
+ source
268
+ rescue SyntaxTree::Parser::ParseError
269
+ raise ArgumentError, "global_variables must be a Ruby expression"
270
+ end
271
+
272
+ def validate_i18n_options(i18n_class, i18n_constant)
273
+ return [nil, nil] if i18n_class.nil? && i18n_constant.nil?
274
+ raise ArgumentError, "i18n_class must be configured when i18n_constant is configured" if i18n_class.nil?
275
+
276
+ i18n_constant = (i18n_constant || "I18n").to_s
277
+ raise ArgumentError, "i18n_constant must be a Ruby constant name" unless i18n_constant.match?(/\A[A-Z]\w*\z/)
278
+
279
+ [
280
+ Transformer::ConstPath.parse(i18n_class, name: "i18n_class").to_s,
281
+ i18n_constant
282
+ ]
283
+ end
284
+
285
+ def i18n_source_for(builder)
286
+ return nil unless @i18n_class
287
+
288
+ builder.constant_assignment(@i18n_constant, "#{@i18n_class}.new(self)")
289
+ end
290
+
291
+ def translations_for(context, module_id)
292
+ intl_plugin = context.plugins.find { |plugin| plugin.respond_to?(:translations_for) }
293
+ intl_plugin ? intl_plugin.translations_for(context, module_id) : {}
294
+ end
295
+
296
+ def component_class_name(module_id)
297
+ basename = File.basename(module_id.path, ".haml")
298
+ classified =
299
+ basename
300
+ .split(/[^A-Za-z0-9]+/)
301
+ .reject(&:empty?)
302
+ .map { |part| part[0].upcase + part[1..] }
303
+ .join
304
+
305
+ classified.empty? ? "Component" : classified
306
+ end
307
+
308
+ def inline_css_sources(source, module_id: nil)
309
+ lines = source.lines
310
+ filter_nodes(source, "css", module_id: module_id).map do |node|
311
+ text = node.value.fetch(:text)
312
+ line_offset = node.line
313
+ column_offset = inline_filter_column_offset(lines, line_offset, text)
314
+
315
+ InlineCssSource.new(text, line_offset, column_offset)
316
+ end
317
+ end
318
+
319
+ def inline_filter_column_offset(lines, line_offset, text)
320
+ text.lines.each_with_index do |text_line, index|
321
+ next if text_line.strip.empty?
322
+
323
+ source_line = lines.fetch(line_offset + index, "")
324
+ return source_line.length - source_line.lstrip.length
325
+ end
326
+
327
+ 0
328
+ end
329
+
330
+ def markdown_filter_nodes(source, module_id: nil)
331
+ return [] unless source.include?(":markdown")
332
+
333
+ filter_nodes(source, "markdown", module_id: module_id)
334
+ end
335
+
336
+ def filter_nodes(source, name, module_id: nil)
337
+ nodes = []
338
+ queue = HamlPlugin.parse_haml(source, module_id: module_id).children.dup
339
+
340
+ until queue.empty?
341
+ node = queue.shift
342
+ nodes << node if node.type == :filter && node.value.fetch(:name) == name
343
+ queue.concat(node.children)
344
+ end
345
+
346
+ nodes
347
+ end
348
+
349
+ def styles_source_for(builder, dependencies, class_names_runtime_dependency:)
350
+ class_names_runtime = builder.import_call(class_names_runtime_dependency.id).source
351
+ imports = dependencies.map { |dependency| builder.import_call(dependency.id) }
352
+ return "#{class_names_runtime}.new({}.freeze)" if imports.empty?
353
+ return imports.fetch(0).source if imports.length == 1
354
+
355
+ builder
356
+ .expression(
357
+ "#{class_names_runtime}.merge(#{imports.map(&:source).join(", ")})"
358
+ )
359
+ .source
360
+ end
361
+
362
+ def css_plugin_available?(context)
363
+ context.plugins.any? { it.respond_to?(:javascript_stylesheet_module?, true) }
364
+ end
365
+ end
366
+ end
367
+ end
368
+ end
369
+ end