lilac-cli 0.2.0
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/LICENSE.txt +21 -0
- data/README.md +144 -0
- data/exe/lilac +6 -0
- data/lib/lilac/cli/build/build_context.rb +31 -0
- data/lib/lilac/cli/build/build_error.rb +50 -0
- data/lib/lilac/cli/build/builder.rb +326 -0
- data/lib/lilac/cli/build/bundle_asset_writer.rb +129 -0
- data/lib/lilac/cli/build/bytecode_builder.rb +212 -0
- data/lib/lilac/cli/build/compiled_boot_module.rb +103 -0
- data/lib/lilac/cli/build/compiled_runtime_resolver.rb +64 -0
- data/lib/lilac/cli/build/component_name.rb +57 -0
- data/lib/lilac/cli/build/component_scripts_assembler.rb +76 -0
- data/lib/lilac/cli/build/directive.rb +51 -0
- data/lib/lilac/cli/build/full_runtime_resolver.rb +61 -0
- data/lib/lilac/cli/build/html_emitter.rb +58 -0
- data/lib/lilac/cli/build/package_stager.rb +111 -0
- data/lib/lilac/cli/build/page_compiler.rb +405 -0
- data/lib/lilac/cli/build/runtime_resolver.rb +150 -0
- data/lib/lilac/cli/build/sfc.rb +150 -0
- data/lib/lilac/cli/build/template_ast.rb +304 -0
- data/lib/lilac/cli/build/template_ast_cache.rb +58 -0
- data/lib/lilac/cli/build/vendor_writer.rb +58 -0
- data/lib/lilac/cli/build/wasm_mrbc_driver.rb +183 -0
- data/lib/lilac/cli/command.rb +110 -0
- data/lib/lilac/cli/config.rb +150 -0
- data/lib/lilac/cli/config_loader.rb +97 -0
- data/lib/lilac/cli/dev_server.rb +156 -0
- data/lib/lilac/cli/doctor.rb +310 -0
- data/lib/lilac/cli/lint/build_linter.rb +95 -0
- data/lib/lilac/cli/lint/cross_ref_linter.rb +336 -0
- data/lib/lilac/cli/lint/lint_warning.rb +53 -0
- data/lib/lilac/cli/lint/script_analyzer.rb +255 -0
- data/lib/lilac/cli/live_reload.rb +194 -0
- data/lib/lilac/cli/offline_verifier.rb +117 -0
- data/lib/lilac/cli/package_build.rb +68 -0
- data/lib/lilac/cli/package_discovery.rb +77 -0
- data/lib/lilac/cli/preview_server.rb +70 -0
- data/lib/lilac/cli/scaffold.rb +85 -0
- data/lib/lilac/cli/source_location.rb +16 -0
- data/lib/lilac/cli/subcommand/base.rb +65 -0
- data/lib/lilac/cli/subcommand/build.rb +82 -0
- data/lib/lilac/cli/subcommand/dev.rb +58 -0
- data/lib/lilac/cli/subcommand/doctor.rb +39 -0
- data/lib/lilac/cli/subcommand/new.rb +81 -0
- data/lib/lilac/cli/subcommand/option_helpers.rb +58 -0
- data/lib/lilac/cli/subcommand/package_build.rb +51 -0
- data/lib/lilac/cli/subcommand/preview.rb +50 -0
- data/lib/lilac/cli/templates/Gemfile +13 -0
- data/lib/lilac/cli/templates/README.md +125 -0
- data/lib/lilac/cli/templates/components/counter.lil +18 -0
- data/lib/lilac/cli/templates/gitignore +5 -0
- data/lib/lilac/cli/templates/lilac.config.rb +24 -0
- data/lib/lilac/cli/templates/pages/index.html +46 -0
- data/lib/lilac/cli/templates/public/.gitkeep +0 -0
- data/lib/lilac/cli/version.rb +7 -0
- data/lib/lilac/cli/watcher.rb +62 -0
- data/lib/lilac/cli.rb +20 -0
- data/lib/lilac/directives/class_parser.rb +179 -0
- data/lib/lilac/directives/collision_rules.rb +49 -0
- data/lib/lilac/directives/grammar.rb +76 -0
- data/lib/lilac/directives/lints.rb +128 -0
- data/lib/lilac/directives/value.rb +85 -0
- data/lib/lilac/directives.rb +16 -0
- metadata +159 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require_relative 'html_emitter'
|
|
5
|
+
|
|
6
|
+
module Lilac
|
|
7
|
+
module CLI
|
|
8
|
+
# In :bundle delivery mode, emits all components' default templates,
|
|
9
|
+
# named templates, and Ruby scripts into a single
|
|
10
|
+
# `dist/lilac.bundle.html`. Pages reference it via
|
|
11
|
+
# `<link rel="lilac-bundle">`, and PageCompiler's boot module / the
|
|
12
|
+
# runtime registry fetches it before mount.
|
|
13
|
+
#
|
|
14
|
+
# For the :compiled target, scripts are NOT inlined into the bundle
|
|
15
|
+
# (the compiled wasm has no parser to evaluate text/ruby). Instead
|
|
16
|
+
# they are aggregated and compiled into a single `bundle.mrb`, plus
|
|
17
|
+
# a tiny standalone `start_only.mrb` (just `Lilac.start`) — both
|
|
18
|
+
# filenames ride on the returned `BundleAssets` so PageCompiler can
|
|
19
|
+
# chain them into each page's boot module.
|
|
20
|
+
#
|
|
21
|
+
# See decisions §20.6 (Lilac.start placement) and the bundle-fetch
|
|
22
|
+
# proposal for the rationale behind splitting bundle.mrb /
|
|
23
|
+
# start_only.mrb instead of appending `Lilac.start` to the bundle.
|
|
24
|
+
class BundleAssetWriter
|
|
25
|
+
# Build-level artifacts handed back to the orchestrator (Builder)
|
|
26
|
+
# and forwarded into the `BuildContext` PageCompiler reads. `url`
|
|
27
|
+
# is what gets stamped into each page's `<link rel="lilac-bundle">`;
|
|
28
|
+
# the two `.mrb` filenames are only populated when
|
|
29
|
+
# `target == :compiled` (otherwise scripts go into the bundle.html
|
|
30
|
+
# itself as <script type="text/ruby">).
|
|
31
|
+
BundleAssets = Struct.new(:url, :bundle_mrb, :start_only_mrb, keyword_init: true)
|
|
32
|
+
|
|
33
|
+
BUNDLE_FILENAME = 'lilac.bundle.html'
|
|
34
|
+
BUNDLE_URL = "/#{BUNDLE_FILENAME}"
|
|
35
|
+
|
|
36
|
+
def initialize(components:, template_cache:, target:,
|
|
37
|
+
output_dir:, bytecode_builder:)
|
|
38
|
+
@components = components
|
|
39
|
+
@template_cache = template_cache
|
|
40
|
+
@target = target
|
|
41
|
+
@output_dir = output_dir
|
|
42
|
+
@bytecode_builder = bytecode_builder
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Writes the bundle file and (for :compiled) compiles bundle.mrb +
|
|
46
|
+
# start_only.mrb. Returns a `BundleAssets`, or nil when there are
|
|
47
|
+
# no components to bundle.
|
|
48
|
+
def write!
|
|
49
|
+
return nil if @components.empty?
|
|
50
|
+
|
|
51
|
+
parts, compiled_scripts = assemble_parts
|
|
52
|
+
write_html_file!(parts)
|
|
53
|
+
bundle_mrb, start_only_mrb = compile_compiled_scripts!(compiled_scripts)
|
|
54
|
+
|
|
55
|
+
BundleAssets.new(
|
|
56
|
+
url: BUNDLE_URL,
|
|
57
|
+
bundle_mrb: bundle_mrb,
|
|
58
|
+
start_only_mrb: start_only_mrb
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Walks every component once, producing the HTML `parts` that go
|
|
65
|
+
# into bundle.html and the deferred `compiled_scripts` list that
|
|
66
|
+
# gets compiled to bytecode (only on :compiled target — :full puts
|
|
67
|
+
# the script bodies inline into `parts` as <script type="text/ruby">).
|
|
68
|
+
def assemble_parts
|
|
69
|
+
parts = []
|
|
70
|
+
compiled_scripts = []
|
|
71
|
+
|
|
72
|
+
@components.each do |name, comp|
|
|
73
|
+
parsed = @template_cache.fetch(name, comp)
|
|
74
|
+
|
|
75
|
+
# Default template
|
|
76
|
+
parts << "<template>#{parsed[:default_html]}</template>"
|
|
77
|
+
|
|
78
|
+
# Named templates (user-defined + synthetic data-each rows)
|
|
79
|
+
parsed[:named].each do |nt|
|
|
80
|
+
parts << HtmlEmitter.render_named_template(nt.name, nt.html)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Scanner-canonical: emit the user script as-is (no codegen
|
|
84
|
+
# bindings). Skipped entirely when no user_script exists.
|
|
85
|
+
user_script = comp.script.strip
|
|
86
|
+
next if user_script.empty?
|
|
87
|
+
|
|
88
|
+
if @target == :compiled
|
|
89
|
+
# Defer: scripts get aggregated into one .mrb below.
|
|
90
|
+
compiled_scripts << user_script
|
|
91
|
+
else
|
|
92
|
+
parts << HtmlEmitter.render_script(user_script)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
[parts, compiled_scripts]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def write_html_file!(parts)
|
|
100
|
+
FileUtils.mkdir_p(@output_dir)
|
|
101
|
+
bundle_path = File.join(@output_dir, BUNDLE_FILENAME)
|
|
102
|
+
File.write(bundle_path, parts.join("\n") + "\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# :compiled — compile aggregated scripts into a .mrb that the
|
|
106
|
+
# page boot module loads via loadBytecode. `Lilac.start` is NOT
|
|
107
|
+
# appended to the bundle: when a page has its own page-inline
|
|
108
|
+
# .mrb chained after the bundle, that one terminates with
|
|
109
|
+
# `Lilac.start`; when a page has only the bundle, we emit a
|
|
110
|
+
# tiny start-only .mrb so the chain still ends with `Lilac.start`
|
|
111
|
+
# running once after both class definitions and any page-local
|
|
112
|
+
# scripts are loaded.
|
|
113
|
+
def compile_compiled_scripts!(compiled_scripts)
|
|
114
|
+
return [nil, nil] unless @target == :compiled && !compiled_scripts.empty?
|
|
115
|
+
|
|
116
|
+
bundle_mrb = @bytecode_builder.build(
|
|
117
|
+
compiled_scripts.join("\n\n"), source_label: 'bundle'
|
|
118
|
+
)
|
|
119
|
+
# Standalone `Lilac.start` .mrb, shared by all pages that have
|
|
120
|
+
# no page-inline scripts. Content-hashed so cache reuses it
|
|
121
|
+
# across pages.
|
|
122
|
+
start_only_mrb = @bytecode_builder.build(
|
|
123
|
+
'Lilac.start', source_label: 'bundle-start'
|
|
124
|
+
)
|
|
125
|
+
[bundle_mrb, start_only_mrb]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
|
|
8
|
+
require_relative 'build_error'
|
|
9
|
+
require_relative 'wasm_mrbc_driver'
|
|
10
|
+
|
|
11
|
+
module Lilac
|
|
12
|
+
module CLI
|
|
13
|
+
# Compiles aggregated Ruby source to mruby bytecode, then writes it
|
|
14
|
+
# under the build output with a content-hash filename so browsers
|
|
15
|
+
# cache-invalidate automatically when the bytes change.
|
|
16
|
+
#
|
|
17
|
+
# Used by `Builder` when `target == :compiled`. Owns the compile
|
|
18
|
+
# subprocess (or wasm driver) lifecycle and error translation;
|
|
19
|
+
# `Builder` keeps the HTML / vendor concerns.
|
|
20
|
+
#
|
|
21
|
+
# Backend dispatch — first hit wins:
|
|
22
|
+
#
|
|
23
|
+
# 1. explicit `mrbc_path:` (from `Lilac::CLI.configure { c.mrbc_path = ... }`
|
|
24
|
+
# or `--mrbc-path` CLI flag) → :binary
|
|
25
|
+
# 2. ENV["MRBC"] → :binary
|
|
26
|
+
# 3. ENV["MRUBY_WASM_RUNTIME_PATH"]/mruby/build/host/bin/mrbc → :binary
|
|
27
|
+
# 4. `lilac-wasm-bin` gem ships `mrbc-host.wasm` AND wasmtime-rb
|
|
28
|
+
# loadable AND module instantiates with required exports → :wasm
|
|
29
|
+
# 5. `mrbc` on $PATH → :binary
|
|
30
|
+
#
|
|
31
|
+
# The wasm-driven backend is the "default for end users" path — a
|
|
32
|
+
# scaffolded `lilac new` Gemfile pulls in `lilac-wasm-bin` and
|
|
33
|
+
# `wasmtime`, so `gem install lilac-cli && bundle install` is enough
|
|
34
|
+
# to make `lilac build --target compiled` work without any external
|
|
35
|
+
# binary. Explicit `mrbc_path` / ENV vars override (priority 1-3) so
|
|
36
|
+
# devs who built mruby themselves get the native path.
|
|
37
|
+
#
|
|
38
|
+
# If nothing resolves, raises BuildError pointing at the most likely
|
|
39
|
+
# fix paths.
|
|
40
|
+
class BytecodeBuilder
|
|
41
|
+
class Error < BuildError; end
|
|
42
|
+
|
|
43
|
+
# 8 hex chars (32 bits) — enough collision resistance for cache
|
|
44
|
+
# busting, short enough to keep filenames readable.
|
|
45
|
+
HASH_LENGTH = 8
|
|
46
|
+
|
|
47
|
+
def initialize(output_dir: nil, mrbc_path: nil, basename: 'app',
|
|
48
|
+
disable_gem_discovery: false)
|
|
49
|
+
@configured_mrbc_path = mrbc_path
|
|
50
|
+
@output_dir = output_dir
|
|
51
|
+
@basename = basename
|
|
52
|
+
# Mirrors `CompiledRuntimeResolver`'s kwarg — lets the bytecode
|
|
53
|
+
# builder's tests sandbox out the gem's `mrbc-host.wasm` when
|
|
54
|
+
# they want to assert specific discovery fallbacks. Production
|
|
55
|
+
# callers leave it false and the gem's wasm is picked up by
|
|
56
|
+
# default (priority #4).
|
|
57
|
+
@disable_gem_discovery = disable_gem_discovery
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Compile a Ruby source string into a `.mrb` file under
|
|
61
|
+
# `output_dir`. Returns the basename of the produced file (e.g.
|
|
62
|
+
# `"app.a3f29b21.mrb"`) so the caller can wire it into a fetch URL.
|
|
63
|
+
def build(ruby_source, source_label: '(aggregated)')
|
|
64
|
+
raise Error, '`build` requires `output_dir:` at construction time' unless @output_dir
|
|
65
|
+
FileUtils.mkdir_p(@output_dir)
|
|
66
|
+
bytecode = compile_to_bytes(ruby_source, source_label: source_label)
|
|
67
|
+
filename = "#{@basename}.#{content_hash(bytecode)}.mrb"
|
|
68
|
+
dest = File.join(@output_dir, filename)
|
|
69
|
+
File.binwrite(dest, bytecode)
|
|
70
|
+
filename
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Compile a Ruby source string into raw mruby bytecode (no write,
|
|
74
|
+
# no hashing). Used by `lilac package-build` which wants explicit
|
|
75
|
+
# control over output path / filename — it has no need for the
|
|
76
|
+
# content-hash cache-busting that `build` does for `.lil` apps.
|
|
77
|
+
def compile_to_bytes(ruby_source, source_label: '(aggregated)')
|
|
78
|
+
backend = resolve_backend!
|
|
79
|
+
case backend.first
|
|
80
|
+
when :binary then compile_via_binary(backend.last, ruby_source, source_label)
|
|
81
|
+
when :wasm then compile_via_wasm(backend.last, ruby_source, source_label)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Diagnostic accessor: returns `[:binary, "/path"]` or
|
|
86
|
+
# `[:wasm, "/path/to/wasm"]` describing which backend `build` would
|
|
87
|
+
# select, or nil when nothing resolves. Doesn't raise — `lilac
|
|
88
|
+
# doctor` uses it to render the OK / WARN line; `build` calls
|
|
89
|
+
# `resolve_backend!` instead.
|
|
90
|
+
def resolve_backend
|
|
91
|
+
if (binary = resolve_mrbc_binary)
|
|
92
|
+
return [:binary, binary]
|
|
93
|
+
end
|
|
94
|
+
if (wasm = discoverable_mrbc_host_wasm) && WasmMrbcDriver.available?(wasm_path: wasm)
|
|
95
|
+
return [:wasm, wasm]
|
|
96
|
+
end
|
|
97
|
+
if (path_binary = path_lookup('mrbc'))
|
|
98
|
+
return [:binary, path_binary]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Resolved mrbc binary path for `lilac doctor` and back-compat.
|
|
105
|
+
# Nil when no binary is discoverable (gem-provided wasm doesn't
|
|
106
|
+
# count). Renamed-but-kept-as-alias for callers that pre-date the
|
|
107
|
+
# backend dispatch (e.g. existing tests).
|
|
108
|
+
def resolve_mrbc
|
|
109
|
+
resolve_mrbc_binary || path_lookup('mrbc')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Discovery routes 1-3 from the docstring above — explicit
|
|
115
|
+
# arg / ENV / monorepo convention. The PATH fallback (#5) lives
|
|
116
|
+
# in `resolve_backend` so it's only consulted after the wasm
|
|
117
|
+
# backend has been tried.
|
|
118
|
+
def resolve_mrbc_binary
|
|
119
|
+
@configured_mrbc_path && File.executable?(@configured_mrbc_path) and return @configured_mrbc_path
|
|
120
|
+
if (env = ENV['MRBC']) && File.executable?(env)
|
|
121
|
+
return env
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if (mwr = ENV['MRUBY_WASM_RUNTIME_PATH'])
|
|
125
|
+
candidate = File.join(mwr, 'mruby', 'build', 'host', 'bin', 'mrbc')
|
|
126
|
+
return candidate if File.executable?(candidate)
|
|
127
|
+
end
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def discoverable_mrbc_host_wasm
|
|
132
|
+
return nil if @disable_gem_discovery
|
|
133
|
+
|
|
134
|
+
require 'lilac/wasm/bin'
|
|
135
|
+
::Lilac::Wasm::Bin.mrbc_host_wasm
|
|
136
|
+
rescue LoadError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def resolve_backend!
|
|
141
|
+
resolve_backend || raise(Error, backend_not_found_message)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def compile_via_binary(mrbc, ruby_source, source_label)
|
|
145
|
+
Dir.mktmpdir('lilac-mrbc-') do |dir|
|
|
146
|
+
tmp_rb = File.join(dir, 'input.rb')
|
|
147
|
+
tmp_mrb = File.join(dir, 'input.mrb')
|
|
148
|
+
File.write(tmp_rb, ruby_source)
|
|
149
|
+
|
|
150
|
+
stdout, stderr, status = Open3.capture3(mrbc, '-o', tmp_mrb, tmp_rb)
|
|
151
|
+
raise Error, binary_error_message(source_label, stdout, stderr, status) unless status.success?
|
|
152
|
+
|
|
153
|
+
File.binread(tmp_mrb)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def compile_via_wasm(wasm_path, ruby_source, source_label)
|
|
158
|
+
# Reuse the driver across `build` calls within a single
|
|
159
|
+
# invocation — engine + module + instance setup is the slow part
|
|
160
|
+
# (~200-500ms cold) and is amortized over every component.
|
|
161
|
+
@wasm_driver ||= WasmMrbcDriver.new(wasm_path: wasm_path)
|
|
162
|
+
@wasm_driver.compile(ruby_source)
|
|
163
|
+
rescue WasmMrbcDriver::CompileError => e
|
|
164
|
+
raise Error, "mrbc-host.wasm failed compiling #{source_label}:\n#{e.message}"
|
|
165
|
+
rescue WasmMrbcDriver::WasmtimeMissingError, WasmMrbcDriver::WasmExportMissingError => e
|
|
166
|
+
# Shouldn't reach here under normal flow — `resolve_backend!`
|
|
167
|
+
# only returns `:wasm` after `available?` succeeded — but if the
|
|
168
|
+
# wasm path goes stale mid-build (e.g. file removed), surface
|
|
169
|
+
# the typed error rather than crashing.
|
|
170
|
+
raise Error, "wasm mrbc backend unavailable: #{e.message}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def content_hash(bytes)
|
|
174
|
+
Digest::SHA256.hexdigest(bytes)[0, HASH_LENGTH]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Walk $PATH for `name`. Falls back to nil when not found —
|
|
178
|
+
# callers translate to a user-facing error with
|
|
179
|
+
# `backend_not_found_message`.
|
|
180
|
+
def path_lookup(name)
|
|
181
|
+
(ENV['PATH'] || '').split(File::PATH_SEPARATOR).each do |dir|
|
|
182
|
+
candidate = File.join(dir, name)
|
|
183
|
+
return candidate if File.executable?(candidate) && !File.directory?(candidate)
|
|
184
|
+
end
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def backend_not_found_message
|
|
189
|
+
<<~MSG.strip
|
|
190
|
+
No mrbc backend found. Tried (in priority order):
|
|
191
|
+
1. configured `c.mrbc_path` / `--mrbc-path`
|
|
192
|
+
2. ENV["MRBC"]
|
|
193
|
+
3. ENV["MRUBY_WASM_RUNTIME_PATH"]/mruby/build/host/bin/mrbc
|
|
194
|
+
4. lilac-wasm-bin gem's mrbc-host.wasm (wasmtime-rb driven)
|
|
195
|
+
5. `mrbc` on $PATH
|
|
196
|
+
|
|
197
|
+
To fix, either:
|
|
198
|
+
• Add `gem "lilac-wasm-bin"` and `gem "wasmtime"` to your Gemfile
|
|
199
|
+
(recommended — the scaffolded Gemfile from `lilac new` already does this)
|
|
200
|
+
• Set ENV["MRBC"]=/abs/path/to/mrbc
|
|
201
|
+
• Set ENV["MRUBY_WASM_RUNTIME_PATH"] to a built mruby-wasm-runtime checkout
|
|
202
|
+
• Add `c.mrbc_path = "/abs/path"` to lilac.config.rb
|
|
203
|
+
• Put `mrbc` on your $PATH (e.g. build mruby locally)
|
|
204
|
+
MSG
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def binary_error_message(source_label, _stdout, stderr, status)
|
|
208
|
+
"mrbc failed (exit=#{status.exitstatus}) compiling #{source_label}:\n#{stderr.strip}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lilac
|
|
4
|
+
module CLI
|
|
5
|
+
# Emits the `<script type="module">` boot stub for the `:compiled`
|
|
6
|
+
# target: it loads `.mrb` bytecode, boots the lilac-compiled wasm,
|
|
7
|
+
# and (in :bundle delivery) pulls the bundle's `<template>` elements
|
|
8
|
+
# into the live document before `Lilac.start` runs.
|
|
9
|
+
#
|
|
10
|
+
# Split out of PageCompiler so the JS-string generation lives next to
|
|
11
|
+
# HtmlEmitter's markup-emitting peers rather than mixing into the
|
|
12
|
+
# per-page compile flow.
|
|
13
|
+
module CompiledBootModule
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Decide the .mrb load order for a :compiled × :bundle page's boot
|
|
17
|
+
# module. The bundle .mrb always loads first (component class
|
|
18
|
+
# definitions, no `Lilac.start`). The tail is either the page-local
|
|
19
|
+
# .mrb (which itself ends with `Lilac.start`) or the shared
|
|
20
|
+
# `start-only.mrb` fallback so every page's chain terminates with
|
|
21
|
+
# `Lilac.start`.
|
|
22
|
+
def mrb_chain(bundle_assets, page_local_mrb)
|
|
23
|
+
return [] unless bundle_assets
|
|
24
|
+
|
|
25
|
+
chain = []
|
|
26
|
+
chain << bundle_assets.bundle_mrb if bundle_assets.bundle_mrb
|
|
27
|
+
if page_local_mrb
|
|
28
|
+
chain << page_local_mrb
|
|
29
|
+
elsif bundle_assets.bundle_mrb && bundle_assets.start_only_mrb
|
|
30
|
+
chain << bundle_assets.start_only_mrb
|
|
31
|
+
end
|
|
32
|
+
chain
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Emits the module script that loads `.mrb` bytecode and boots
|
|
36
|
+
# the lilac-compiled wasm. Inlines the boot logic instead of
|
|
37
|
+
# depending on `@takahashim/lilac-compiled`'s published `index.js`
|
|
38
|
+
# — the npm boot helper has occasionally drifted from the bridge's
|
|
39
|
+
# current API (e.g. `loadIrep` rename → `loadBytecode`) and a
|
|
40
|
+
# self-contained module is one fewer moving part to keep in sync.
|
|
41
|
+
# The `data-lilac-bootstrap` attribute marks the tag so a future
|
|
42
|
+
# asset-pipeline pass can rewrite the URLs.
|
|
43
|
+
#
|
|
44
|
+
# `Lilac.start` is NOT called here via `vm.eval`: the compiled
|
|
45
|
+
# wasm excludes `mruby-compiler` / `mruby-eval`, so post-load
|
|
46
|
+
# eval of arbitrary Ruby source is unsupported. Instead the
|
|
47
|
+
# builder appends `Lilac.start` to the bundle so it runs as part
|
|
48
|
+
# of `loadBytecode` (decisions §20.6 caveat).
|
|
49
|
+
def render(mrb_filenames, package_urls: [], delivery: :inline)
|
|
50
|
+
# Accept either a single filename or an array (used by the
|
|
51
|
+
# :bundle delivery path to chain bundle.mrb + page-inline.mrb in
|
|
52
|
+
# one VM).
|
|
53
|
+
mrb_filenames = Array(mrb_filenames)
|
|
54
|
+
|
|
55
|
+
# Package `.mrb` bundles load BEFORE the user bytecode so any
|
|
56
|
+
# `Scanner.register("ClassName")` calls (and the Handler classes
|
|
57
|
+
# they refer to) are ready by the time component mount runs.
|
|
58
|
+
# Mirrors the load ordering in `npm/lilac-compiled/index.js`'s
|
|
59
|
+
# boot helper.
|
|
60
|
+
package_loads = package_urls.map do |url|
|
|
61
|
+
"vm.loadBytecode(new Uint8Array(await (await fetch(#{url.inspect})).arrayBuffer()));"
|
|
62
|
+
end
|
|
63
|
+
bytecode_loads = mrb_filenames.map do |filename|
|
|
64
|
+
%(vm.loadBytecode(new Uint8Array(await (await fetch("./#{filename}")).arrayBuffer())));
|
|
65
|
+
end
|
|
66
|
+
all_loads = (package_loads + bytecode_loads).join("\n ")
|
|
67
|
+
|
|
68
|
+
# :bundle delivery: the compiled wasm has no parser, so unlike
|
|
69
|
+
# the :full boot helper we can't `vm.eval` bundle scripts —
|
|
70
|
+
# those land in the chained .mrb above. But we still need to
|
|
71
|
+
# pull the bundle's <template> elements into the live document
|
|
72
|
+
# before `Lilac.start` runs (which is in the page-local /
|
|
73
|
+
# start-only .mrb). Fetch + DOMParser the bundle and append
|
|
74
|
+
# each <template>; scripts inside the bundle are intentionally
|
|
75
|
+
# ignored (they don't exist for :compiled bundles).
|
|
76
|
+
bundle_block =
|
|
77
|
+
if delivery == :bundle
|
|
78
|
+
<<~JS.chomp.gsub(/^/, ' ')
|
|
79
|
+
for (const link of document.querySelectorAll('link[rel="lilac-bundle"]')) {
|
|
80
|
+
const res = await fetch(link.getAttribute("href"));
|
|
81
|
+
const doc = new DOMParser().parseFromString(await res.text(), "text/html");
|
|
82
|
+
for (const tpl of doc.querySelectorAll("template")) {
|
|
83
|
+
document.body.appendChild(tpl.cloneNode(true));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
JS
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
body_parts = []
|
|
90
|
+
body_parts << bundle_block if bundle_block
|
|
91
|
+
body_parts << " #{all_loads}" unless all_loads.empty?
|
|
92
|
+
|
|
93
|
+
<<~HTML.strip
|
|
94
|
+
<script type="module" data-lilac-bootstrap>
|
|
95
|
+
import { createVM } from "./vendor/lilac-compiled/mruby-wasm-js/index.js";
|
|
96
|
+
const vm = await createVM({ wasm: "./vendor/lilac-compiled/lilac.wasm" });
|
|
97
|
+
#{body_parts.join("\n")}
|
|
98
|
+
</script>
|
|
99
|
+
HTML
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "runtime_resolver"
|
|
4
|
+
|
|
5
|
+
module Lilac
|
|
6
|
+
module CLI
|
|
7
|
+
# Locates the `target: :compiled` runtime that gets vendored into
|
|
8
|
+
# `dist/vendor/lilac-compiled/`:
|
|
9
|
+
#
|
|
10
|
+
# * `lilac.wasm` — the compiled-variant wasm bundle (vendored
|
|
11
|
+
# name; the monorepo/gem source is
|
|
12
|
+
# `lilac-compiled.wasm`)
|
|
13
|
+
# * `mruby-wasm-js/` — the JS↔mruby bridge (resolved by the shared
|
|
14
|
+
# base — same artifact as the :full target)
|
|
15
|
+
#
|
|
16
|
+
# The boot module is rendered inline by the builder
|
|
17
|
+
# (`render_compiled_boot_module`); we don't ship the npm package's
|
|
18
|
+
# `index.js`. Only the wasm-specific discovery differs from `:full`,
|
|
19
|
+
# so this class supplies just those hooks. See `RuntimeResolver`.
|
|
20
|
+
class CompiledRuntimeResolver < RuntimeResolver
|
|
21
|
+
# Distinct from FullRuntimeResolver::Error; both < RuntimeResolver::Error.
|
|
22
|
+
class Error < RuntimeResolver::Error; end
|
|
23
|
+
|
|
24
|
+
def initialize(lilac_compiled_path: nil, **opts)
|
|
25
|
+
super(lilac_wasm_path: lilac_compiled_path, **opts)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def wasm_env_key
|
|
31
|
+
"LILAC_COMPILED_WASM"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def gem_provided_wasm
|
|
35
|
+
return nil if @disable_gem_discovery
|
|
36
|
+
require "lilac/wasm/bin"
|
|
37
|
+
::Lilac::Wasm::Bin.lilac_compiled_wasm
|
|
38
|
+
rescue LoadError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def monorepo_wasm_candidate
|
|
43
|
+
File.join(monorepo_root, "build", "lilac-compiled.wasm")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def wasm_not_found_message
|
|
47
|
+
<<~MSG.strip
|
|
48
|
+
lilac-compiled.wasm not found. Tried:
|
|
49
|
+
• configured `c.lilac_compiled_path` (#{@configured_wasm_path.inspect})
|
|
50
|
+
• ENV["LILAC_COMPILED_WASM"] (#{ENV["LILAC_COMPILED_WASM"].inspect})
|
|
51
|
+
• lilac-wasm-bin gem (not on load path)
|
|
52
|
+
• monorepo: #{monorepo_wasm_candidate}
|
|
53
|
+
|
|
54
|
+
To fix, either:
|
|
55
|
+
• Add `gem "lilac-wasm-bin"` to your Gemfile (recommended — the scaffolded Gemfile from `lilac new` already includes it)
|
|
56
|
+
• Pass `--lilac-compiled-path /abs/path/to/lilac.wasm` on the command line
|
|
57
|
+
• Add `c.lilac_compiled_path = "/abs/path"` to lilac.config.rb
|
|
58
|
+
• Set ENV["LILAC_COMPILED_WASM"]
|
|
59
|
+
• In the monorepo: run `make lilac-compiled` to produce build/lilac-compiled.wasm
|
|
60
|
+
MSG
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lilac
|
|
4
|
+
module CLI
|
|
5
|
+
# Value object for a `.lil` component's kebab-case name. Holds the
|
|
6
|
+
# raw string and exposes the derived forms the build pipeline
|
|
7
|
+
# needs:
|
|
8
|
+
#
|
|
9
|
+
# name = ComponentName.new("admin--user-card")
|
|
10
|
+
# name.ruby_class # => "Admin::UserCard"
|
|
11
|
+
# name.to_s # => "admin--user-card"
|
|
12
|
+
#
|
|
13
|
+
# `--` separates namespace segments, `-` separates words within a
|
|
14
|
+
# segment. Empty segments (leading/trailing `--`, double `-` between
|
|
15
|
+
# words) raise at construction so misnamed files fail loudly rather
|
|
16
|
+
# than emitting `::Foo` / `Foo::` downstream.
|
|
17
|
+
class ComponentName
|
|
18
|
+
attr_reader :kebab
|
|
19
|
+
|
|
20
|
+
def initialize(kebab)
|
|
21
|
+
@kebab = kebab.to_s
|
|
22
|
+
@ruby_class = build_ruby_class
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ruby_class
|
|
26
|
+
@ruby_class
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s
|
|
30
|
+
@kebab
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ==(other)
|
|
34
|
+
other.is_a?(ComponentName) && other.kebab == @kebab
|
|
35
|
+
end
|
|
36
|
+
alias_method :eql?, :==
|
|
37
|
+
|
|
38
|
+
def hash
|
|
39
|
+
@kebab.hash
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_ruby_class
|
|
45
|
+
@kebab.split("--", -1).map { |segment|
|
|
46
|
+
raise ArgumentError, "Invalid component name: #{@kebab.inspect} (empty namespace segment)" if segment.empty?
|
|
47
|
+
|
|
48
|
+
segment.split("-").map { |word|
|
|
49
|
+
raise ArgumentError, "Invalid component name: #{@kebab.inspect} (empty word segment)" if word.empty?
|
|
50
|
+
|
|
51
|
+
word[0].upcase + (word[1..] || "")
|
|
52
|
+
}.join
|
|
53
|
+
}.join("::")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'component_name'
|
|
4
|
+
require_relative '../lint/cross_ref_linter'
|
|
5
|
+
|
|
6
|
+
module Lilac
|
|
7
|
+
module CLI
|
|
8
|
+
# Per-page assembly of the Ruby source one bundle script contains:
|
|
9
|
+
# the user-authored `<script>` body of every component used on the
|
|
10
|
+
# page. Binding is scanner-canonical (the runtime scanner wires
|
|
11
|
+
# `data-*` at mount), so no `Lilac::Bindings::<Class>` codegen is
|
|
12
|
+
# emitted here — this class only runs `CrossRefLinter` (to surface
|
|
13
|
+
# signal/method mismatches as build errors) and returns the user
|
|
14
|
+
# script unchanged.
|
|
15
|
+
#
|
|
16
|
+
# Used only by `PageCompiler` (per-page injection). The :bundle
|
|
17
|
+
# delivery writer takes a leaner path through `BundleAssetWriter`
|
|
18
|
+
# because the bundle file gathers ALL components rather than the
|
|
19
|
+
# "used on this page" subset, and skips the lint pass entirely.
|
|
20
|
+
class ComponentScriptsAssembler
|
|
21
|
+
def initialize(template_cache:)
|
|
22
|
+
@template_cache = template_cache
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns `Array<String>` — one Ruby source chunk per component
|
|
26
|
+
# that had a non-empty user script. Order matches `used_names`
|
|
27
|
+
# so that any user-side dependency on declaration order (e.g.
|
|
28
|
+
# subclass after parent) is preserved across the bundle.
|
|
29
|
+
#
|
|
30
|
+
# `synthesized_names` marks components that originate from a
|
|
31
|
+
# page-inline `<X data-component=...>` (their `comp.script` slot
|
|
32
|
+
# is empty by construction; the actual class body lives in the
|
|
33
|
+
# page's `<script type="text/ruby">` blocks). For those the
|
|
34
|
+
# cross-ref linter reads the page-level inline Ruby instead so
|
|
35
|
+
# `@signal` / `def method` lookups resolve.
|
|
36
|
+
def assemble(used_names, components, synthesized_names:, page_inline_scripts:)
|
|
37
|
+
synth_lint_script = page_inline_scripts.join("\n\n")
|
|
38
|
+
used_names.map do |name|
|
|
39
|
+
assemble_one(name, components[name], synthesized_names, synth_lint_script)
|
|
40
|
+
end.reject(&:empty?)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def assemble_one(name, comp, synthesized_names, synth_lint_script)
|
|
46
|
+
parsed = @template_cache.fetch(name, comp)
|
|
47
|
+
user_script = comp.script.strip
|
|
48
|
+
lint_script = synthesized_names.include?(name) ? synth_lint_script : user_script
|
|
49
|
+
|
|
50
|
+
run_lint!(name, parsed, lint_script)
|
|
51
|
+
|
|
52
|
+
# Scanner-canonical: no codegen module is emitted. The user
|
|
53
|
+
# script is returned as-is; the runtime scanner wires the
|
|
54
|
+
# component's `data-*` directives at mount.
|
|
55
|
+
user_script
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Cross-reference lint surfaces signal/method mismatches as build
|
|
59
|
+
# errors before the bytecode pass. Errors raise — warnings go to
|
|
60
|
+
# stderr and the build carries on inside CrossRefLinter.
|
|
61
|
+
def run_lint!(name, parsed, lint_script)
|
|
62
|
+
result = CrossRefLinter.lint(
|
|
63
|
+
script_text: lint_script,
|
|
64
|
+
directives: parsed[:default_directives],
|
|
65
|
+
refs_map: parsed[:default_refs_map],
|
|
66
|
+
component_name: ComponentName.new(name).ruby_class,
|
|
67
|
+
file: parsed[:source_path] ? File.basename(parsed[:source_path]) : '(template)'
|
|
68
|
+
)
|
|
69
|
+
return unless result.errors?
|
|
70
|
+
|
|
71
|
+
raise Builder::Error,
|
|
72
|
+
"build failed: #{result.errors} lint error(s) in template; see warnings above."
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|