klenod-build 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +29 -0
- data/lib/klenod/build/asset.rb +235 -0
- data/lib/klenod/build/asset_compression.rb +58 -0
- data/lib/klenod/build/asset_generation_queue.rb +50 -0
- data/lib/klenod/build/cli/application.rb +108 -0
- data/lib/klenod/build/cli.rb +3 -0
- data/lib/klenod/build/config.rb +142 -0
- data/lib/klenod/build/context.rb +370 -0
- data/lib/klenod/build/dependency.rb +21 -0
- data/lib/klenod/build/errors.rb +175 -0
- data/lib/klenod/build/filesystem_resolver.rb +147 -0
- data/lib/klenod/build/graph/invalidator.rb +246 -0
- data/lib/klenod/build/graph.rb +864 -0
- data/lib/klenod/build/graphviz.rb +282 -0
- data/lib/klenod/build/hashing.rb +21 -0
- data/lib/klenod/build/invalidation_result.rb +55 -0
- data/lib/klenod/build/load_result.rb +7 -0
- data/lib/klenod/build/loaded_module.rb +38 -0
- data/lib/klenod/build/module_id.rb +100 -0
- data/lib/klenod/build/module_record.rb +22 -0
- data/lib/klenod/build/plugin.rb +35 -0
- data/lib/klenod/build/plugins/class_names_runtime.rb +125 -0
- data/lib/klenod/build/plugins/component_defaults.rb +12 -0
- data/lib/klenod/build/plugins/data_plugin.rb +117 -0
- data/lib/klenod/build/plugins/gem_import_plugin.rb +125 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.json +17687 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.rb +105 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.txt +31 -0
- data/lib/klenod/build/plugins/google_fonts_plugin.rb +387 -0
- data/lib/klenod/build/plugins/haml_plugin/companions.rb +40 -0
- data/lib/klenod/build/plugins/haml_plugin/errors.rb +114 -0
- data/lib/klenod/build/plugins/haml_plugin/helper_source.rb +123 -0
- data/lib/klenod/build/plugins/haml_plugin/parser.rb +85 -0
- data/lib/klenod/build/plugins/haml_plugin/transformer/ruby_builder.rb +1039 -0
- data/lib/klenod/build/plugins/haml_plugin/transformer.rb +766 -0
- data/lib/klenod/build/plugins/haml_plugin.rb +369 -0
- data/lib/klenod/build/plugins/image_plugin.rb +401 -0
- data/lib/klenod/build/plugins/intl_plugin.rb +34 -0
- data/lib/klenod/build/plugins/markdown_compiler.rb +180 -0
- data/lib/klenod/build/plugins/markdown_plugin.rb +161 -0
- data/lib/klenod/build/plugins/router_plugin.rb +942 -0
- data/lib/klenod/build/plugins/ruby_plugin.rb +34 -0
- data/lib/klenod/build/plugins/svg_plugin.rb +197 -0
- data/lib/klenod/build/plugins.rb +22 -0
- data/lib/klenod/build/profiler.rb +79 -0
- data/lib/klenod/build/resolution_error_formatter.rb +42 -0
- data/lib/klenod/build/resolver.rb +95 -0
- data/lib/klenod/build/ruby_import_rewriter.rb +436 -0
- data/lib/klenod/build/source_map/editor.rb +110 -0
- data/lib/klenod/build/source_map/map.rb +168 -0
- data/lib/klenod/build/source_map/vlq.rb +68 -0
- data/lib/klenod/build/source_map.rb +12 -0
- data/lib/klenod/build/transform_result.rb +12 -0
- data/lib/klenod/build/version.rb +7 -0
- data/lib/klenod/build/watched_pattern.rb +11 -0
- data/lib/klenod/build/watcher.rb +141 -0
- data/lib/klenod/build.rb +15 -0
- metadata +310 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0a05a70d821a5b94964b4bade32066b9fd1bdade13d2b3899becdeb46d3d33ef
|
|
4
|
+
data.tar.gz: 68ac76064521f6ca7678d51256672d5ea9161814215bf4c9e3b5a3f743350bc8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 440df52a915ecb0b8312622268b94d413c3447d80912ef42a797a959aaadca42ec3c108a08439d79eb7cf9fff0d7baf4a5425e565d087b4f6b18efee84d0f2df
|
|
7
|
+
data.tar.gz: 1ddb4e48b0156a21160fa3d93f855ab657bc4ed40d323e5f3d2f6bbd9dc3d27b36c093ec9731b1eeb791a4b601499721ce89665c23c84be4ad57c02c84491d9f
|
data/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# klenod-build
|
|
2
|
+
|
|
3
|
+
`klenod-build` builds [Klenod](https://github.com/aalin/klenod) bundles.
|
|
4
|
+
|
|
5
|
+
It owns development and build-time behavior:
|
|
6
|
+
|
|
7
|
+
- module resolving and graph collection
|
|
8
|
+
- plugins
|
|
9
|
+
- asset generation
|
|
10
|
+
- bundle serialization
|
|
11
|
+
- a basic `klenod` CLI
|
|
12
|
+
- file watching and updates in development mode
|
|
13
|
+
|
|
14
|
+
Use this gem when building an application, running development tooling, or authoring build plugins.
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require "klenod/build"
|
|
18
|
+
|
|
19
|
+
context = Klenod::Build::Context.new(source_dir: "src")
|
|
20
|
+
context.build(entrypoints: ["pages/server"], output: "dist/klenod.bundle")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then you can load that bundle with [klenod-runtime](https://github.com/aalin/klenod/tree/main/gems/klenod-runtime).
|
|
24
|
+
|
|
25
|
+
The CLI can also generate a [Graphviz](https://graphviz.org/) DOT-file from a generated bundle:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
bundle exec klenod graph dist/klenod.bundle > graph.dot
|
|
29
|
+
```
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
module Klenod
|
|
8
|
+
module Build
|
|
9
|
+
class Asset
|
|
10
|
+
attr_reader :logical_name, :content_hash, :output_path, :source_path, :content_type, :metadata, :queue_kind, :error
|
|
11
|
+
|
|
12
|
+
def initialize(logical_name, content_hash, output_path, source_path, bytes, content_type, metadata, generator: nil, writer: nil, queue: nil, queue_kind: :cpu)
|
|
13
|
+
@logical_name = logical_name
|
|
14
|
+
@content_hash = content_hash
|
|
15
|
+
@output_path = output_path
|
|
16
|
+
@source_path = source_path
|
|
17
|
+
@bytes = bytes
|
|
18
|
+
@content_type = content_type
|
|
19
|
+
@metadata = metadata
|
|
20
|
+
@generator = generator
|
|
21
|
+
@writer = writer
|
|
22
|
+
@queue = queue
|
|
23
|
+
@queue_kind = queue_kind
|
|
24
|
+
@disk_path = nil
|
|
25
|
+
@task = nil
|
|
26
|
+
@mutex = Mutex.new
|
|
27
|
+
@state = (bytes || (source_path && !generator && !writer)) ? :ready : :pending
|
|
28
|
+
@error = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.generated(logical_name, content_hash, output_path, source_path, content_type, metadata, writer: nil, queue: nil, queue_kind: :cpu, &generator)
|
|
32
|
+
new(logical_name, content_hash, output_path, source_path, nil, content_type, metadata, generator: generator, writer: writer, queue: queue, queue_kind: queue_kind)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def bytes
|
|
36
|
+
wait
|
|
37
|
+
@bytes || File.binread(@disk_path || @source_path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def wait
|
|
41
|
+
return self if ready?
|
|
42
|
+
raise error if failed?
|
|
43
|
+
|
|
44
|
+
task = start_generation
|
|
45
|
+
task ? wait_for_task(task) : generate_now_or_queued
|
|
46
|
+
raise error if failed?
|
|
47
|
+
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ready?
|
|
52
|
+
@state == :ready
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def pending?
|
|
56
|
+
@state == :pending
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def running?
|
|
60
|
+
@state == :running
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def failed?
|
|
64
|
+
@state == :failed
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def static?
|
|
68
|
+
@generator.nil? && @writer.nil?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def generated?
|
|
72
|
+
!static?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def write_to(path, &block)
|
|
76
|
+
path = path.to_s
|
|
77
|
+
|
|
78
|
+
return skip_existing(path) if File.file?(path)
|
|
79
|
+
|
|
80
|
+
task = start_write(path, &block)
|
|
81
|
+
task ? wait_for_task(task) : write_now_or_queued(path, &block)
|
|
82
|
+
raise error if failed?
|
|
83
|
+
|
|
84
|
+
:written
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def start_generation
|
|
90
|
+
@mutex.synchronize do
|
|
91
|
+
return @task if @task
|
|
92
|
+
return nil if ready?
|
|
93
|
+
|
|
94
|
+
task = current_async_task
|
|
95
|
+
return nil unless task
|
|
96
|
+
|
|
97
|
+
@state = :running
|
|
98
|
+
@task = queued_task(task) { generate_now }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def start_write(path, &block)
|
|
103
|
+
@mutex.synchronize do
|
|
104
|
+
return @task if @task
|
|
105
|
+
return nil if ready? && materialized_for?(path)
|
|
106
|
+
|
|
107
|
+
task = current_async_task
|
|
108
|
+
return nil unless task
|
|
109
|
+
|
|
110
|
+
@state = :running
|
|
111
|
+
@task = queued_task(task) { write_now(path, &block) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def wait_for_task(task)
|
|
116
|
+
task.wait
|
|
117
|
+
rescue => e
|
|
118
|
+
fail_with(e)
|
|
119
|
+
raise
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def generate_now
|
|
123
|
+
return self if ready?
|
|
124
|
+
|
|
125
|
+
@mutex.synchronize do
|
|
126
|
+
return self if ready?
|
|
127
|
+
|
|
128
|
+
@state = :running
|
|
129
|
+
end
|
|
130
|
+
@bytes = generate_bytes
|
|
131
|
+
@mutex.synchronize do
|
|
132
|
+
@state = :ready
|
|
133
|
+
@task = nil
|
|
134
|
+
end
|
|
135
|
+
self
|
|
136
|
+
rescue => e
|
|
137
|
+
fail_with(e)
|
|
138
|
+
raise
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def write_now(path, &block)
|
|
142
|
+
return skip_existing(path) if File.file?(path)
|
|
143
|
+
return mark_disk_ready(path) if ready? && materialized_for?(path)
|
|
144
|
+
|
|
145
|
+
temp_path = "#{path}.tmp.#{$$}.#{object_id}"
|
|
146
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
147
|
+
block&.call(generated? ? :generate_start : :write_start, self, path)
|
|
148
|
+
|
|
149
|
+
write_bytes_to(temp_path)
|
|
150
|
+
|
|
151
|
+
File.rename(temp_path, path)
|
|
152
|
+
mark_disk_ready(path)
|
|
153
|
+
rescue => e
|
|
154
|
+
FileUtils.rm_f(temp_path) if temp_path
|
|
155
|
+
fail_with(e)
|
|
156
|
+
raise
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def fail_with(error)
|
|
160
|
+
@mutex.synchronize do
|
|
161
|
+
@error ||= error
|
|
162
|
+
@state = :failed
|
|
163
|
+
@task = nil
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def skip_existing(path)
|
|
168
|
+
@mutex.synchronize do
|
|
169
|
+
@disk_path = path
|
|
170
|
+
@state = :ready
|
|
171
|
+
@task = nil
|
|
172
|
+
end
|
|
173
|
+
:skipped
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def mark_disk_ready(path)
|
|
177
|
+
@mutex.synchronize do
|
|
178
|
+
@disk_path = path
|
|
179
|
+
@state = :ready
|
|
180
|
+
@task = nil
|
|
181
|
+
end
|
|
182
|
+
:written
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def materialized_for?(path)
|
|
186
|
+
@disk_path == path
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def current_async_task
|
|
190
|
+
Async::Task.current
|
|
191
|
+
rescue RuntimeError
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def queued_task(task, &block)
|
|
196
|
+
return @queue.run(kind: queue_kind, &block) if @queue
|
|
197
|
+
|
|
198
|
+
task.async(&block)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def generate_now_or_queued
|
|
202
|
+
return @queue.run(kind: queue_kind) { generate_now } if @queue
|
|
203
|
+
|
|
204
|
+
generate_now
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def write_now_or_queued(path, &block)
|
|
208
|
+
return @queue.run(kind: queue_kind) { write_now(path, &block) } if @queue
|
|
209
|
+
|
|
210
|
+
write_now(path, &block)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def write_bytes_to(path)
|
|
214
|
+
return FileUtils.copy_file(@source_path, path) if static? && @bytes.nil? && @source_path
|
|
215
|
+
|
|
216
|
+
File.open(path, "wb") do |file|
|
|
217
|
+
if @writer
|
|
218
|
+
@writer.call(file)
|
|
219
|
+
else
|
|
220
|
+
file.write(@bytes || generate_bytes)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def generate_bytes
|
|
226
|
+
return @generator.call if @generator
|
|
227
|
+
return File.binread(@source_path) unless @writer
|
|
228
|
+
|
|
229
|
+
io = StringIO.new("".b)
|
|
230
|
+
@writer.call(io)
|
|
231
|
+
io.string
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "brotli"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Klenod
|
|
7
|
+
module Build
|
|
8
|
+
module AssetCompression
|
|
9
|
+
TEXT_CONTENT_TYPES = [
|
|
10
|
+
"application/javascript",
|
|
11
|
+
"application/json",
|
|
12
|
+
"application/manifest+json",
|
|
13
|
+
"application/rss+xml",
|
|
14
|
+
"application/xml",
|
|
15
|
+
"image/svg+xml"
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
def self.compressible?(asset)
|
|
19
|
+
content_type = asset.content_type.to_s.split(";", 2).first
|
|
20
|
+
content_type.start_with?("text/") || TEXT_CONTENT_TYPES.include?(content_type)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.sidecar_path(path)
|
|
24
|
+
"#{path}.br"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.write_sidecar(asset, path)
|
|
28
|
+
return nil unless compressible?(asset)
|
|
29
|
+
|
|
30
|
+
brotli_path = sidecar_path(path)
|
|
31
|
+
return :skipped if File.file?(brotli_path)
|
|
32
|
+
|
|
33
|
+
bytes = asset.bytes
|
|
34
|
+
compressed = Brotli.deflate(bytes, quality: 11, mode: :text)
|
|
35
|
+
return nil unless compressed.bytesize < bytes.bytesize
|
|
36
|
+
|
|
37
|
+
write_atomic(brotli_path, compressed)
|
|
38
|
+
:written
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.remove_sidecar(path)
|
|
42
|
+
brotli_path = sidecar_path(path)
|
|
43
|
+
FileUtils.rm_f(brotli_path)
|
|
44
|
+
brotli_path
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.write_atomic(path, bytes)
|
|
48
|
+
temp_path = "#{path}.tmp.#{$$}.#{object_id}"
|
|
49
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
50
|
+
File.binwrite(temp_path, bytes)
|
|
51
|
+
File.rename(temp_path, path)
|
|
52
|
+
rescue
|
|
53
|
+
FileUtils.rm_f(temp_path) if temp_path
|
|
54
|
+
raise
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/semaphore"
|
|
5
|
+
require "etc"
|
|
6
|
+
|
|
7
|
+
module Klenod
|
|
8
|
+
module Build
|
|
9
|
+
class AssetGenerationQueue
|
|
10
|
+
DEFAULT_CONCURRENCY = [Etc.nprocessors / 2, 2].max
|
|
11
|
+
DEFAULT_DOWNLOAD_CONCURRENCY = 4
|
|
12
|
+
|
|
13
|
+
attr_reader :concurrency, :download_concurrency
|
|
14
|
+
|
|
15
|
+
def initialize(concurrency: DEFAULT_CONCURRENCY, download_concurrency: DEFAULT_DOWNLOAD_CONCURRENCY)
|
|
16
|
+
@concurrency = Integer(concurrency)
|
|
17
|
+
@download_concurrency = Integer(download_concurrency)
|
|
18
|
+
raise ArgumentError, "concurrency must be positive" unless @concurrency.positive?
|
|
19
|
+
raise ArgumentError, "download_concurrency must be positive" unless @download_concurrency.positive?
|
|
20
|
+
|
|
21
|
+
@semaphores = {
|
|
22
|
+
cpu: Async::Semaphore.new(@concurrency),
|
|
23
|
+
io: Async::Semaphore.new(@download_concurrency)
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run(kind: :cpu, &block)
|
|
28
|
+
semaphore = semaphore_for(kind)
|
|
29
|
+
task = current_async_task
|
|
30
|
+
return semaphore.acquire { block.call } unless task
|
|
31
|
+
|
|
32
|
+
semaphore.async(parent: task) { block.call }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def semaphore_for(kind)
|
|
38
|
+
@semaphores.fetch(kind) do
|
|
39
|
+
raise ArgumentError, "unknown asset generation queue kind: #{kind.inspect}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def current_async_task
|
|
44
|
+
Async::Task.current
|
|
45
|
+
rescue RuntimeError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
previous_verbose = $VERBOSE
|
|
5
|
+
$VERBOSE = nil
|
|
6
|
+
require "samovar"
|
|
7
|
+
ensure
|
|
8
|
+
$VERBOSE = previous_verbose
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require_relative "../context"
|
|
12
|
+
require_relative "../graphviz"
|
|
13
|
+
require_relative "../version"
|
|
14
|
+
|
|
15
|
+
module Klenod
|
|
16
|
+
module Build
|
|
17
|
+
module CLI
|
|
18
|
+
class Build < Samovar::Command
|
|
19
|
+
self.description = "Build a Klenod runtime bundle."
|
|
20
|
+
|
|
21
|
+
options do
|
|
22
|
+
option "--executable", "Write a Ruby executable bundle that loads all entrypoints."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call
|
|
26
|
+
config_path = Klenod::Build::ConfigLoader.find
|
|
27
|
+
raise Samovar::InvalidInputError.new(self, ["Could not find klenod.config.rb"]) unless config_path
|
|
28
|
+
|
|
29
|
+
config = nil
|
|
30
|
+
Dir.chdir(File.dirname(config_path)) do
|
|
31
|
+
config = Klenod::Build::ConfigLoader.load(config_path)
|
|
32
|
+
end
|
|
33
|
+
entrypoints = config.entrypoints
|
|
34
|
+
raise Samovar::MissingValueError.new(self, :entry) if entrypoints.empty?
|
|
35
|
+
|
|
36
|
+
output = config.output_path
|
|
37
|
+
assets_dir = config.assets_path
|
|
38
|
+
|
|
39
|
+
bundle = nil
|
|
40
|
+
Dir.chdir(config.base_dir) do
|
|
41
|
+
context = config.context(mode: :build)
|
|
42
|
+
bundle =
|
|
43
|
+
if @options[:executable]
|
|
44
|
+
context.build_executable(entrypoints: entrypoints, output: output, assets_dir: assets_dir)
|
|
45
|
+
else
|
|
46
|
+
context.build(entrypoints: entrypoints, output: output, assets_dir: assets_dir)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
self.output.puts "Built #{build_kind} #{output}"
|
|
51
|
+
self.output.puts "Source root: #{bundle.source_root}"
|
|
52
|
+
self.output.puts "Entrypoints: #{bundle.entrypoints.keys.join(", ")}"
|
|
53
|
+
self.output.puts "Assets: #{bundle.assets.length}"
|
|
54
|
+
self.output.puts "Assets directory: #{assets_dir}" if assets_dir
|
|
55
|
+
|
|
56
|
+
bundle
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def build_kind
|
|
62
|
+
@options[:executable] ? "executable bundle" : "runtime bundle"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class Graph < Samovar::Command
|
|
67
|
+
self.description = "Export a Klenod runtime bundle as Graphviz DOT."
|
|
68
|
+
|
|
69
|
+
options do
|
|
70
|
+
option "--no-assets", "Hide generated asset nodes and edges."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
one :bundle_path, "Path to a Klenod runtime bundle.", required: true
|
|
74
|
+
|
|
75
|
+
def call
|
|
76
|
+
bundle = Klenod::Runtime.load_bundle(@bundle_path)
|
|
77
|
+
dot = Klenod::Build::Graphviz.call(bundle, include_assets: !@options[:no_assets])
|
|
78
|
+
output.write(dot)
|
|
79
|
+
dot
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class Application < Samovar::Command
|
|
84
|
+
self.description = "Build and develop Klenod applications."
|
|
85
|
+
|
|
86
|
+
options do
|
|
87
|
+
option "-h/--help", "Print this help."
|
|
88
|
+
option "-v/--version", "Print the Klenod version."
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
nested :command, {
|
|
92
|
+
"build" => Build,
|
|
93
|
+
"graph" => Graph
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def call
|
|
97
|
+
if @options[:version]
|
|
98
|
+
output.puts Klenod::Build::VERSION
|
|
99
|
+
elsif @options[:help] || !@command
|
|
100
|
+
print_usage
|
|
101
|
+
else
|
|
102
|
+
@command.call
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Klenod
|
|
4
|
+
module Build
|
|
5
|
+
Config =
|
|
6
|
+
Data.define(:source_dir, :entrypoints, :output, :assets_dir, :plugins, :mode, :base_dir) do
|
|
7
|
+
def initialize(source_dir: "src", entrypoints: [], output: "dist/klenod.bundle", assets_dir: nil, plugins: Context::DEFAULT_PLUGINS, mode: :development, base_dir: Dir.pwd)
|
|
8
|
+
super(
|
|
9
|
+
source_dir: source_dir,
|
|
10
|
+
entrypoints: Array(entrypoints),
|
|
11
|
+
output: output,
|
|
12
|
+
assets_dir: assets_dir,
|
|
13
|
+
plugins: plugins,
|
|
14
|
+
mode: mode,
|
|
15
|
+
base_dir: base_dir
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def with(**attributes)
|
|
20
|
+
self.class.new(**to_h.merge(attributes))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def context(**overrides)
|
|
24
|
+
Context.new(
|
|
25
|
+
source_dir: source_path,
|
|
26
|
+
plugins: plugins,
|
|
27
|
+
mode: mode,
|
|
28
|
+
**overrides
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def source_path
|
|
33
|
+
expand_path(source_dir)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def output_path
|
|
37
|
+
expand_path(output)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def assets_path
|
|
41
|
+
assets_dir && expand_path(assets_dir)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def expand_path(path)
|
|
47
|
+
path = path.to_s
|
|
48
|
+
return File.expand_path(path) if path.start_with?("/")
|
|
49
|
+
|
|
50
|
+
File.expand_path(path, base_dir)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class ConfigBuilder
|
|
55
|
+
def initialize(base_dir: Dir.pwd)
|
|
56
|
+
@source_dir = "src"
|
|
57
|
+
@entrypoints = []
|
|
58
|
+
@output = "dist/klenod.bundle"
|
|
59
|
+
@assets_dir = nil
|
|
60
|
+
@plugins = Context::DEFAULT_PLUGINS
|
|
61
|
+
@mode = :development
|
|
62
|
+
@base_dir = base_dir
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def source_dir(value = nil)
|
|
66
|
+
return @source_dir unless value
|
|
67
|
+
|
|
68
|
+
@source_dir = value
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def entrypoint(value)
|
|
72
|
+
@entrypoints << value
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def entrypoints(*values)
|
|
76
|
+
@entrypoints.concat(values.flatten)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def output(value = nil)
|
|
80
|
+
return @output unless value
|
|
81
|
+
|
|
82
|
+
@output = value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def assets_dir(value = nil)
|
|
86
|
+
return @assets_dir unless value
|
|
87
|
+
|
|
88
|
+
@assets_dir = value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def plugins(value = nil, &block)
|
|
92
|
+
return @plugins unless value || block
|
|
93
|
+
|
|
94
|
+
@plugins = block ? block.call : value
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def mode(value = nil)
|
|
98
|
+
return @mode unless value
|
|
99
|
+
|
|
100
|
+
@mode = value
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def config
|
|
104
|
+
Config.new(
|
|
105
|
+
source_dir: @source_dir,
|
|
106
|
+
entrypoints: @entrypoints,
|
|
107
|
+
output: @output,
|
|
108
|
+
assets_dir: @assets_dir,
|
|
109
|
+
plugins: @plugins,
|
|
110
|
+
mode: @mode,
|
|
111
|
+
base_dir: @base_dir
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
module ConfigLoader
|
|
117
|
+
CONFIG_FILE = "klenod.config.rb"
|
|
118
|
+
|
|
119
|
+
module_function
|
|
120
|
+
|
|
121
|
+
def find(start_dir = Dir.pwd)
|
|
122
|
+
dir = File.expand_path(start_dir)
|
|
123
|
+
|
|
124
|
+
loop do
|
|
125
|
+
path = File.join(dir, CONFIG_FILE)
|
|
126
|
+
return path if File.file?(path)
|
|
127
|
+
|
|
128
|
+
parent = File.dirname(dir)
|
|
129
|
+
return nil if parent == dir
|
|
130
|
+
|
|
131
|
+
dir = parent
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def load(path)
|
|
136
|
+
builder = ConfigBuilder.new(base_dir: File.dirname(File.expand_path(path)))
|
|
137
|
+
builder.instance_eval(File.read(path), path, 1)
|
|
138
|
+
builder.config
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|