klenod-plugin-css 0.0.1-aarch64-linux-gnu

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 52c223a2158bddb792f3fa5a63fe94d5807ef46aa2349b5aa2ed4852210ba969
4
+ data.tar.gz: 86832bab45585e3df707d759a1f54fd682551e89ae9b33d5c169e0d82b3cdafc
5
+ SHA512:
6
+ metadata.gz: 745246e7362eff233aa036a8a7e28b68a5d5f95d8bd6b5755dc3fa54cf78b0d15811e213f52ea35974299b110721d51a87c2a3043154125d9c545c0b197eddaf
7
+ data.tar.gz: d0f9421b070945252bd45e1f94b13b42405683869aed5540a5242aad395b126446128369721c4cf73c41982d78f52ad6b8be87a1929f4ad92b83802e7cd8150f
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # klenod-plugin-css
2
+
3
+ CSS asset and CSS Modules plugin for
4
+ [Klenod](https://github.com/aalin/klenod), powered by
5
+ [Lightning CSS](https://lightningcss.dev/).
6
+
7
+ ## Options
8
+
9
+ ```ruby
10
+ require "klenod/plugin/css"
11
+
12
+ Klenod::Build::Plugins::CSSPlugin.new(
13
+ source_maps: :development,
14
+ minify: false,
15
+ class_pattern: "[component].[local]?[hash]",
16
+ tag_pattern: "[component]_[local]?[hash]",
17
+ local_css_variables: false,
18
+ variable_pattern: "[component]-[local]?[hash]"
19
+ )
20
+ ```
21
+
22
+ - `source_maps:` accepts `false`, `true`, or `:development` (the default).
23
+ - `minify:` also minifies in development when `true`; build output is always
24
+ minified.
25
+ - `class_pattern:` and `tag_pattern:` control generated selector names.
26
+ - `local_css_variables:` enables CSS Modules dashed identifiers.
27
+ - `variable_pattern:` controls generated local variable names; the leading
28
+ `--` is added automatically.
29
+
30
+ ## Local CSS variables
31
+
32
+ CSS custom properties remain global by default. When
33
+ `local_css_variables: true`, declarations and ordinary references are scoped.
34
+ References can explicitly come from another CSS module or remain global:
35
+
36
+ ```css
37
+ .button {
38
+ background: var(--accent-color from "./vars.css");
39
+ color: var(--text-color from global);
40
+ }
41
+ ```
@@ -0,0 +1,581 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "klenod/plugin/css/transformer"
4
+ require "uri"
5
+
6
+ require "klenod/build/asset"
7
+ require "klenod/build/dependency"
8
+ require "klenod/build/errors"
9
+ require "klenod/build/filesystem_resolver"
10
+ require "klenod/build/hashing"
11
+ require "klenod/build/plugin"
12
+ require "klenod/build/source_map"
13
+ require "klenod/build/transform_result"
14
+ require "klenod/build/plugins/class_names_runtime"
15
+
16
+ module Klenod
17
+ module Build
18
+ module Plugins
19
+ module CSSPlugin
20
+ def self.new(...)
21
+ Plugin.new(...)
22
+ end
23
+
24
+ class Plugin < Klenod::Build::Plugin
25
+ include ClassNamesRuntime
26
+
27
+ CSS_DEPENDENCY_TYPES = {
28
+ ImportDependency => :css_import,
29
+ UrlDependency => :asset_url
30
+ }.freeze
31
+
32
+ JAVASCRIPT_STYLESHEET_QUERY = "javascript"
33
+ VALID_SOURCE_MAP_MODES = [false, true, :development].freeze
34
+
35
+ def initialize(
36
+ source_maps: :development,
37
+ minify: false,
38
+ class_pattern: "[component].[local]?[hash]",
39
+ tag_pattern: "[component]_[local]?[hash]",
40
+ local_css_variables: false,
41
+ variable_pattern: "[component]-[local]?[hash]"
42
+ )
43
+ unless VALID_SOURCE_MAP_MODES.include?(source_maps)
44
+ raise ArgumentError, "source_maps must be false, true, or :development"
45
+ end
46
+
47
+ @source_maps = source_maps
48
+ @minify = minify
49
+ @class_pattern = class_pattern
50
+ @tag_pattern = tag_pattern
51
+ @local_css_variables = local_css_variables
52
+ @variable_pattern = variable_pattern
53
+ end
54
+
55
+ def resolve(dependency, context)
56
+ resolve_class_names_runtime(dependency) || resolve_javascript_stylesheet_dependency(dependency, context)
57
+ end
58
+
59
+ def load(module_id, _context)
60
+ load_class_names_runtime(module_id)
61
+ end
62
+
63
+ def transform(module_id, code, context)
64
+ return super unless module_id.extname == ".css"
65
+
66
+ if javascript_stylesheet_module?(module_id)
67
+ javascript_result = transform_css(module_id, code, transform_names: false, context: context)
68
+ css_dependencies = build_dependencies(module_id, javascript_result.dependencies, context)
69
+
70
+ return Klenod::Build::TransformResult.new(
71
+ "Default = nil\n",
72
+ css_dependencies.dependencies,
73
+ nil,
74
+ [],
75
+ [],
76
+ {
77
+ css_javascript_result: javascript_result,
78
+ css_javascript_only: true,
79
+ external_dependencies: css_dependencies.external_dependencies
80
+ }
81
+ )
82
+ end
83
+
84
+ scoped_result = transform_css(module_id, code, transform_names: true, context: context)
85
+ css_dependencies = build_dependencies(module_id, scoped_result.dependencies, context)
86
+ compose_dependencies = build_compose_dependencies(module_id, scoped_result)
87
+ variable_dependencies = build_variable_dependencies(module_id, scoped_result)
88
+ styles_dependency = class_names_runtime_dependency(module_id)
89
+ selectors = css_selectors(scoped_result)
90
+
91
+ Klenod::Build::TransformResult.new(
92
+ ruby_module_source(selectors, nil, styles_dependency: styles_dependency),
93
+ [styles_dependency, *css_dependencies.dependencies, *compose_dependencies, *variable_dependencies],
94
+ nil,
95
+ [],
96
+ [],
97
+ {
98
+ css_result: scoped_result,
99
+ css_classes: selectors,
100
+ css_variables: scoped_result.serialized_variables,
101
+ external_dependencies: css_dependencies.external_dependencies
102
+ }
103
+ )
104
+ end
105
+
106
+ def finalize(module_id, result, resolved_dependencies, dependency_records, context)
107
+ css_result = result.metadata[:css_result]
108
+ javascript_only = result.metadata[:css_javascript_only]
109
+ return result unless css_result || javascript_only
110
+
111
+ if javascript_only
112
+ javascript_css_edit = finalized_css_edit(
113
+ result.metadata.fetch(:css_javascript_result),
114
+ resolved_dependencies,
115
+ dependency_records,
116
+ result.metadata.fetch(:external_dependencies),
117
+ import_asset_type: :css_javascript_stylesheet
118
+ )
119
+ javascript_asset, javascript_source_map_asset = css_asset_pair(module_id, javascript_css_edit, :css_javascript_stylesheet, {}, context)
120
+
121
+ return result.with(
122
+ code: "Default = #{javascript_asset.output_path.inspect}\n",
123
+ assets: [javascript_asset, javascript_source_map_asset, *result.assets].compact,
124
+ metadata: result.metadata.merge(
125
+ css_javascript_stylesheet_path: javascript_asset.output_path
126
+ )
127
+ )
128
+ end
129
+
130
+ css_edit = finalized_css_edit(css_result, resolved_dependencies, dependency_records, result.metadata.fetch(:external_dependencies))
131
+ classes = finalized_css_selectors(css_result, resolved_dependencies, dependency_records)
132
+ asset, source_map_asset = css_asset_pair(
133
+ module_id,
134
+ css_edit,
135
+ :css,
136
+ {classes: classes, variables: css_result.variables},
137
+ context
138
+ )
139
+
140
+ result.with(
141
+ code: ruby_module_source(classes, asset.output_path, styles_dependency: result.dependencies.fetch(0)),
142
+ assets: [asset, source_map_asset, *result.assets].compact,
143
+ metadata: result.metadata.merge(
144
+ css_asset_path: asset.output_path,
145
+ css_classes: classes
146
+ )
147
+ )
148
+ end
149
+
150
+ def import_value(resolved_dependency, record, context)
151
+ styles_import = class_names_runtime_import_value(resolved_dependency, record, context)
152
+ return styles_import if styles_import
153
+ return nil unless record.id.extname == ".css"
154
+
155
+ context.mods.fetch(record.id).const_get(:Exports)::Default
156
+ end
157
+
158
+ def runtime_import_value(resolved_dependency, record, _context)
159
+ styles_import = class_names_runtime_runtime_import_value(resolved_dependency, record)
160
+ return styles_import if styles_import
161
+ return Runtime::DefaultImport.new(:Default) if record.id.extname == ".css"
162
+
163
+ super
164
+ end
165
+
166
+ private
167
+
168
+ CssDependencies = ::Data.define(:dependencies, :external_dependencies)
169
+ CssEdit = ::Data.define(:code, :source_map)
170
+
171
+ def resolve_javascript_stylesheet_dependency(dependency, context)
172
+ return nil unless javascript_stylesheet_dependency?(dependency)
173
+ return nil if external_url?(dependency.specifier)
174
+
175
+ base_module_id =
176
+ if dependency.importer_id
177
+ dependency.importer_id.merge(dependency.specifier)
178
+ else
179
+ ModuleId.new("app:/#{dependency.specifier.to_s.delete_prefix("/")}")
180
+ end
181
+ return nil unless base_module_id.scheme == :app && base_module_id.extname == ".css"
182
+
183
+ path = FilesystemResolver.new(root: context.source_dir).resolve(base_module_id.relative_path)
184
+ relative_path = path.relative_path_from(context.source_dir).to_s
185
+ module_id = ModuleId.new("app:/#{relative_path}", JAVASCRIPT_STYLESHEET_QUERY)
186
+
187
+ ResolvedDependency.new(dependency, module_id, {})
188
+ rescue ResolveError => error
189
+ raise error.with_path_prefix("app:/")
190
+ end
191
+
192
+ def javascript_stylesheet_dependency?(dependency)
193
+ dependency.kind == :javascript_import ||
194
+ (dependency.kind == :css_import && dependency.importer_id&.query == JAVASCRIPT_STYLESHEET_QUERY)
195
+ end
196
+
197
+ def javascript_stylesheet_module?(module_id)
198
+ module_id.query == JAVASCRIPT_STYLESHEET_QUERY
199
+ end
200
+
201
+ def transform_css(module_id, code, transform_names:, context:)
202
+ Transformer.transform(
203
+ module_id.path,
204
+ code,
205
+ minify: minify_enabled?(context),
206
+ transform_names: transform_names,
207
+ class_pattern: @class_pattern,
208
+ tag_pattern: @tag_pattern,
209
+ local_css_variables: @local_css_variables && transform_names,
210
+ variable_pattern: @variable_pattern
211
+ )
212
+ end
213
+
214
+ def minify_enabled?(context)
215
+ context.mode == :build || @minify
216
+ end
217
+
218
+ def build_dependencies(module_id, dependencies, context)
219
+ external_dependencies = {}
220
+ resolved_dependencies =
221
+ dependencies.each_with_index.filter_map do |dependency, index|
222
+ resolved_dependency =
223
+ Dependency
224
+ .create(
225
+ specifier: dependency.url,
226
+ importer_id: module_id,
227
+ kind: CSS_DEPENDENCY_TYPES.fetch(dependency.class),
228
+ loc: SourceLocation.new(
229
+ module_id.to_s,
230
+ dependency.loc.start.line,
231
+ dependency.loc.start.column
232
+ ),
233
+ metadata: {placeholder: dependency.placeholder}
234
+ )
235
+ .with(id: "#{module_id}:dependency:#{index}")
236
+
237
+ if external_url?(dependency.url) && !plugin_resolvable_external_import?(resolved_dependency, dependency, context)
238
+ external_dependencies[dependency.placeholder] = dependency.url
239
+ next
240
+ end
241
+
242
+ resolved_dependency
243
+ end
244
+
245
+ CssDependencies.new(resolved_dependencies, external_dependencies.freeze)
246
+ end
247
+
248
+ def build_compose_dependencies(module_id, css_result)
249
+ css_result
250
+ .exports
251
+ .values
252
+ .flat_map(&:composes)
253
+ .grep(ComposeDependency)
254
+ .map(&:specifier)
255
+ .uniq
256
+ .each_with_index
257
+ .map do |specifier, index|
258
+ Dependency
259
+ .create(
260
+ specifier: specifier,
261
+ importer_id: module_id,
262
+ kind: :css_compose
263
+ )
264
+ .with(id: "#{module_id}:compose:#{index}")
265
+ end
266
+ end
267
+
268
+ def build_variable_dependencies(module_id, css_result)
269
+ css_result
270
+ .references
271
+ .values
272
+ .grep(VariableDependency)
273
+ .map(&:specifier)
274
+ .uniq
275
+ .each_with_index
276
+ .map do |specifier, index|
277
+ Dependency
278
+ .create(
279
+ specifier: specifier,
280
+ importer_id: module_id,
281
+ kind: :css_variable
282
+ )
283
+ .with(id: "#{module_id}:variable:#{index}")
284
+ end
285
+ end
286
+
287
+ def plugin_resolvable_external_import?(dependency, css_dependency, context)
288
+ return false unless css_dependency.is_a?(ImportDependency)
289
+
290
+ context.resolve_dependency(dependency)
291
+ true
292
+ rescue ResolveError
293
+ false
294
+ end
295
+
296
+ def finalized_css_edit(css_result, resolved_dependencies, dependency_records, external_dependencies, import_asset_type: :css)
297
+ replace_dependencies(css_result, resolved_dependencies, dependency_records, external_dependencies, import_asset_type:)
298
+ .then { replace_variable_references(css_result, it, resolved_dependencies, dependency_records) }
299
+ .then { remove_empty_imports(it) }
300
+ end
301
+
302
+ def replace_variable_references(css_result, css_edit, resolved_dependencies, dependency_records)
303
+ variable_records =
304
+ resolved_dependencies
305
+ .select { it.dependency.kind == :css_variable }
306
+ .to_h do |resolved_dependency|
307
+ [
308
+ resolved_dependency.dependency.specifier,
309
+ dependency_records.fetch(resolved_dependency.dependency.id)
310
+ ]
311
+ end
312
+
313
+ replacements =
314
+ css_result.references.to_h do |placeholder, reference|
315
+ record = variable_records.fetch(reference.specifier)
316
+ variables = record.metadata[:css_variables]
317
+ unless variables
318
+ raise UnsupportedFileError,
319
+ "CSS variable from #{reference.specifier.inspect} resolved to module #{record.id}, which does not export local CSS variables"
320
+ end
321
+ variable =
322
+ variables.fetch(reference.name) do
323
+ raise UnsupportedFileError,
324
+ "CSS variable #{reference.name.inspect} from #{reference.specifier.inspect} is not exported by module #{record.id}"
325
+ end
326
+ [placeholder, variable]
327
+ end
328
+
329
+ edits =
330
+ replacements.flat_map do |placeholder, replacement|
331
+ offsets = offsets_for(css_edit.code, placeholder)
332
+ if offsets.empty?
333
+ raise KeyError, "Could not find CSS variable placeholder #{placeholder.inspect}"
334
+ end
335
+ offsets.map do |start_offset|
336
+ SourceMap::Edit.replace(start_offset, start_offset + placeholder.length, replacement)
337
+ end
338
+ end
339
+ return css_edit if edits.empty?
340
+
341
+ edited = SourceMap::Editor.new(css_edit.code, css_edit.source_map).apply(edits)
342
+ CssEdit.new(edited.code, edited.source_map)
343
+ end
344
+
345
+ def offsets_for(code, value)
346
+ offsets = []
347
+ offset = 0
348
+ while (found = code.index(value, offset))
349
+ offsets << found
350
+ offset = found + value.length
351
+ end
352
+ offsets
353
+ end
354
+
355
+ def replace_dependencies(css_result, resolved_dependencies, dependency_records, external_dependencies, import_asset_type:)
356
+ dependencies_by_placeholder =
357
+ resolved_dependencies.filter_map do |resolved_dependency|
358
+ next unless resolved_dependency.dependency.metadata.key?(:placeholder)
359
+
360
+ [resolved_dependency.dependency.metadata.fetch(:placeholder), resolved_dependency]
361
+ end.to_h
362
+
363
+ replacements =
364
+ css_result.dependencies.to_h do |css_dependency|
365
+ replacement =
366
+ if external_dependencies.key?(css_dependency.placeholder)
367
+ external_dependencies.fetch(css_dependency.placeholder)
368
+ else
369
+ resolved_dependency = dependencies_by_placeholder.fetch(css_dependency.placeholder)
370
+ record = dependency_records.fetch(resolved_dependency.dependency.id)
371
+ replacement_for_dependency(resolved_dependency, record, import_asset_type:)
372
+ end
373
+
374
+ [css_dependency.placeholder, replacement]
375
+ end
376
+
377
+ map = SourceMap::Map.parse(css_result.source_map)
378
+ edits =
379
+ replacements.map do |placeholder, replacement|
380
+ start_offset = css_result.code.index(placeholder) || raise(KeyError, "Could not find CSS dependency placeholder #{placeholder.inspect}")
381
+ SourceMap::Edit.replace(start_offset, start_offset + placeholder.length, replacement)
382
+ end
383
+
384
+ edited = SourceMap::Editor.new(css_result.code, map).apply(edits)
385
+ CssEdit.new(edited.code, edited.source_map)
386
+ end
387
+
388
+ def remove_empty_imports(css_edit)
389
+ edits =
390
+ css_edit
391
+ .code
392
+ .to_enum(:scan, /@import\s+(?:url\(\s*)?["']{2}\s*\)?\s*;\s*/i)
393
+ .map do
394
+ match = Regexp.last_match
395
+ SourceMap::Edit.delete(match.begin(0), match.end(0))
396
+ end
397
+
398
+ return css_edit if edits.empty?
399
+
400
+ edited = SourceMap::Editor.new(css_edit.code, css_edit.source_map).apply(edits)
401
+ CssEdit.new(edited.code, edited.source_map)
402
+ end
403
+
404
+ def css_asset_pair(module_id, css_edit, type, metadata, context)
405
+ source_map_asset = nil
406
+ css = css_edit.code
407
+
408
+ if source_maps_enabled?(context)
409
+ source_map = source_map_for(css_edit.source_map, module_id, context)
410
+ source_map_json = source_map.to_json
411
+ source_map_hash = Hashing.short(source_map_json)
412
+ suffix = (type == :css) ? "" : ".javascript"
413
+ source_map_output_path = "/assets/#{asset_name(module_id)}#{suffix}.#{source_map_hash}.css.map"
414
+ source_map_asset =
415
+ Asset.new(
416
+ module_id.path,
417
+ source_map_hash,
418
+ source_map_output_path,
419
+ nil,
420
+ source_map_json,
421
+ "application/json",
422
+ {type: (type == :css) ? :css_source_map : :"#{type}_source_map"}
423
+ )
424
+ css = "#{css.chomp}\n/*# sourceMappingURL=#{File.basename(source_map_output_path)} */\n"
425
+ end
426
+
427
+ hash = Hashing.short(css)
428
+ suffix = (type == :css) ? "" : ".javascript"
429
+ output_path = "/assets/#{asset_name(module_id)}#{suffix}.#{hash}.css"
430
+ asset =
431
+ Asset.new(
432
+ module_id.path,
433
+ hash,
434
+ output_path,
435
+ nil,
436
+ css,
437
+ "text/css",
438
+ metadata.merge(type: type)
439
+ )
440
+
441
+ [asset, source_map_asset]
442
+ end
443
+
444
+ def source_map_for(source_map, module_id, context)
445
+ inline_origin = context.virtual_module_metadata(module_id)[:inline_css_origin]
446
+ return source_map unless inline_origin
447
+
448
+ adjusted =
449
+ source_map
450
+ .with(
451
+ sources: [inline_origin.fetch(:module_id).path],
452
+ sources_content: [inline_origin.fetch(:source)]
453
+ )
454
+ .map_original_lines(source_index: 0) { |line| line + inline_origin.fetch(:line_offset) }
455
+
456
+ adjusted
457
+ .with(
458
+ segments: adjusted
459
+ .segments
460
+ .map do |segment|
461
+ if segment.source_index == 0
462
+ segment.with_original(
463
+ line: segment.original_line,
464
+ column: segment.original_column + inline_origin.fetch(:column_offset)
465
+ )
466
+ else
467
+ segment
468
+ end
469
+ end
470
+ )
471
+ end
472
+
473
+ def source_maps_enabled?(context)
474
+ case @source_maps
475
+ when true
476
+ true
477
+ when :development
478
+ context.mode == :development
479
+ else
480
+ false
481
+ end
482
+ end
483
+
484
+ def replacement_for_dependency(resolved_dependency, record, import_asset_type:)
485
+ case resolved_dependency.dependency.kind
486
+ when :css_import
487
+ css_asset = record.assets.find { |asset| asset.metadata[:type] == import_asset_type }
488
+ css_asset ||= record.assets.find { |asset| asset.metadata[:type] == :css } if import_asset_type == :css_javascript_stylesheet
489
+ unless css_asset
490
+ raise UnsupportedFileError, "CSS @import #{resolved_dependency.dependency.specifier.inspect} from #{resolved_dependency.dependency.importer_id} resolved to module #{record.id}, which does not emit a CSS asset"
491
+ end
492
+ return (import_asset_type == :css) ? "" : css_asset.output_path
493
+ when :asset_url
494
+ unless record.assets.first
495
+ raise UnsupportedFileError, "CSS url() #{resolved_dependency.dependency.specifier.inspect} from #{resolved_dependency.dependency.importer_id} resolved to module #{record.id}, which does not emit an asset"
496
+ end
497
+ end
498
+
499
+ record.assets.first&.output_path || record.id.to_s
500
+ end
501
+
502
+ def external_url?(value)
503
+ uri = URI.parse(value)
504
+ uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
505
+ rescue URI::InvalidURIError
506
+ false
507
+ end
508
+
509
+ def ruby_module_source(classes, asset_path, styles_dependency:)
510
+ <<~RUBY
511
+ ClassNames = __klenod_import__(#{styles_dependency.id.inspect})
512
+ CSS_CLASSES = #{classes.inspect}.freeze
513
+ CSS_ASSET_PATH = #{asset_path.inspect}
514
+ Default = ClassNames.new(CSS_CLASSES)
515
+ RUBY
516
+ end
517
+
518
+ def css_selectors(result)
519
+ result.classes.merge(result.elements.transform_keys { :"__#{it}" })
520
+ end
521
+
522
+ def finalized_css_selectors(css_result, resolved_dependencies, dependency_records)
523
+ compose_records =
524
+ resolved_dependencies
525
+ .select { it.dependency.kind == :css_compose }
526
+ .to_h do |resolved_dependency|
527
+ [
528
+ resolved_dependency.dependency.specifier,
529
+ dependency_records.fetch(resolved_dependency.dependency.id)
530
+ ]
531
+ end
532
+ resolved_classes = {}
533
+
534
+ resolve_class =
535
+ lambda do |name, resolving|
536
+ return resolved_classes.fetch(name) if resolved_classes.key?(name)
537
+ if resolving.include?(name)
538
+ raise Error, "Circular local CSS composition involving #{name.inspect}"
539
+ end
540
+
541
+ generated_name = css_result.classes.fetch(name)
542
+ export = css_result.exports.fetch(generated_name)
543
+ values = [export.name]
544
+
545
+ export.composes.each do |compose|
546
+ case compose
547
+ when ComposeLocal
548
+ values.concat(resolve_class.call(compose.name, [*resolving, name]).split)
549
+ when ComposeGlobal
550
+ values << compose.name
551
+ when ComposeDependency
552
+ record = compose_records.fetch(compose.specifier)
553
+ dependency_classes = record.metadata[:css_classes]
554
+ unless dependency_classes
555
+ raise UnsupportedFileError,
556
+ "CSS composes from #{compose.specifier.inspect} resolved to module #{record.id}, which does not export CSS classes"
557
+ end
558
+ dependency_class =
559
+ dependency_classes.fetch(compose.name) do
560
+ raise UnsupportedFileError,
561
+ "CSS composes class #{compose.name.inspect} from #{compose.specifier.inspect}, but module #{record.id} does not export it"
562
+ end
563
+ values.concat(dependency_class.split)
564
+ end
565
+ end
566
+
567
+ resolved_classes[name] = values.uniq.join(" ")
568
+ end
569
+
570
+ css_result.classes.each_key { resolve_class.call(it, []) }
571
+ resolved_classes.merge(css_result.elements.transform_keys { :"__#{it}" })
572
+ end
573
+
574
+ def asset_name(module_id)
575
+ module_id.path.gsub(/[^A-Za-z0-9]+/, "_").sub(/\A_+/, "").sub(/_+\z/, "")
576
+ end
577
+ end
578
+ end
579
+ end
580
+ end
581
+ end
Binary file
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ begin
6
+ require "klenod/plugin/css/native"
7
+ rescue LoadError
8
+ nil
9
+ end
10
+
11
+ module Klenod
12
+ module Build
13
+ module Plugins
14
+ module CSSPlugin
15
+ class Error < StandardError; end
16
+ class ParseError < Error; end
17
+
18
+ ImportDependency = Data.define(:url, :placeholder, :supports, :media, :loc)
19
+ UrlDependency = Data.define(:url, :placeholder, :loc)
20
+ Export = Data.define(:name, :composes, :referenced?)
21
+ ComposeLocal = Data.define(:name)
22
+ ComposeGlobal = Data.define(:name)
23
+ ComposeDependency = Data.define(:name, :specifier)
24
+ VariableDependency = Data.define(:name, :specifier)
25
+
26
+ Loc = Data.define(:file_path, :start, :end) do
27
+ def self.from_ext(data) =
28
+ new(
29
+ file_path: data.fetch("filePath"),
30
+ start: Pos.from_ext(data.fetch("start")),
31
+ end: Pos.from_ext(data.fetch("end"))
32
+ )
33
+ end
34
+
35
+ Pos = Data.define(:line, :column) do
36
+ def self.from_ext(data) =
37
+ new(data.fetch("line"), data.fetch("column"))
38
+ end
39
+
40
+ TransformResult = Data.define(:classes, :elements, :variables, :serialized_variables, :code, :source_map, :dependencies, :exports, :references) do
41
+ def self.from_ext(data) =
42
+ new(
43
+ classes: data.fetch("classes").transform_keys(&:to_sym),
44
+ elements: data.fetch("elements").transform_keys(&:to_sym),
45
+ variables: data.fetch("variables"),
46
+ serialized_variables: data.fetch("serialized_variables"),
47
+ code: data.fetch("code"),
48
+ source_map: data.fetch("source_map"),
49
+ dependencies: JSON.parse(data.fetch("dependencies")).map { dependency_from_ext(it) },
50
+ exports: JSON.parse(data.fetch("exports")).transform_values { export_from_ext(it) },
51
+ references: JSON.parse(data.fetch("references")).transform_values { reference_from_ext(it) }
52
+ )
53
+
54
+ def self.dependency_from_ext(dep)
55
+ case dep.fetch("type")
56
+ when "import"
57
+ ImportDependency[
58
+ url: dep.fetch("url"),
59
+ placeholder: dep.fetch("placeholder"),
60
+ supports: dep.fetch("supports"),
61
+ media: dep.fetch("media"),
62
+ loc: Loc.from_ext(dep.fetch("loc"))
63
+ ]
64
+ when "url"
65
+ UrlDependency[
66
+ url: dep.fetch("url"),
67
+ placeholder: dep.fetch("placeholder"),
68
+ loc: Loc.from_ext(dep.fetch("loc"))
69
+ ]
70
+ end
71
+ end
72
+
73
+ def self.export_from_ext(export)
74
+ Export[
75
+ name: export.fetch("name"),
76
+ referenced?: export.fetch("isReferenced"),
77
+ composes: export.fetch("composes").map do |compose|
78
+ case compose.fetch("type")
79
+ when "local"
80
+ ComposeLocal[name: compose.fetch("name").to_sym]
81
+ when "global"
82
+ ComposeGlobal[name: compose.fetch("name")]
83
+ when "dependency"
84
+ ComposeDependency[name: compose.fetch("name").to_sym, specifier: compose.fetch("specifier")]
85
+ end
86
+ end
87
+ ]
88
+ end
89
+
90
+ def self.reference_from_ext(reference)
91
+ case reference.fetch("type")
92
+ when "dependency"
93
+ VariableDependency[
94
+ name: reference.fetch("name"),
95
+ specifier: reference.fetch("specifier")
96
+ ]
97
+ end
98
+ end
99
+ end
100
+
101
+ module Transformer
102
+ module_function
103
+
104
+ def native?
105
+ !native_transformer.nil?
106
+ end
107
+
108
+ def transform(
109
+ file,
110
+ code,
111
+ minify: true,
112
+ transform_names: true,
113
+ class_pattern: "[component].[local]?[hash]",
114
+ tag_pattern: "[component]_[local]?[hash]",
115
+ local_css_variables: false,
116
+ variable_pattern: "[component]-[local]?[hash]"
117
+ )
118
+ native = native_transformer || raise(Error, "CSS support requires the klenod-plugin-css native extension. Run `bundle exec rake compile` in gems/klenod-plugin-css.")
119
+ TransformResult.from_ext(
120
+ native.transform_native(
121
+ code,
122
+ file,
123
+ {
124
+ "minify" => minify,
125
+ "transform_names" => transform_names,
126
+ "class_pattern" => class_pattern,
127
+ "tag_pattern" => tag_pattern,
128
+ "local_css_variables" => local_css_variables,
129
+ "variable_pattern" => variable_pattern
130
+ }
131
+ )
132
+ )
133
+ end
134
+
135
+ def native_transformer
136
+ @native_transformer =
137
+ if defined?(@native_transformer)
138
+ @native_transformer
139
+ else
140
+ begin
141
+ require "klenod/plugin/css/native"
142
+ Klenod::Build::Plugins::CSSPlugin::Native
143
+ rescue LoadError
144
+ nil
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klenod
4
+ module Build
5
+ module Plugins
6
+ module CSSPlugin
7
+ VERSION = "0.0.1"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "klenod/build"
4
+ require_relative "../build/plugins/css_plugin"
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: klenod-plugin-css
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: aarch64-linux-gnu
6
+ authors:
7
+ - Andrés Alin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: klenod-build
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake-compiler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ description: Optional CSS asset plugin for Klenod build graphs.
42
+ email:
43
+ - andreas.alin@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - README.md
49
+ - lib/klenod/build/plugins/css_plugin.rb
50
+ - lib/klenod/plugin/css.rb
51
+ - lib/klenod/plugin/css/native.so
52
+ - lib/klenod/plugin/css/transformer.rb
53
+ - lib/klenod/plugin/css/version.rb
54
+ homepage: https://github.com/aalin/klenod
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ homepage_uri: https://github.com/aalin/klenod
59
+ source_code_uri: https://github.com/aalin/klenod/tree/main/gems/klenod-plugin-css
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 4.0.6
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '4.0'
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: 4.1.dev
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 3.3.22
80
+ requirements: []
81
+ rubygems_version: 3.5.23
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: CSS plugin for Klenod.
85
+ test_files: []