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 +7 -0
- data/README.md +41 -0
- data/lib/klenod/build/plugins/css_plugin.rb +581 -0
- data/lib/klenod/plugin/css/native.so +0 -0
- data/lib/klenod/plugin/css/transformer.rb +152 -0
- data/lib/klenod/plugin/css/version.rb +11 -0
- data/lib/klenod/plugin/css.rb +4 -0
- metadata +85 -0
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
|
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: []
|