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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +144 -0
  4. data/exe/lilac +6 -0
  5. data/lib/lilac/cli/build/build_context.rb +31 -0
  6. data/lib/lilac/cli/build/build_error.rb +50 -0
  7. data/lib/lilac/cli/build/builder.rb +326 -0
  8. data/lib/lilac/cli/build/bundle_asset_writer.rb +129 -0
  9. data/lib/lilac/cli/build/bytecode_builder.rb +212 -0
  10. data/lib/lilac/cli/build/compiled_boot_module.rb +103 -0
  11. data/lib/lilac/cli/build/compiled_runtime_resolver.rb +64 -0
  12. data/lib/lilac/cli/build/component_name.rb +57 -0
  13. data/lib/lilac/cli/build/component_scripts_assembler.rb +76 -0
  14. data/lib/lilac/cli/build/directive.rb +51 -0
  15. data/lib/lilac/cli/build/full_runtime_resolver.rb +61 -0
  16. data/lib/lilac/cli/build/html_emitter.rb +58 -0
  17. data/lib/lilac/cli/build/package_stager.rb +111 -0
  18. data/lib/lilac/cli/build/page_compiler.rb +405 -0
  19. data/lib/lilac/cli/build/runtime_resolver.rb +150 -0
  20. data/lib/lilac/cli/build/sfc.rb +150 -0
  21. data/lib/lilac/cli/build/template_ast.rb +304 -0
  22. data/lib/lilac/cli/build/template_ast_cache.rb +58 -0
  23. data/lib/lilac/cli/build/vendor_writer.rb +58 -0
  24. data/lib/lilac/cli/build/wasm_mrbc_driver.rb +183 -0
  25. data/lib/lilac/cli/command.rb +110 -0
  26. data/lib/lilac/cli/config.rb +150 -0
  27. data/lib/lilac/cli/config_loader.rb +97 -0
  28. data/lib/lilac/cli/dev_server.rb +156 -0
  29. data/lib/lilac/cli/doctor.rb +310 -0
  30. data/lib/lilac/cli/lint/build_linter.rb +95 -0
  31. data/lib/lilac/cli/lint/cross_ref_linter.rb +336 -0
  32. data/lib/lilac/cli/lint/lint_warning.rb +53 -0
  33. data/lib/lilac/cli/lint/script_analyzer.rb +255 -0
  34. data/lib/lilac/cli/live_reload.rb +194 -0
  35. data/lib/lilac/cli/offline_verifier.rb +117 -0
  36. data/lib/lilac/cli/package_build.rb +68 -0
  37. data/lib/lilac/cli/package_discovery.rb +77 -0
  38. data/lib/lilac/cli/preview_server.rb +70 -0
  39. data/lib/lilac/cli/scaffold.rb +85 -0
  40. data/lib/lilac/cli/source_location.rb +16 -0
  41. data/lib/lilac/cli/subcommand/base.rb +65 -0
  42. data/lib/lilac/cli/subcommand/build.rb +82 -0
  43. data/lib/lilac/cli/subcommand/dev.rb +58 -0
  44. data/lib/lilac/cli/subcommand/doctor.rb +39 -0
  45. data/lib/lilac/cli/subcommand/new.rb +81 -0
  46. data/lib/lilac/cli/subcommand/option_helpers.rb +58 -0
  47. data/lib/lilac/cli/subcommand/package_build.rb +51 -0
  48. data/lib/lilac/cli/subcommand/preview.rb +50 -0
  49. data/lib/lilac/cli/templates/Gemfile +13 -0
  50. data/lib/lilac/cli/templates/README.md +125 -0
  51. data/lib/lilac/cli/templates/components/counter.lil +18 -0
  52. data/lib/lilac/cli/templates/gitignore +5 -0
  53. data/lib/lilac/cli/templates/lilac.config.rb +24 -0
  54. data/lib/lilac/cli/templates/pages/index.html +46 -0
  55. data/lib/lilac/cli/templates/public/.gitkeep +0 -0
  56. data/lib/lilac/cli/version.rb +7 -0
  57. data/lib/lilac/cli/watcher.rb +62 -0
  58. data/lib/lilac/cli.rb +20 -0
  59. data/lib/lilac/directives/class_parser.rb +179 -0
  60. data/lib/lilac/directives/collision_rules.rb +49 -0
  61. data/lib/lilac/directives/grammar.rb +76 -0
  62. data/lib/lilac/directives/lints.rb +128 -0
  63. data/lib/lilac/directives/value.rb +85 -0
  64. data/lib/lilac/directives.rb +16 -0
  65. 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,5 @@
1
+ /dist/
2
+ /.bundle/
3
+ /vendor/bundle/
4
+ *.gem
5
+ .DS_Store
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lilac
4
+ module CLI
5
+ VERSION = "0.2.0"
6
+ end
7
+ end