klenod-plugin-javascript 0.0.1-x86_64-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 +28 -0
- data/lib/klenod/build/plugins/javascript_plugin.rb +521 -0
- data/lib/klenod/plugin/javascript/native.so +0 -0
- data/lib/klenod/plugin/javascript/parser.rb +175 -0
- data/lib/klenod/plugin/javascript/version.rb +11 -0
- data/lib/klenod/plugin/javascript.rb +4 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 413f1306f71d3ad4e25dbfc5bb2894b4022f790f7ecf59797d7a3b81248bed1e
|
|
4
|
+
data.tar.gz: 6b0b643648aadbbdab03a7d24336ef584d12911a7b84a81b4de188bc6605081d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: dae69f2d98defc797907d02883014d4bd30bb2a2c6c35816a26a486db211285f5ff59ac092a6d249caa10aa71d092704bb1286e360f00719394b02997f1fe6bc
|
|
7
|
+
data.tar.gz: e8f8c341c1caa944b3d228ebb5fe646d2fd1c24cfeb76d72f286042dd1ca1a37e07ac54e041d359cf7792e6f9c9c81cf48a5bc209ca51333d10e8a992bb45b79
|
data/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# klenod-plugin-javascript
|
|
2
|
+
|
|
3
|
+
JavaScript asset plugin for [Klenod](https://github.com/aalin/klenod), powered
|
|
4
|
+
by [SWC](https://swc.rs/).
|
|
5
|
+
|
|
6
|
+
It:
|
|
7
|
+
|
|
8
|
+
- collects static imports, re-exports, and string-literal dynamic imports
|
|
9
|
+
- transforms JavaScript, TypeScript, JSX, and TSX
|
|
10
|
+
- rewrites local imports to content-hashed asset paths
|
|
11
|
+
- preserves external URL imports
|
|
12
|
+
|
|
13
|
+
Bare package imports and npm resolution are not currently supported.
|
|
14
|
+
|
|
15
|
+
## Options
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
require "klenod/plugin/javascript"
|
|
19
|
+
|
|
20
|
+
Klenod::Build::Plugins::JavaScriptPlugin.new(
|
|
21
|
+
source_maps: :development,
|
|
22
|
+
minify: false
|
|
23
|
+
)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `source_maps:` accepts `false`, `true`, or `:development` (the default).
|
|
27
|
+
- `minify:` also minifies in development when `true`; build output is always
|
|
28
|
+
minified.
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "klenod/build/asset"
|
|
4
|
+
require "klenod/build/dependency"
|
|
5
|
+
require "klenod/build/errors"
|
|
6
|
+
require "klenod/build/hashing"
|
|
7
|
+
require "klenod/build/load_result"
|
|
8
|
+
require "klenod/build/module_id"
|
|
9
|
+
require "klenod/build/plugin"
|
|
10
|
+
require "klenod/build/source_map"
|
|
11
|
+
require "klenod/build/transform_result"
|
|
12
|
+
require "klenod/runtime"
|
|
13
|
+
|
|
14
|
+
require_relative "../../plugin/javascript/parser"
|
|
15
|
+
|
|
16
|
+
module Klenod
|
|
17
|
+
module Build
|
|
18
|
+
module Plugins
|
|
19
|
+
module JavaScriptPlugin
|
|
20
|
+
def self.new(...)
|
|
21
|
+
Plugin.new(...)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Plugin < Klenod::Build::Plugin
|
|
25
|
+
CONTENT_TYPE = "application/javascript"
|
|
26
|
+
EXTENSIONS = [".js", ".jsx", ".ts", ".tsx"].freeze
|
|
27
|
+
CUSTOM_ELEMENT_EXTENSIONS = [".jsx", ".tsx"].freeze
|
|
28
|
+
JSX_RUNTIME_SPECIFIER = "virtual:klenod/jsx-runtime"
|
|
29
|
+
JSX_RUNTIME_MODULE_ID = ModuleId.new("virtual:klenod/jsx-runtime.js", nil)
|
|
30
|
+
LOCAL_SPECIFIER_PATTERN = %r{\A(?:\.{1,2}/|/|app:/)}
|
|
31
|
+
EXTERNAL_SPECIFIER_PATTERN = %r{\A(?:[A-Za-z][A-Za-z0-9+.-]*:)?//}
|
|
32
|
+
VALID_SOURCE_MAP_MODES = [false, true, :development].freeze
|
|
33
|
+
IDENTIFIER_PATTERN = '[$_\p{Alpha}][$\u200c\u200d\p{Alnum}_]*'
|
|
34
|
+
DEFAULT_EXPORT_CLASS_PATTERN = /\bexport\s+default\s+class\s+(#{IDENTIFIER_PATTERN})\b/
|
|
35
|
+
DEFAULT_EXPORT_IDENTIFIER_PATTERN = /\bexport\s+default\s+(#{IDENTIFIER_PATTERN})\s*;/
|
|
36
|
+
DEFAULT_IMPORT_PREFIX_PATTERN = /\Aimport\s+#{IDENTIFIER_PATTERN}\s+from\z/
|
|
37
|
+
|
|
38
|
+
def initialize(source_maps: :development, minify: false)
|
|
39
|
+
unless VALID_SOURCE_MAP_MODES.include?(source_maps)
|
|
40
|
+
raise ArgumentError, "source_maps must be false, true, or :development"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@source_maps = source_maps
|
|
44
|
+
@minify = minify
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve(dependency, context)
|
|
48
|
+
return ResolvedDependency.new(dependency, JSX_RUNTIME_MODULE_ID, {virtual: true}) if dependency.specifier == JSX_RUNTIME_SPECIFIER
|
|
49
|
+
return nil unless dependency.kind.to_s.start_with?("javascript_")
|
|
50
|
+
return nil unless extensionless_javascript_specifier?(dependency.specifier)
|
|
51
|
+
|
|
52
|
+
extensionless_resolve_extensions(dependency).each do |extension|
|
|
53
|
+
return context.resolve_dependency(dependency.with(specifier: "#{dependency.specifier}#{extension}"))
|
|
54
|
+
rescue ResolveError
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def load(module_id, _context)
|
|
62
|
+
return nil unless module_id.scheme == :virtual && module_id == JSX_RUNTIME_MODULE_ID
|
|
63
|
+
|
|
64
|
+
LoadResult.new(jsx_runtime_source, nil, nil)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def transform(module_id, code, context)
|
|
68
|
+
return super unless EXTENSIONS.include?(module_id.extname)
|
|
69
|
+
|
|
70
|
+
custom_element = custom_element_module?(module_id)
|
|
71
|
+
jsx_runtime_namespace = custom_element ? jsx_runtime_namespace(module_id) : nil
|
|
72
|
+
transform = Parser.transform(
|
|
73
|
+
code,
|
|
74
|
+
filename: module_id.to_s,
|
|
75
|
+
source_kind: source_kind(module_id),
|
|
76
|
+
minify: minify_enabled?(context),
|
|
77
|
+
jsx_runtime_namespace: jsx_runtime_namespace
|
|
78
|
+
)
|
|
79
|
+
javascript_source, imports =
|
|
80
|
+
if custom_element
|
|
81
|
+
inject_jsx_runtime(transform.code, transform.imports, module_id, jsx_runtime_namespace)
|
|
82
|
+
else
|
|
83
|
+
[transform.code, transform.imports]
|
|
84
|
+
end
|
|
85
|
+
dependencies = build_dependencies(module_id, imports)
|
|
86
|
+
|
|
87
|
+
Klenod::Build::TransformResult.new(
|
|
88
|
+
module_source(nil),
|
|
89
|
+
dependencies,
|
|
90
|
+
nil,
|
|
91
|
+
[],
|
|
92
|
+
[],
|
|
93
|
+
{
|
|
94
|
+
javascript_source: javascript_source,
|
|
95
|
+
javascript_original_source: code,
|
|
96
|
+
javascript_imports: imports,
|
|
97
|
+
javascript_custom_element: custom_element,
|
|
98
|
+
javascript_custom_element_tag: custom_element ? custom_element_tag(module_id) : nil
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def finalize(module_id, result, resolved_dependencies, dependency_records, context)
|
|
104
|
+
source = result.metadata[:javascript_source]
|
|
105
|
+
original_source = result.metadata[:javascript_original_source]
|
|
106
|
+
imports = result.metadata[:javascript_imports]
|
|
107
|
+
return result unless source && imports
|
|
108
|
+
|
|
109
|
+
edit = rewrite_imports(module_id, source, original_source, imports, resolved_dependencies, dependency_records)
|
|
110
|
+
source_map_asset = nil
|
|
111
|
+
code = edit.code
|
|
112
|
+
custom_element_descriptor = nil
|
|
113
|
+
|
|
114
|
+
if result.metadata[:javascript_custom_element]
|
|
115
|
+
tag = result.metadata.fetch(:javascript_custom_element_tag)
|
|
116
|
+
code = register_custom_element(module_id, code, tag)
|
|
117
|
+
custom_element_descriptor = custom_element_descriptor(tag, nil)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if source_maps_enabled?(context)
|
|
121
|
+
source_map_json = edit.source_map.to_json
|
|
122
|
+
source_map_hash = Hashing.short(source_map_json)
|
|
123
|
+
source_map_output_path = "/assets/#{asset_name(module_id)}.#{source_map_hash}.js.map"
|
|
124
|
+
source_map_asset =
|
|
125
|
+
Asset.new(
|
|
126
|
+
module_id.path,
|
|
127
|
+
source_map_hash,
|
|
128
|
+
source_map_output_path,
|
|
129
|
+
nil,
|
|
130
|
+
source_map_json,
|
|
131
|
+
"application/json",
|
|
132
|
+
{type: :javascript_source_map}
|
|
133
|
+
)
|
|
134
|
+
code = "#{code.chomp}\n//# sourceMappingURL=#{File.basename(source_map_output_path)}\n"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
hash = Hashing.short(code)
|
|
138
|
+
output_path = "/assets/#{asset_name(module_id)}.#{hash}.js"
|
|
139
|
+
asset_javascript_assets = asset_javascript_assets(resolved_dependencies, dependency_records)
|
|
140
|
+
metadata = {type: :javascript}
|
|
141
|
+
css_preload_assets = css_preload_assets(resolved_dependencies, dependency_records)
|
|
142
|
+
metadata[:preload_assets] = css_preload_assets unless css_preload_assets.empty?
|
|
143
|
+
asset =
|
|
144
|
+
Asset.new(
|
|
145
|
+
module_id.path,
|
|
146
|
+
hash,
|
|
147
|
+
output_path,
|
|
148
|
+
nil,
|
|
149
|
+
code,
|
|
150
|
+
CONTENT_TYPE,
|
|
151
|
+
metadata
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
result.with(
|
|
155
|
+
code: module_source(output_path, custom_element_descriptor: custom_element_descriptor&.merge(asset_path: output_path)),
|
|
156
|
+
assets: [asset, source_map_asset, *asset_javascript_assets].compact,
|
|
157
|
+
metadata: result.metadata.merge(
|
|
158
|
+
javascript_asset_path: output_path,
|
|
159
|
+
javascript_custom_element_descriptor: custom_element_descriptor&.merge(asset_path: output_path)
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def import_value(_resolved_dependency, record, context)
|
|
165
|
+
return nil unless EXTENSIONS.include?(record.id.extname)
|
|
166
|
+
|
|
167
|
+
context.mods.fetch(record.id).const_get(:Exports)::Default
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def runtime_import_value(_resolved_dependency, record, _context)
|
|
171
|
+
return nil unless EXTENSIONS.include?(record.id.extname)
|
|
172
|
+
|
|
173
|
+
return record.metadata.fetch(:javascript_custom_element_descriptor) if record.metadata[:javascript_custom_element]
|
|
174
|
+
|
|
175
|
+
record.metadata.fetch(:javascript_asset_path)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def custom_element_module?(module_id)
|
|
181
|
+
CUSTOM_ELEMENT_EXTENSIONS.include?(module_id.extname)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def inject_jsx_runtime(code, imports, module_id, namespace)
|
|
185
|
+
prefix = %(import * as #{namespace} from "#{JSX_RUNTIME_SPECIFIER}";\n)
|
|
186
|
+
[
|
|
187
|
+
"#{prefix}#{code}",
|
|
188
|
+
[
|
|
189
|
+
jsx_runtime_import_record(prefix, module_id),
|
|
190
|
+
*imports.map { shifted_import_record(it, prefix.length) }
|
|
191
|
+
]
|
|
192
|
+
]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def jsx_runtime_import_record(prefix, module_id)
|
|
196
|
+
start_offset = prefix.index(JSX_RUNTIME_SPECIFIER) || raise(KeyError, "Missing JSX runtime specifier in injected import")
|
|
197
|
+
ImportRecord.new(
|
|
198
|
+
JSX_RUNTIME_SPECIFIER,
|
|
199
|
+
:javascript_import,
|
|
200
|
+
start_offset,
|
|
201
|
+
start_offset + JSX_RUNTIME_SPECIFIER.length,
|
|
202
|
+
{},
|
|
203
|
+
"#{module_id}:1:#{start_offset + 1}"
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def shifted_import_record(import, offset)
|
|
208
|
+
ImportRecord.new(
|
|
209
|
+
import.specifier,
|
|
210
|
+
import.kind,
|
|
211
|
+
import.start_offset + offset,
|
|
212
|
+
import.end_offset + offset,
|
|
213
|
+
import.attributes,
|
|
214
|
+
import.loc
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def build_dependencies(module_id, imports)
|
|
219
|
+
imports.filter_map.with_index do |import, index|
|
|
220
|
+
next if external_specifier?(import.specifier)
|
|
221
|
+
raise DynamicImportError, unsupported_specifier_message(import) unless local_specifier?(import.specifier) || import.specifier == JSX_RUNTIME_SPECIFIER
|
|
222
|
+
|
|
223
|
+
Dependency
|
|
224
|
+
.create(
|
|
225
|
+
specifier: import.specifier,
|
|
226
|
+
importer_id: module_id,
|
|
227
|
+
kind: import.kind,
|
|
228
|
+
loc: source_location(import.loc, module_id),
|
|
229
|
+
metadata: {
|
|
230
|
+
start_offset: import.start_offset,
|
|
231
|
+
end_offset: import.end_offset,
|
|
232
|
+
attribute_insert_offset: import.end_offset + 1,
|
|
233
|
+
attributes: import.attributes
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
.with(id: "#{module_id}:dependency:#{index}")
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def source_location(value, module_id)
|
|
241
|
+
match = value.to_s.match(/\A(?<path>.*):(?<line>\d+):(?<column>\d+)\z/)
|
|
242
|
+
return SourceLocation.new(module_id.to_s, nil, nil) unless match
|
|
243
|
+
|
|
244
|
+
SourceLocation.new(match[:path], match[:line].to_i, match[:column].to_i)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def rewrite_imports(module_id, source, original_source, imports, resolved_dependencies, dependency_records)
|
|
248
|
+
resolved_by_range =
|
|
249
|
+
resolved_dependencies.to_h do |resolved_dependency|
|
|
250
|
+
metadata = resolved_dependency.dependency.metadata
|
|
251
|
+
range = [metadata.fetch(:start_offset), metadata.fetch(:end_offset)]
|
|
252
|
+
record = dependency_records.fetch(resolved_dependency.dependency.id)
|
|
253
|
+
[range, [resolved_dependency.dependency, record]]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
edits =
|
|
257
|
+
imports.flat_map do |import|
|
|
258
|
+
dependency, record = resolved_by_range[[import.start_offset, import.end_offset]]
|
|
259
|
+
next [] unless dependency
|
|
260
|
+
|
|
261
|
+
import_edits(module_id, source, dependency, record)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
SourceMap::Editor.new(source, identity_source_map(module_id, source, original_source)).apply(edits)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def import_edits(module_id, source, dependency, record)
|
|
268
|
+
if (stylesheet_asset = css_javascript_stylesheet_asset(record))
|
|
269
|
+
validate_css_import!(module_id, source, dependency)
|
|
270
|
+
edits = [SourceMap::Edit.replace(dependency.metadata.fetch(:start_offset), dependency.metadata.fetch(:end_offset), stylesheet_asset.output_path)]
|
|
271
|
+
if dependency.metadata.fetch(:attributes)[:type].nil?
|
|
272
|
+
edits << SourceMap::Edit.replace(dependency.metadata.fetch(:attribute_insert_offset), dependency.metadata.fetch(:attribute_insert_offset), %( with { type: "css" }))
|
|
273
|
+
end
|
|
274
|
+
return edits
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
if (asset_path = asset_javascript_asset_path(record))
|
|
278
|
+
validate_asset_import!(module_id, source, dependency)
|
|
279
|
+
return [SourceMap::Edit.replace(dependency.metadata.fetch(:start_offset), dependency.metadata.fetch(:end_offset), asset_path)]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
[SourceMap::Edit.replace(dependency.metadata.fetch(:start_offset), dependency.metadata.fetch(:end_offset), record.metadata.fetch(:javascript_asset_path))]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def asset_javascript_asset_path(record)
|
|
286
|
+
record.metadata[:image_javascript_asset_path] || record.metadata[:svg_javascript_asset_path]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def asset_javascript_assets(resolved_dependencies, dependency_records)
|
|
290
|
+
resolved_dependencies.filter_map do |resolved_dependency|
|
|
291
|
+
record = dependency_records.fetch(resolved_dependency.dependency.id)
|
|
292
|
+
asset = record.assets.find { asset_javascript_metadata?(it) }
|
|
293
|
+
next unless asset
|
|
294
|
+
|
|
295
|
+
javascript_asset(asset)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def asset_javascript_metadata?(asset)
|
|
300
|
+
asset.metadata[:type] == :image_javascript_metadata || asset.metadata[:type] == :svg_javascript_metadata
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def javascript_asset(asset)
|
|
304
|
+
Asset.new(
|
|
305
|
+
asset.logical_name,
|
|
306
|
+
asset.content_hash,
|
|
307
|
+
asset.output_path,
|
|
308
|
+
asset.source_path,
|
|
309
|
+
asset.bytes,
|
|
310
|
+
asset.content_type,
|
|
311
|
+
asset.metadata.merge(type: :javascript)
|
|
312
|
+
)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def css_javascript_stylesheet_asset(record)
|
|
316
|
+
return nil unless record.id.extname == ".css"
|
|
317
|
+
|
|
318
|
+
record.assets.find { it.metadata[:type] == :css_javascript_stylesheet } || record.assets.find { it.metadata[:type] == :css }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def css_preload_assets(resolved_dependencies, dependency_records)
|
|
322
|
+
resolved_dependencies.filter_map do |resolved_dependency|
|
|
323
|
+
record = dependency_records.fetch(resolved_dependency.dependency.id)
|
|
324
|
+
asset = css_javascript_stylesheet_asset(record)
|
|
325
|
+
{path: asset.output_path, as: "style"} if asset
|
|
326
|
+
end.uniq
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def validate_asset_import!(module_id, source, dependency)
|
|
330
|
+
return if dependency.kind == :javascript_import && default_import?(source, dependency)
|
|
331
|
+
|
|
332
|
+
raise DynamicImportError, "Unsupported asset import #{dependency.specifier.inspect} in #{module_id}. Use a default import, e.g. `import asset from \"#{dependency.specifier}\"`."
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def validate_css_import!(module_id, source, dependency)
|
|
336
|
+
validate_asset_import!(module_id, source, dependency)
|
|
337
|
+
type = dependency.metadata.fetch(:attributes)[:type]
|
|
338
|
+
return if type.nil? || type == "css"
|
|
339
|
+
|
|
340
|
+
raise DynamicImportError, "Unsupported CSS import attribute type #{type.inspect} for #{dependency.specifier.inspect} in #{module_id}. CSS imports must use `with { type: \"css\" }`."
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def default_import?(source, dependency)
|
|
344
|
+
start_offset = dependency.metadata.fetch(:start_offset)
|
|
345
|
+
statement_start = source.rindex(/\bimport\b/, start_offset) || 0
|
|
346
|
+
prefix = source[statement_start...start_offset].sub(/["']\z/, "").strip
|
|
347
|
+
prefix.match?(DEFAULT_IMPORT_PREFIX_PATTERN)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def register_custom_element(module_id, code, tag)
|
|
351
|
+
constructor = custom_element_constructor(module_id, code)
|
|
352
|
+
<<~JS
|
|
353
|
+
#{code.chomp}
|
|
354
|
+
Object.defineProperty(#{constructor}, "__klenodCustomElementTag", { value: #{tag.inspect} });
|
|
355
|
+
customElements.define(#{tag.inspect}, #{constructor});
|
|
356
|
+
JS
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def custom_element_constructor(module_id, code)
|
|
360
|
+
if (match = code.match(DEFAULT_EXPORT_CLASS_PATTERN))
|
|
361
|
+
return match[1]
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
if (match = code.match(DEFAULT_EXPORT_IDENTIFIER_PATTERN))
|
|
365
|
+
return match[1]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
raise Error, "\"custom element\" modules must default-export a named class or identifier: #{module_id}"
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def custom_element_descriptor(tag, asset_path)
|
|
372
|
+
{
|
|
373
|
+
__klenod_custom_element: true,
|
|
374
|
+
tag: tag,
|
|
375
|
+
asset_path: asset_path
|
|
376
|
+
}
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def identity_source_map(module_id, source, original_source)
|
|
380
|
+
SourceMap::Map.new(
|
|
381
|
+
version: 3,
|
|
382
|
+
source_root: nil,
|
|
383
|
+
sources: [module_id.path],
|
|
384
|
+
sources_content: [original_source || source],
|
|
385
|
+
names: [],
|
|
386
|
+
segments: identity_segments(source)
|
|
387
|
+
)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def identity_segments(source)
|
|
391
|
+
line_count = source.count("\n") + 1
|
|
392
|
+
line_count.times.map do |line|
|
|
393
|
+
SourceMap::Segment.new(line, 0, 0, line, 0, nil)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def source_maps_enabled?(context)
|
|
398
|
+
case @source_maps
|
|
399
|
+
when true
|
|
400
|
+
true
|
|
401
|
+
when :development
|
|
402
|
+
context.mode == :development
|
|
403
|
+
else
|
|
404
|
+
false
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def minify_enabled?(context)
|
|
409
|
+
context.mode == :build || @minify
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def unsupported_specifier_message(import)
|
|
413
|
+
"Unsupported JavaScript import #{import.specifier.inspect} at #{import.loc}. Only relative, app-root, and external URL imports are supported."
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def local_specifier?(specifier)
|
|
417
|
+
specifier.match?(LOCAL_SPECIFIER_PATTERN)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def external_specifier?(specifier)
|
|
421
|
+
specifier.match?(EXTERNAL_SPECIFIER_PATTERN)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def extensionless_javascript_specifier?(specifier)
|
|
425
|
+
local_specifier?(specifier) && File.extname(specifier).empty?
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def extensionless_resolve_extensions(dependency)
|
|
429
|
+
case dependency.importer_id&.extname
|
|
430
|
+
when ".tsx"
|
|
431
|
+
[".tsx", ".ts", ".jsx", ".js"]
|
|
432
|
+
when ".ts"
|
|
433
|
+
[".ts", ".js"]
|
|
434
|
+
when ".jsx"
|
|
435
|
+
[".jsx", ".js"]
|
|
436
|
+
else
|
|
437
|
+
[".js"]
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def source_kind(module_id)
|
|
442
|
+
case module_id.extname
|
|
443
|
+
when ".ts"
|
|
444
|
+
:typescript
|
|
445
|
+
when ".jsx"
|
|
446
|
+
:javascript_jsx
|
|
447
|
+
when ".tsx"
|
|
448
|
+
:typescript_jsx
|
|
449
|
+
else
|
|
450
|
+
:javascript
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def jsx_runtime_source
|
|
455
|
+
<<~JS
|
|
456
|
+
export function h(type, attrs, ...children) {
|
|
457
|
+
if (type && typeof type.__klenodCustomElementTag === "string") {
|
|
458
|
+
return h(type.__klenodCustomElementTag, attrs, ...children);
|
|
459
|
+
}
|
|
460
|
+
if (typeof type === "function") return type(attrs, ...children);
|
|
461
|
+
|
|
462
|
+
const el = document.createElement(type);
|
|
463
|
+
|
|
464
|
+
for (const [key, value] of Object.entries(attrs || {})) {
|
|
465
|
+
if (value) {
|
|
466
|
+
if (value === true) {
|
|
467
|
+
el.setAttribute(key, key);
|
|
468
|
+
} else {
|
|
469
|
+
el.setAttribute(key, value);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
appendChildren(el, children);
|
|
475
|
+
return el;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function Fragment(_attrs, ...children) {
|
|
479
|
+
const fragment = document.createDocumentFragment();
|
|
480
|
+
appendChildren(fragment, children);
|
|
481
|
+
return fragment;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function appendChildren(parent, children) {
|
|
485
|
+
children.flat().forEach((child) => {
|
|
486
|
+
if (child instanceof Node) {
|
|
487
|
+
parent.appendChild(child);
|
|
488
|
+
} else if (child) {
|
|
489
|
+
parent.appendChild(document.createTextNode(String(child)));
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
JS
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def module_source(output_path, custom_element_descriptor: nil)
|
|
497
|
+
default = custom_element_descriptor || output_path
|
|
498
|
+
|
|
499
|
+
<<~RUBY
|
|
500
|
+
Default = #{default.inspect}
|
|
501
|
+
RUBY
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def asset_name(module_id)
|
|
505
|
+
module_id.path.gsub(/[^A-Za-z0-9]+/, "_").sub(/\A_+/, "").sub(/_+\z/, "")
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def custom_element_tag(module_id)
|
|
509
|
+
name = asset_name(module_id).downcase.gsub(/_+/, "-")
|
|
510
|
+
hash = Hashing.short(module_id.to_s, length: 8)
|
|
511
|
+
"klenod-#{name}-#{hash}"
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def jsx_runtime_namespace(module_id)
|
|
515
|
+
"__klenod_jsx_#{Hashing.short(module_id.to_s, length: 8)}"
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Klenod
|
|
4
|
+
module Build
|
|
5
|
+
module Plugins
|
|
6
|
+
module JavaScriptPlugin
|
|
7
|
+
ImportRecord = Data.define(:specifier, :kind, :start_offset, :end_offset, :attributes, :loc)
|
|
8
|
+
TransformResult = Data.define(:code, :imports)
|
|
9
|
+
|
|
10
|
+
module Parser
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def native?
|
|
14
|
+
!native_parser.nil?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse(source, filename:)
|
|
18
|
+
transform(source, filename: filename, source_kind: :javascript, minify: false).imports
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def transform(source, filename:, source_kind:, minify: false, jsx_runtime_namespace: nil)
|
|
22
|
+
if NATIVE_SOURCE_KINDS.include?(source_kind)
|
|
23
|
+
native = native_parser || raise(SyntaxError, "TypeScript support requires the klenod-plugin-javascript native extension. Run `bundle exec rake compile` in gems/klenod-plugin-javascript.")
|
|
24
|
+
return native_transform_result(native.transform_native(source, filename, source_kind.to_s, {"minify" => minify, "jsx_runtime_namespace" => jsx_runtime_namespace}))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if (native = native_parser)
|
|
28
|
+
return native_transform_result(native.transform_native(source, filename, "javascript", {"minify" => minify, "jsx_runtime_namespace" => jsx_runtime_namespace}))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
scanner = FallbackScanner.new(source, filename)
|
|
32
|
+
TransformResult.new(source, scanner.imports)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
NATIVE_SOURCE_KINDS = [:typescript, :javascript_jsx, :typescript_jsx].freeze
|
|
36
|
+
|
|
37
|
+
def native_parser
|
|
38
|
+
@native_parser =
|
|
39
|
+
if defined?(@native_parser)
|
|
40
|
+
@native_parser
|
|
41
|
+
else
|
|
42
|
+
begin
|
|
43
|
+
require "klenod/plugin/javascript/native"
|
|
44
|
+
Klenod::Build::Plugins::JavaScriptPlugin::Native
|
|
45
|
+
rescue LoadError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def native_import_record(record)
|
|
52
|
+
ImportRecord.new(
|
|
53
|
+
record.fetch("specifier"),
|
|
54
|
+
record.fetch("kind").to_sym,
|
|
55
|
+
record.fetch("start_offset"),
|
|
56
|
+
record.fetch("end_offset"),
|
|
57
|
+
record.fetch("attributes").transform_keys(&:to_sym),
|
|
58
|
+
record.fetch("loc")
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def native_transform_result(result)
|
|
63
|
+
TransformResult.new(
|
|
64
|
+
result.fetch("code"),
|
|
65
|
+
result.fetch("imports").map { native_import_record(it) }
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class FallbackScanner
|
|
70
|
+
STATIC_IMPORT_PATTERN =
|
|
71
|
+
/
|
|
72
|
+
\bimport
|
|
73
|
+
(?:\s+type)?
|
|
74
|
+
(?:
|
|
75
|
+
\s* (?<side_effect>["'][^"']+["'])
|
|
76
|
+
|
|
|
77
|
+
(?<clause>[\s\S]*?) \s+ from \s+ (?<from>["'][^"']+["'])
|
|
78
|
+
)
|
|
79
|
+
(?:\s+with\s+\{(?<attributes>[^}]*)\})?
|
|
80
|
+
/x
|
|
81
|
+
EXPORT_FROM_PATTERN = /\bexport\b[\s\S]*?\bfrom\s+(?<from>["'][^"']+["'])/
|
|
82
|
+
DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*(?<from>["'][^"']+["'])\s*\)/
|
|
83
|
+
|
|
84
|
+
def initialize(source, filename)
|
|
85
|
+
@source = source
|
|
86
|
+
@filename = filename
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def imports
|
|
90
|
+
records = []
|
|
91
|
+
scan_regex(STATIC_IMPORT_PATTERN, :javascript_import, records) { |match| match[:side_effect] || match[:from] }
|
|
92
|
+
scan_regex(EXPORT_FROM_PATTERN, :javascript_export, records) { |match| match[:from] }
|
|
93
|
+
scan_regex(DYNAMIC_IMPORT_PATTERN, :javascript_dynamic_import, records) { |match| match[:from] }
|
|
94
|
+
records.sort_by(&:start_offset)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def scan_regex(pattern, kind, records)
|
|
100
|
+
@source.to_enum(:scan, pattern).each do
|
|
101
|
+
match = Regexp.last_match
|
|
102
|
+
next if inside_comment_or_string?(match.begin(0))
|
|
103
|
+
|
|
104
|
+
literal = yield(match)
|
|
105
|
+
start_offset = @source.index(literal, match.begin(0)) + 1
|
|
106
|
+
end_offset = start_offset + literal.length - 2
|
|
107
|
+
attributes = import_attributes(match)
|
|
108
|
+
records << ImportRecord.new(literal[1...-1], kind, start_offset, end_offset, attributes, loc(start_offset))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def import_attributes(match)
|
|
113
|
+
attributes = match.names.include?("attributes") ? match[:attributes] : nil
|
|
114
|
+
type = attributes&.match(/\btype\s*:\s*["'](?<type>[^"']+)["']/) { it[:type] }
|
|
115
|
+
type ? {type: type} : {}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def inside_comment_or_string?(offset)
|
|
119
|
+
state = :code
|
|
120
|
+
quote = nil
|
|
121
|
+
escaped = false
|
|
122
|
+
index = 0
|
|
123
|
+
|
|
124
|
+
while index < offset
|
|
125
|
+
char = @source[index]
|
|
126
|
+
next_char = @source[index + 1]
|
|
127
|
+
|
|
128
|
+
case state
|
|
129
|
+
when :code
|
|
130
|
+
if char == "/" && next_char == "/"
|
|
131
|
+
state = :line_comment
|
|
132
|
+
index += 1
|
|
133
|
+
elsif char == "/" && next_char == "*"
|
|
134
|
+
state = :block_comment
|
|
135
|
+
index += 1
|
|
136
|
+
elsif char == "\"" || char == "'" || char == "`"
|
|
137
|
+
quote = char
|
|
138
|
+
escaped = false
|
|
139
|
+
state = :string
|
|
140
|
+
end
|
|
141
|
+
when :line_comment
|
|
142
|
+
state = :code if char == "\n"
|
|
143
|
+
when :block_comment
|
|
144
|
+
if char == "*" && next_char == "/"
|
|
145
|
+
state = :code
|
|
146
|
+
index += 1
|
|
147
|
+
end
|
|
148
|
+
when :string
|
|
149
|
+
if escaped
|
|
150
|
+
escaped = false
|
|
151
|
+
elsif char == "\\"
|
|
152
|
+
escaped = true
|
|
153
|
+
elsif char == quote
|
|
154
|
+
state = :code
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
index += 1
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
state != :code
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def loc(offset)
|
|
165
|
+
prefix = @source.byteslice(0, offset)
|
|
166
|
+
line = prefix.count("\n") + 1
|
|
167
|
+
column = offset - (prefix.rindex("\n") || -1)
|
|
168
|
+
"#{@filename}:#{line}:#{column}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: klenod-plugin-javascript
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: x86_64-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 JavaScript 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/javascript_plugin.rb
|
|
50
|
+
- lib/klenod/plugin/javascript.rb
|
|
51
|
+
- lib/klenod/plugin/javascript/native.so
|
|
52
|
+
- lib/klenod/plugin/javascript/parser.rb
|
|
53
|
+
- lib/klenod/plugin/javascript/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-javascript
|
|
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: JavaScript plugin for Klenod.
|
|
85
|
+
test_files: []
|