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,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
require_relative "../config"
|
|
6
|
+
require_relative "../build/builder"
|
|
7
|
+
|
|
8
|
+
module Lilac
|
|
9
|
+
module CLI
|
|
10
|
+
module Subcommand
|
|
11
|
+
class Build < Base
|
|
12
|
+
def run
|
|
13
|
+
opts = parse_opts
|
|
14
|
+
|
|
15
|
+
config = Config.load(
|
|
16
|
+
root: opts[:root],
|
|
17
|
+
components_dir: opts[:components],
|
|
18
|
+
pages_dir: opts[:pages],
|
|
19
|
+
output_dir: opts[:output],
|
|
20
|
+
public_dir: opts[:public],
|
|
21
|
+
build_target: opts[:target],
|
|
22
|
+
mrbc_path: opts[:mrbc_path],
|
|
23
|
+
lilac_compiled_path: opts[:lilac_compiled_path],
|
|
24
|
+
lilac_full_path: opts[:lilac_full_path],
|
|
25
|
+
mruby_wasm_js_path: opts[:mruby_wasm_js_path],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Default to clean: stale per-page `.mrb` (whose content hash
|
|
29
|
+
# changed across builds) would otherwise accumulate, and most
|
|
30
|
+
# modern build tools (Vite / Next / Eleventy / Webpack 5)
|
|
31
|
+
# follow the same convention. Opt out with `--no-clean` for
|
|
32
|
+
# incremental inspection or when something outside Lilac
|
|
33
|
+
# populates the same dist directory.
|
|
34
|
+
clean_output_dir!(config.output_dir, project_root: config.root) unless opts[:clean] == false
|
|
35
|
+
|
|
36
|
+
result = Builder.from_config(config).build
|
|
37
|
+
public_suffix = result[:public_files].positive? ? " + #{result[:public_files]} static file(s)" : ""
|
|
38
|
+
@out.puts "Built #{result[:pages]} page(s) from #{result[:components]} component(s)#{public_suffix} " \
|
|
39
|
+
"→ #{relative(config.output_dir)} (target: #{config.build_target})"
|
|
40
|
+
0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def opts_parser(opts = {})
|
|
46
|
+
OptionParser.new do |o|
|
|
47
|
+
o.banner = "Usage: lilac build [options]"
|
|
48
|
+
OptionHelpers.add_path_options(o, opts)
|
|
49
|
+
OptionHelpers.add_target_options(o, opts)
|
|
50
|
+
o.on("--[no-]clean", "Remove the output dir before building (default: true)") do |v|
|
|
51
|
+
opts[:clean] = v
|
|
52
|
+
end
|
|
53
|
+
o.on("-h", "--help", "Show help") { @out.puts o; exit 0 }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# `--clean` wipes the output dir before building so old artefacts
|
|
58
|
+
# (e.g. stale per-page `.mrb` bundles whose content hash changed)
|
|
59
|
+
# don't accumulate. We refuse to wipe paths that look dangerous —
|
|
60
|
+
# the project root (so `output_dir = "."` doesn't nuke the source),
|
|
61
|
+
# a filesystem root, the user's home dir. Raises `Builder::Error`
|
|
62
|
+
# (caught by the top-level `Command#run` rescue) rather than
|
|
63
|
+
# exiting, so tests and embedders see the error as a normal
|
|
64
|
+
# non-zero return.
|
|
65
|
+
def clean_output_dir!(output_dir, project_root:)
|
|
66
|
+
path = File.expand_path(output_dir.to_s)
|
|
67
|
+
forbidden = [
|
|
68
|
+
"/",
|
|
69
|
+
File.expand_path(Dir.home),
|
|
70
|
+
File.expand_path(project_root.to_s),
|
|
71
|
+
]
|
|
72
|
+
if forbidden.include?(path)
|
|
73
|
+
raise Builder::Error,
|
|
74
|
+
"--clean refused: output_dir #{output_dir.inspect} resolves to a root / home / project dir " \
|
|
75
|
+
"(this would delete your project). Set `output_dir` to a dedicated subdir like `dist/`."
|
|
76
|
+
end
|
|
77
|
+
FileUtils.rm_rf(path) if File.exist?(path)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../config"
|
|
5
|
+
require_relative "../dev_server"
|
|
6
|
+
|
|
7
|
+
module Lilac
|
|
8
|
+
module CLI
|
|
9
|
+
module Subcommand
|
|
10
|
+
class Dev < Base
|
|
11
|
+
def run
|
|
12
|
+
opts = parse_opts
|
|
13
|
+
|
|
14
|
+
# `--target` for `lilac dev` controls the watch-rebuild path.
|
|
15
|
+
# Defaults to `c.dev_target` (Config DEFAULT is `:full`) — the
|
|
16
|
+
# `:compiled` path will fire `mrbc` on every change once the
|
|
17
|
+
# DevServer wiring lands (Phase 2 of the proposals.md entry).
|
|
18
|
+
config = Config.load(
|
|
19
|
+
root: opts[:root],
|
|
20
|
+
components_dir: opts[:components],
|
|
21
|
+
pages_dir: opts[:pages],
|
|
22
|
+
output_dir: opts[:output],
|
|
23
|
+
public_dir: opts[:public],
|
|
24
|
+
dev_host: opts[:host],
|
|
25
|
+
dev_port: opts[:port],
|
|
26
|
+
dev_target: opts[:target],
|
|
27
|
+
mrbc_path: opts[:mrbc_path],
|
|
28
|
+
lilac_compiled_path: opts[:lilac_compiled_path],
|
|
29
|
+
lilac_full_path: opts[:lilac_full_path],
|
|
30
|
+
mruby_wasm_js_path: opts[:mruby_wasm_js_path],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
DevServer.new(
|
|
34
|
+
config,
|
|
35
|
+
host: config.dev_host,
|
|
36
|
+
port: config.dev_port,
|
|
37
|
+
out: @out,
|
|
38
|
+
err: @err,
|
|
39
|
+
).start
|
|
40
|
+
0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def opts_parser(opts = {})
|
|
46
|
+
OptionParser.new do |o|
|
|
47
|
+
o.banner = "Usage: lilac dev [options]"
|
|
48
|
+
o.on("--host HOST", "Bind host (default: #{Config::DEFAULT_DEV_HOST})") { |v| opts[:host] = v }
|
|
49
|
+
o.on("--port PORT", Integer, "Bind port (default: #{Config::DEFAULT_DEV_PORT})") { |v| opts[:port] = v }
|
|
50
|
+
OptionHelpers.add_path_options(o, opts)
|
|
51
|
+
OptionHelpers.add_target_options(o, opts)
|
|
52
|
+
o.on("-h", "--help", "Show help") { @out.puts o; exit 0 }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../config"
|
|
5
|
+
require_relative "../doctor"
|
|
6
|
+
|
|
7
|
+
module Lilac
|
|
8
|
+
module CLI
|
|
9
|
+
module Subcommand
|
|
10
|
+
class Doctor < Base
|
|
11
|
+
def run
|
|
12
|
+
opts = parse_opts
|
|
13
|
+
|
|
14
|
+
config = Config.load(
|
|
15
|
+
root: opts[:root],
|
|
16
|
+
components_dir: opts[:components],
|
|
17
|
+
pages_dir: opts[:pages],
|
|
18
|
+
output_dir: opts[:output],
|
|
19
|
+
public_dir: opts[:public],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Fully qualify the outer Doctor class to disambiguate from
|
|
23
|
+
# the enclosing `Subcommand::Doctor` we're inside.
|
|
24
|
+
::Lilac::CLI::Doctor.new(config, out: @out).run
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def opts_parser(opts = {})
|
|
30
|
+
OptionParser.new do |o|
|
|
31
|
+
o.banner = "Usage: lilac doctor [options]"
|
|
32
|
+
OptionHelpers.add_path_options(o, opts)
|
|
33
|
+
o.on("-h", "--help", "Show help") { @out.puts o; exit 0 }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../scaffold"
|
|
5
|
+
|
|
6
|
+
module Lilac
|
|
7
|
+
module CLI
|
|
8
|
+
module Subcommand
|
|
9
|
+
class New < Base
|
|
10
|
+
def run
|
|
11
|
+
parse_opts
|
|
12
|
+
name = @argv.shift
|
|
13
|
+
|
|
14
|
+
if name.nil?
|
|
15
|
+
@err.puts "Usage: lilac new <project-name>"
|
|
16
|
+
return 1
|
|
17
|
+
end
|
|
18
|
+
unless @argv.empty?
|
|
19
|
+
@err.puts "lilac new takes exactly one argument; extra: #{@argv.inspect}"
|
|
20
|
+
return 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
files = Scaffold.new(name).run
|
|
24
|
+
print_creation_summary(name, files)
|
|
25
|
+
print_next_steps(name)
|
|
26
|
+
0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# `lilac new` currently has no flags beyond -h/--help, but the
|
|
32
|
+
# parser is kept here so future flags (e.g. --no-counter,
|
|
33
|
+
# --with-router) slot in symmetrically with `build` / `dev`.
|
|
34
|
+
# `order!` (not `parse!`) so positional `<project-name>` after
|
|
35
|
+
# `lilac new` doesn't get consumed as a flag value.
|
|
36
|
+
def parse_opts
|
|
37
|
+
opts = {}
|
|
38
|
+
opts_parser(opts).order!(@argv)
|
|
39
|
+
opts
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def opts_parser(_opts = {})
|
|
43
|
+
OptionParser.new do |o|
|
|
44
|
+
o.banner = "Usage: lilac new <project-name>"
|
|
45
|
+
o.on("-h", "--help", "Show help") { @out.puts o; exit 0 }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def print_creation_summary(name, files)
|
|
50
|
+
@out.puts "Created #{name}/ (#{files.length} files):"
|
|
51
|
+
files.each { |f| @out.puts " #{name}/#{f}" }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def print_next_steps(name)
|
|
55
|
+
@out.puts
|
|
56
|
+
@out.puts "Next steps:"
|
|
57
|
+
@out.puts " cd #{name}"
|
|
58
|
+
@out.puts " bundle install"
|
|
59
|
+
@out.puts
|
|
60
|
+
@out.puts " # 1. Install the mruby-wasm runtime for `lilac dev` (one-time, ~5MB):"
|
|
61
|
+
@out.puts " mkdir -p public/vendor/lilac-full/mruby-wasm-js"
|
|
62
|
+
@out.puts " cp /path/to/lilac/build/lilac-full.wasm \\"
|
|
63
|
+
@out.puts " public/vendor/lilac-full/lilac-full.wasm"
|
|
64
|
+
@out.puts " cp -r /path/to/mruby-wasm-runtime/mrbgem/mruby-wasm-js/js/* \\"
|
|
65
|
+
@out.puts " public/vendor/lilac-full/mruby-wasm-js/"
|
|
66
|
+
@out.puts
|
|
67
|
+
@out.puts " # 2. Verify the setup:"
|
|
68
|
+
@out.puts " bundle exec lilac doctor"
|
|
69
|
+
@out.puts
|
|
70
|
+
@out.puts " # 3. Start the dev server (live reload at http://localhost:5173):"
|
|
71
|
+
@out.puts " bundle exec lilac dev"
|
|
72
|
+
@out.puts
|
|
73
|
+
@out.puts " # 4. When ready to ship, `lilac build` produces an optimized"
|
|
74
|
+
@out.puts " # --target compiled dist (smaller bundle; requires mrbc"
|
|
75
|
+
@out.puts " # discoverable via env / monorepo / npm — see README)."
|
|
76
|
+
@out.puts " # For an mrbc-free build, run: lilac build --target full"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../config"
|
|
4
|
+
|
|
5
|
+
module Lilac
|
|
6
|
+
module CLI
|
|
7
|
+
module Subcommand
|
|
8
|
+
# Shared OptionParser option groups. `build` / `dev` / `doctor` /
|
|
9
|
+
# `preview` accept the same path-config flags; `build` / `dev`
|
|
10
|
+
# additionally accept the target / mrbc / runtime flags. Keeping
|
|
11
|
+
# these here so the subcommand classes don't drift out of sync.
|
|
12
|
+
module OptionHelpers
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# The path-config flags `build` / `dev` / `doctor` / `preview`
|
|
16
|
+
# all accept. `o.on` mutates the OptionParser passed in; `opts`
|
|
17
|
+
# collects the parsed values for the caller's later Config.load
|
|
18
|
+
# merge.
|
|
19
|
+
def add_path_options(o, opts)
|
|
20
|
+
o.on("--components DIR", "Components directory (default: components)") { |v| opts[:components] = v }
|
|
21
|
+
o.on("--pages DIR", "Pages directory (default: pages)") { |v| opts[:pages] = v }
|
|
22
|
+
o.on("--public DIR", "Static-passthrough directory (default: public)") { |v| opts[:public] = v }
|
|
23
|
+
o.on("--output DIR", "-o DIR", "Output directory (default: dist)") { |v| opts[:output] = v }
|
|
24
|
+
o.on("--root DIR", "Project root (default: cwd)") { |v| opts[:root] = v }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Build / dev target selection. `--target full` produces dist HTML
|
|
28
|
+
# with inline Ruby + lilac-full wasm (original Lilac story).
|
|
29
|
+
# `--target compiled` invokes `mrbc` to produce `.mrb` bytecode +
|
|
30
|
+
# lilac-compiled wasm (smaller production bundle, ~32% brotli).
|
|
31
|
+
# `--mrbc-path` lets the user pin a specific mrbc binary when the
|
|
32
|
+
# auto-discovery would pick the wrong one.
|
|
33
|
+
def add_target_options(o, opts)
|
|
34
|
+
o.on("--target TARGET", Config::TARGET_VALUES.map(&:to_s),
|
|
35
|
+
"Build target (#{Config::TARGET_VALUES.join(' / ')}; default: full)") do |v|
|
|
36
|
+
opts[:target] = v.to_sym
|
|
37
|
+
end
|
|
38
|
+
o.on("--mrbc-path PATH",
|
|
39
|
+
"Path to the mrbc binary (default: auto-discover)") do |v|
|
|
40
|
+
opts[:mrbc_path] = v
|
|
41
|
+
end
|
|
42
|
+
o.on("--lilac-compiled-path PATH",
|
|
43
|
+
"Path to lilac-compiled.wasm (default: auto-discover; --target compiled only)") do |v|
|
|
44
|
+
opts[:lilac_compiled_path] = v
|
|
45
|
+
end
|
|
46
|
+
o.on("--lilac-full-path PATH",
|
|
47
|
+
"Path to lilac-full.wasm (default: auto-discover; --target full only)") do |v|
|
|
48
|
+
opts[:lilac_full_path] = v
|
|
49
|
+
end
|
|
50
|
+
o.on("--mruby-wasm-js-path PATH",
|
|
51
|
+
"Path to the mruby-wasm-js bridge directory (default: auto-discover)") do |v|
|
|
52
|
+
opts[:mruby_wasm_js_path] = v
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../package_build"
|
|
5
|
+
|
|
6
|
+
module Lilac
|
|
7
|
+
module CLI
|
|
8
|
+
module Subcommand
|
|
9
|
+
class PackageBuild < Base
|
|
10
|
+
def run
|
|
11
|
+
opts = parse_opts
|
|
12
|
+
|
|
13
|
+
if opts[:output].nil?
|
|
14
|
+
@err.puts "Usage: lilac package-build <input.rb>... -o <output.mrb>"
|
|
15
|
+
return 1
|
|
16
|
+
end
|
|
17
|
+
if @argv.empty?
|
|
18
|
+
@err.puts "lilac package-build: at least one input file required"
|
|
19
|
+
@err.puts "Usage: lilac package-build <input.rb>... -o <output.mrb>"
|
|
20
|
+
return 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Fully qualify the outer engine class to disambiguate from
|
|
24
|
+
# the enclosing `Subcommand::PackageBuild` we're inside.
|
|
25
|
+
package = ::Lilac::CLI::PackageBuild.new(
|
|
26
|
+
inputs: @argv.dup,
|
|
27
|
+
output: opts[:output],
|
|
28
|
+
mrbc_path: opts[:mrbc_path],
|
|
29
|
+
)
|
|
30
|
+
out_path = package.run
|
|
31
|
+
@out.puts "Built package bytecode: #{relative(out_path)} (#{File.size(out_path)} bytes)"
|
|
32
|
+
0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def opts_parser(opts = {})
|
|
38
|
+
OptionParser.new do |o|
|
|
39
|
+
o.banner = "Usage: lilac package-build <input.rb>... -o <output.mrb>"
|
|
40
|
+
o.on("-o", "--output PATH", "Output `.mrb` file (required)") { |v| opts[:output] = v }
|
|
41
|
+
o.on("--mrbc-path PATH",
|
|
42
|
+
"Path to the mrbc binary (default: auto-discover via lilac-wasm-bin)") do |v|
|
|
43
|
+
opts[:mrbc_path] = v
|
|
44
|
+
end
|
|
45
|
+
o.on("-h", "--help", "Show help") { @out.puts o; exit 0 }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
require_relative "../config"
|
|
5
|
+
require_relative "../preview_server"
|
|
6
|
+
|
|
7
|
+
module Lilac
|
|
8
|
+
module CLI
|
|
9
|
+
module Subcommand
|
|
10
|
+
class Preview < Base
|
|
11
|
+
def run
|
|
12
|
+
opts = parse_opts
|
|
13
|
+
|
|
14
|
+
# `preview` only needs the output_dir resolved; target / mrbc /
|
|
15
|
+
# vendor paths are irrelevant because we're serving an already-
|
|
16
|
+
# built `dist/`. Reuse `Config.load` so `lilac.config.rb` is
|
|
17
|
+
# still consulted for `output_dir`.
|
|
18
|
+
config = Config.load(
|
|
19
|
+
root: opts[:root],
|
|
20
|
+
components_dir: opts[:components],
|
|
21
|
+
pages_dir: opts[:pages],
|
|
22
|
+
output_dir: opts[:output],
|
|
23
|
+
public_dir: opts[:public],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
PreviewServer.new(
|
|
27
|
+
config.output_dir,
|
|
28
|
+
host: opts[:host] || PreviewServer::DEFAULT_HOST,
|
|
29
|
+
port: opts[:port] || PreviewServer::DEFAULT_PORT,
|
|
30
|
+
out: @out,
|
|
31
|
+
err: @err,
|
|
32
|
+
).start
|
|
33
|
+
0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def opts_parser(opts = {})
|
|
39
|
+
OptionParser.new do |o|
|
|
40
|
+
o.banner = "Usage: lilac preview [options]"
|
|
41
|
+
o.on("--host HOST", "Bind host (default: #{PreviewServer::DEFAULT_HOST})") { |v| opts[:host] = v }
|
|
42
|
+
o.on("--port PORT", Integer, "Bind port (default: #{PreviewServer::DEFAULT_PORT})") { |v| opts[:port] = v }
|
|
43
|
+
OptionHelpers.add_path_options(o, opts)
|
|
44
|
+
o.on("-h", "--help", "Show help") { @out.puts o; exit 0 }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
group :development do
|
|
6
|
+
gem "lilac-cli", "~> 0.1"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Lilac wasm runtimes (full + compiled) + JS bridge, packaged as a Ruby
|
|
10
|
+
# gem so `bundle install` is all you need. The lilac-cli soft-requires
|
|
11
|
+
# this; absent gem falls through to npm / monorepo / explicit-path
|
|
12
|
+
# discovery, but having it as a Gemfile dep is the simplest path.
|
|
13
|
+
gem "lilac-wasm-bin", "~> 0.1"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
A Lilac app — Ruby in the browser via [mruby-wasm-runtime][1], composed
|
|
4
|
+
from `.lil` single-file components by [lilac-cli][2].
|
|
5
|
+
|
|
6
|
+
[1]: https://github.com/takahashim/mruby-wasm-runtime
|
|
7
|
+
[2]: https://github.com/takahashim/lilac-cli
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bundle install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Place the Lilac wasm runtime under `public/vendor/`. Everything in
|
|
16
|
+
`public/` is mirrored verbatim into `dist/` at build time (Vite /
|
|
17
|
+
Eleventy convention), so requests to `/vendor/...` resolve from there
|
|
18
|
+
both in dev (`lilac dev`) and in production (`lilac build`).
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
mkdir -p public/vendor/lilac-full/mruby-wasm-js
|
|
22
|
+
cp /path/to/lilac/build/lilac-full.wasm \
|
|
23
|
+
public/vendor/lilac-full/lilac-full.wasm
|
|
24
|
+
cp -r /path/to/mruby-wasm-runtime/mrbgem/mruby-wasm-js/js/* \
|
|
25
|
+
public/vendor/lilac-full/mruby-wasm-js/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Add anything else you want passthrough-served (favicons, images, CSS
|
|
29
|
+
without templating) directly under `public/`.
|
|
30
|
+
|
|
31
|
+
## Development
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bundle exec lilac dev
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Open <http://localhost:5173>. Edit any `.lil` component or `.html` page and
|
|
38
|
+
the browser reloads automatically (Server-Sent Events).
|
|
39
|
+
|
|
40
|
+
## Build for production
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
bundle exec lilac build # default --target compiled (smaller bundle, needs mrbc)
|
|
44
|
+
bundle exec lilac build --target full # opt out: ship runtime parser, no mrbc required
|
|
45
|
+
bundle exec lilac preview # serve dist/ at http://127.0.0.1:4173 (no watch / no reload)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Output goes to `dist/`, which is self-contained (HTML + everything from
|
|
49
|
+
`public/`). Deploy `dist/` as your static site root. `lilac preview` is
|
|
50
|
+
the recommended way to smoke-test the production build locally before
|
|
51
|
+
deploy — it runs a plain static server with no live-reload injection,
|
|
52
|
+
mirroring what a CDN or static host would do.
|
|
53
|
+
|
|
54
|
+
### `--target compiled` (default for `lilac build`)
|
|
55
|
+
|
|
56
|
+
`--target compiled` precompiles Ruby into `.mrb` bytecode (smaller
|
|
57
|
+
bundle, no in-browser parser) and **automatically vendors the
|
|
58
|
+
`lilac-compiled` runtime** into `dist/vendor/lilac-compiled/`. This
|
|
59
|
+
is the default for `lilac build` — `lilac dev` keeps `--target full`
|
|
60
|
+
so the development loop stays mrbc-free.
|
|
61
|
+
|
|
62
|
+
You do not need to copy anything into `public/vendor/lilac-compiled/` —
|
|
63
|
+
the CLI resolves the wasm + JS bridge from one of:
|
|
64
|
+
|
|
65
|
+
1. `--lilac-compiled-path` / `--mruby-wasm-js-path` CLI flags
|
|
66
|
+
2. `c.lilac_compiled_path` / `c.mruby_wasm_js_path` in `lilac.config.rb`
|
|
67
|
+
3. `LILAC_COMPILED_WASM` / `MRUBY_WASM_JS_PATH` env vars
|
|
68
|
+
4. The `lilac-wasm-bin` Ruby gem (canonical install path — your
|
|
69
|
+
scaffolded `Gemfile` declares it, so `bundle install` brings the
|
|
70
|
+
wasm + bridge in automatically)
|
|
71
|
+
5. The Lilac monorepo (if you're working inside this repo)
|
|
72
|
+
|
|
73
|
+
Most projects rely on path 4 — `bundle install` is all you need.
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
`lilac.config.rb` at the project root overrides built-in defaults
|
|
78
|
+
for paths, host, and port. Every field is commented out by default;
|
|
79
|
+
uncomment what you want to change. CLI flags (`lilac dev --port`)
|
|
80
|
+
still take precedence over the file.
|
|
81
|
+
|
|
82
|
+
## Components and `data-*` directives
|
|
83
|
+
|
|
84
|
+
Templates wire DOM to reactive state via `data-*` attributes; the
|
|
85
|
+
build extracts them and generates the equivalent Ruby bindings
|
|
86
|
+
alongside your `<script>`, so the script holds only component logic.
|
|
87
|
+
The scaffold's `components/counter.lil` shows the most common ones:
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<button data-on-click="decrement" type="button">-</button>
|
|
91
|
+
<span data-text="@count" class="count">0</span>
|
|
92
|
+
<button data-on-click="increment" type="button">+</button>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
class Counter < Lilac::Component
|
|
97
|
+
def setup
|
|
98
|
+
@count = signal(0)
|
|
99
|
+
end
|
|
100
|
+
def increment(_ev) = @count.update(&:succ)
|
|
101
|
+
def decrement(_ev) = @count.update(&:pred)
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Available directive families:
|
|
106
|
+
|
|
107
|
+
| Directive | Purpose |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `data-text="@x"` / `data-unsafe-html="@x"` | element body (escaped / raw) |
|
|
110
|
+
| `data-show="@x"` / `data-hide="@x"` | visibility (toggles `.lil-hidden`) |
|
|
111
|
+
| `data-class="{ active: @x, 'btn-primary': @y }"` | class toggles (bare ident or quoted key) |
|
|
112
|
+
| `data-attr-href="@x"` | reactive HTML attribute (URLs auto-sanitized) |
|
|
113
|
+
| `data-css-color="@x"` | reactive CSS custom property (`--color`) |
|
|
114
|
+
| `data-on-click="m"` / `data-on-<event>="m"` | event handler → method on the component |
|
|
115
|
+
| `data-each="@items" data-key="id"` | keyed list iteration |
|
|
116
|
+
| `data-field="name"` / `data-button="name"` | input two-way binding / button action (form gem) |
|
|
117
|
+
|
|
118
|
+
Values must be `@ivar` (a signal) or, inside `data-each`, `it` /
|
|
119
|
+
`it.field`. Arbitrary Ruby expressions are rejected at build time so
|
|
120
|
+
templates stay statically auditable.
|
|
121
|
+
|
|
122
|
+
Build-time also runs a cross-reference linter that warns when a
|
|
123
|
+
template references an undeclared `@signal` or `data-on-X` method
|
|
124
|
+
(stderr, non-fatal). Typos get a "Did you mean?" suggestion based on
|
|
125
|
+
edit distance against the declared names.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div data-component="counter">
|
|
3
|
+
<button data-on-click="decrement" type="button">-</button>
|
|
4
|
+
<span data-text="@count" class="count">0</span>
|
|
5
|
+
<button data-on-click="increment" type="button">+</button>
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script type="text/ruby">
|
|
10
|
+
class Counter < Lilac::Component
|
|
11
|
+
def setup
|
|
12
|
+
@count = signal(0)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def increment(_ev) = @count.update(&:succ)
|
|
16
|
+
def decrement(_ev) = @count.update(&:pred)
|
|
17
|
+
end
|
|
18
|
+
</script>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Project-wide lilac-cli configuration. Uncomment any field to
|
|
4
|
+
# override the corresponding built-in default. CLI flags (e.g.
|
|
5
|
+
# `lilac dev --port 8000`) take precedence over values set here.
|
|
6
|
+
|
|
7
|
+
Lilac::CLI.configure do |c|
|
|
8
|
+
# c.components_dir = "components" # where .lil components live
|
|
9
|
+
# c.pages_dir = "pages" # where .html pages live
|
|
10
|
+
# c.public_dir = "public" # static-passthrough directory
|
|
11
|
+
# c.output_dir = "dist" # build output
|
|
12
|
+
|
|
13
|
+
# c.dev_host = "127.0.0.1" # `lilac dev` bind host
|
|
14
|
+
# c.dev_port = 5173 # `lilac dev` bind port
|
|
15
|
+
|
|
16
|
+
# Runtime wasm + bridge discovery overrides. All default to nil, in
|
|
17
|
+
# which case the CLI auto-discovers via env vars, the `lilac-wasm-bin`
|
|
18
|
+
# gem (recommended — already in the scaffolded Gemfile), or a monorepo
|
|
19
|
+
# ancestor. Set these to vendor a specific wasm for a fully offline,
|
|
20
|
+
# CDN-free build.
|
|
21
|
+
# c.lilac_full_path = "/abs/path/to/lilac-full.wasm" # --target full
|
|
22
|
+
# c.lilac_compiled_path = "/abs/path/to/lilac-compiled.wasm" # --target compiled
|
|
23
|
+
# c.mruby_wasm_js_path = "/abs/path/to/mruby-wasm-js/" # JS bridge (both targets)
|
|
24
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>{{name}}</title>
|
|
6
|
+
<style>
|
|
7
|
+
/* Reserved class used by data-show / data-hide directives.
|
|
8
|
+
`!important` so user-side display rules can't override. */
|
|
9
|
+
.lil-hidden { display: none !important; }
|
|
10
|
+
|
|
11
|
+
body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 2rem auto; padding: 0 1rem; }
|
|
12
|
+
[data-component="counter"] { display: flex; gap: 0.5rem; align-items: center; }
|
|
13
|
+
.count { font-size: 1.5rem; min-width: 2rem; text-align: center; }
|
|
14
|
+
button { font-size: 1rem; padding: 0.25rem 0.75rem; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<h1>{{name}}</h1>
|
|
19
|
+
|
|
20
|
+
<div data-use="counter"></div>
|
|
21
|
+
|
|
22
|
+
<script type="module">
|
|
23
|
+
import { createVM } from "/vendor/lilac-full/mruby-wasm-js/index.js";
|
|
24
|
+
const vm = await createVM({
|
|
25
|
+
wasm: "/vendor/lilac-full/lilac-full.wasm",
|
|
26
|
+
});
|
|
27
|
+
// If `lilac.config.rb` declares Lilac package gems via Bundler,
|
|
28
|
+
// `lilac build` stages them under `dist/packages/` and writes a
|
|
29
|
+
// `lilac.packages.json` manifest. Load each before evaluating the
|
|
30
|
+
// page's text/ruby scripts so package handler registrations / class
|
|
31
|
+
// definitions are ready before component mount (decisions §25/§26).
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch("/lilac.packages.json");
|
|
34
|
+
if (res.ok) {
|
|
35
|
+
const { packages = [] } = await res.json();
|
|
36
|
+
for (const url of packages) {
|
|
37
|
+
vm.loadBytecode(new Uint8Array(await (await fetch(url)).arrayBuffer()));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch { /* No manifest => no packages; harmless. */ }
|
|
41
|
+
document.querySelectorAll('script[type="text/ruby"]')
|
|
42
|
+
.forEach(s => vm.eval(s.textContent));
|
|
43
|
+
vm.eval("Lilac.start");
|
|
44
|
+
</script>
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|
|
File without changes
|