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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fe398ae7cd444978bcce0e26b56f755b7f75de66aa63e62a1cda3729c4bbba06
|
|
4
|
+
data.tar.gz: a0bfd3e47f5d61af27b30e0c362aaf3a24154a4f1fb27f56c2b1efd2154d66ec
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b2edc6ad8d25033f378d941fd230134d11927f247d4e1eb8878f4b9e5c5a39cfd59ceee489864a8b697f730d6420392a21141f818c08281ef23091c8740b510f
|
|
7
|
+
data.tar.gz: 32e2e3ee8d5ef6e4dbb3763f7dbb8a9f67eeb6f1faeebd687773147a2db86349bbed58993190ae62f8da9e4e5c245d352df80ba90e96b35f30d824b18310b780
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 takahashim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# lilac-cli
|
|
2
|
+
|
|
3
|
+
Optional build tool for **Lilac single-file components** (`.lil`) —
|
|
4
|
+
template + Ruby script in one file, compiled to static HTML for the
|
|
5
|
+
Lilac component runtime on mruby-wasm.
|
|
6
|
+
|
|
7
|
+
The runtime is the canonical interpreter of `data-*` directives, so
|
|
8
|
+
Lilac apps can be shipped as plain HTML without this CLI (see the
|
|
9
|
+
repo-root README's Quick start). What lilac-cli adds on top:
|
|
10
|
+
|
|
11
|
+
- **Static lint** for directive grammar / undeclared signals / dead
|
|
12
|
+
methods, caught at build time with source positions.
|
|
13
|
+
- **`.lil` single-file component** format + project structure
|
|
14
|
+
(`components/` + `pages/`) and `<div data-use="..."></div>`
|
|
15
|
+
placeholders.
|
|
16
|
+
- **Pre-compiled bindings** (`codegen: :auto`, default) — directive
|
|
17
|
+
interpretation moves from mount time to build time; the generated
|
|
18
|
+
`Lilac::Bindings::<Class>#bind_template_hook` takes precedence over
|
|
19
|
+
the runtime scanner, so there's no double-binding when both paths
|
|
20
|
+
coexist on the same page. Set `codegen: :off` to validate the
|
|
21
|
+
runtime path against the same `.lil` source.
|
|
22
|
+
- **Dev server** with live reload, `scaffold`, `doctor`.
|
|
23
|
+
|
|
24
|
+
> [!WARNING]
|
|
25
|
+
> Pre-alpha. API and CLI surface will change.
|
|
26
|
+
|
|
27
|
+
## Status
|
|
28
|
+
|
|
29
|
+
| Phase | Status |
|
|
30
|
+
|---|---|
|
|
31
|
+
| 1. `.lil` parser + `lilac build` (static output) | ✅ |
|
|
32
|
+
| 2. `lilac dev` (file watch via `listen`) | ✅ |
|
|
33
|
+
| 3. SSE live reload (via `Wsv::Response.sse`) | ✅ |
|
|
34
|
+
| 4. `lilac new` scaffold | ✅ |
|
|
35
|
+
| 5. `lilac doctor` setup verifier | ✅ |
|
|
36
|
+
| 6. `lilac.config.rb` project config | ✅ |
|
|
37
|
+
|
|
38
|
+
## What is a `.lil` file?
|
|
39
|
+
|
|
40
|
+
A single-file component containing markup, optional sub-templates, and a
|
|
41
|
+
Ruby script that defines a `Lilac::Component` subclass:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<!-- components/counter.lil -->
|
|
45
|
+
<template>
|
|
46
|
+
<div data-component="counter">
|
|
47
|
+
<button data-ref="inc">+</button>
|
|
48
|
+
<span data-ref="count">0</span>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<!-- Sub-templates use `data-template="..."` — same convention the
|
|
53
|
+
Lilac runtime uses for `bind_list` clone-targets. -->
|
|
54
|
+
<template data-template="counter-banner">
|
|
55
|
+
<div class="banner"></div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script type="text/ruby">
|
|
59
|
+
class Counter < Lilac::Component
|
|
60
|
+
def setup
|
|
61
|
+
@count = signal(0)
|
|
62
|
+
refs.inc.on(:click) { @count.update(&:succ) }
|
|
63
|
+
bind refs.count, text: @count
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
</script>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`.lil` is HTML5-valid (use a `.lil → text/html` association in your
|
|
70
|
+
editor for syntax highlighting). The build tool extracts the top-level
|
|
71
|
+
`<template>` and `<script type="text/ruby">` blocks; everything inside
|
|
72
|
+
each block is preserved verbatim.
|
|
73
|
+
|
|
74
|
+
`lilac build` reads `components/*.lil` + `pages/*.html`, inlines the
|
|
75
|
+
referenced component templates and scripts into each page, and writes the
|
|
76
|
+
result to `dist/`.
|
|
77
|
+
|
|
78
|
+
The kebab-to-CamelCase autoregister feature in Lilac means
|
|
79
|
+
`<div data-component="counter"></div>` resolves to the `Counter` class with
|
|
80
|
+
no `Lilac.register` call needed.
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
gem install lilac-cli
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or in a Gemfile:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
group :development do
|
|
92
|
+
gem "lilac-cli"
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Requires Ruby 3.2 or later.
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
lilac new my-app # scaffold a fresh project (Counter sample inside)
|
|
102
|
+
cd my-app && bundle install
|
|
103
|
+
|
|
104
|
+
lilac doctor # check project setup (runtime, component refs, paths)
|
|
105
|
+
|
|
106
|
+
lilac build # produce dist/ (default --target compiled — requires mrbc)
|
|
107
|
+
lilac build --target full # opt out: ship runtime parser in dist (mrbc-free)
|
|
108
|
+
lilac build --output _site
|
|
109
|
+
|
|
110
|
+
lilac dev # build + serve + live reload at http://127.0.0.1:5173
|
|
111
|
+
# (default --target full so no mrbc needed for iteration)
|
|
112
|
+
lilac dev --port 8000 # bind to a different port
|
|
113
|
+
|
|
114
|
+
lilac preview # serve the built dist/ at http://127.0.0.1:4173
|
|
115
|
+
# (no watch / no reload — use to verify production output)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Configuration
|
|
119
|
+
|
|
120
|
+
Project-wide defaults can live in `lilac.config.rb` at the project
|
|
121
|
+
root (a commented template is generated by `lilac new`):
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
Lilac::CLI.configure do |c|
|
|
125
|
+
c.components_dir = "src/components"
|
|
126
|
+
c.pages_dir = "src/pages"
|
|
127
|
+
c.dev_port = 3000
|
|
128
|
+
# c.codegen = :off # skip bind_template_hook codegen and let
|
|
129
|
+
# the runtime scanner interpret directives
|
|
130
|
+
# at mount time (parity-testing or
|
|
131
|
+
# runtime-only deployment).
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Precedence (lowest to highest): built-in defaults → `lilac.config.rb`
|
|
136
|
+
→ CLI flags (`--components`, `--port`, etc.).
|
|
137
|
+
|
|
138
|
+
`lilac dev` watches `components/**/*.lil` and `pages/**/*.html` for
|
|
139
|
+
changes, rebuilds on save, and pushes a `reload` event over SSE to every
|
|
140
|
+
connected browser tab.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT. See [`LICENSE.txt`](./LICENSE.txt).
|
data/exe/lilac
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lilac
|
|
4
|
+
module CLI
|
|
5
|
+
# Per-build shared state handed from `Builder` (orchestrator) to
|
|
6
|
+
# `PageCompiler` (per-page compiler). All fields are set before the
|
|
7
|
+
# page loop starts, so PageCompiler treats the struct as read-only;
|
|
8
|
+
# the cache / linter references inside it accumulate per-page state
|
|
9
|
+
# via their own APIs.
|
|
10
|
+
#
|
|
11
|
+
# Fields:
|
|
12
|
+
# - `components` — { name => SFC::Component }, .lil + synthesized
|
|
13
|
+
# - `bundle_assets` — BundleAssetWriter::BundleAssets or nil (:inline mode)
|
|
14
|
+
# - `package_dist_urls` — Array<String> page-relative URLs for `.mrb` packages
|
|
15
|
+
# - `template_cache` — shared TemplateASTCache (parse once, reuse)
|
|
16
|
+
# - `build_linter` — BuildLinter accumulating cross-page diagnostics
|
|
17
|
+
# - `bytecode_builder` — BytecodeBuilder for compiled-target `.mrb` emission
|
|
18
|
+
# - `target` — :full | :compiled
|
|
19
|
+
# - `delivery` — :inline | :bundle
|
|
20
|
+
# - `live_reload` — boolean (dev path injects SSE client script)
|
|
21
|
+
# - `output_dir` — absolute dist path (relative refs in boot module need this)
|
|
22
|
+
# - `pages_dir` — absolute pages root (for computing output paths)
|
|
23
|
+
BuildContext = Struct.new(
|
|
24
|
+
:components, :bundle_assets, :package_dist_urls,
|
|
25
|
+
:template_cache, :build_linter, :bytecode_builder,
|
|
26
|
+
:target, :delivery, :live_reload,
|
|
27
|
+
:output_dir, :pages_dir,
|
|
28
|
+
keyword_init: true
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lilac
|
|
4
|
+
module CLI
|
|
5
|
+
# Common base + formatter for build-time errors. Wraps a structured
|
|
6
|
+
# message into the multi-line shape:
|
|
7
|
+
#
|
|
8
|
+
# lilac: build error in <file>:<line>
|
|
9
|
+
# <snippet> # optional
|
|
10
|
+
# <message body>
|
|
11
|
+
# <suggestion> # optional
|
|
12
|
+
#
|
|
13
|
+
# The bare-string form (`raise BuildError, "..."`) is supported so
|
|
14
|
+
# callers that don't have a location on hand can still raise, but
|
|
15
|
+
# the formatted shape only kicks in when `at:` is provided.
|
|
16
|
+
#
|
|
17
|
+
# Subclassed by `Lilac::Directives::Lints::Error`.
|
|
18
|
+
class BuildError < StandardError
|
|
19
|
+
attr_reader :at, :snippet, :suggestion
|
|
20
|
+
|
|
21
|
+
def initialize(message = nil, at: nil, snippet: nil, suggestion: nil)
|
|
22
|
+
@at = at
|
|
23
|
+
@snippet = snippet
|
|
24
|
+
@suggestion = suggestion
|
|
25
|
+
@body = message
|
|
26
|
+
super(format)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Two paths: structured (location known → spec header + indented
|
|
32
|
+
# body) vs bare (no location → return body verbatim, mostly used
|
|
33
|
+
# by code paths that have no source coordinates).
|
|
34
|
+
def format
|
|
35
|
+
return @body unless @at
|
|
36
|
+
|
|
37
|
+
parts = ["lilac: build error in #{@at}"]
|
|
38
|
+
parts << " #{@snippet}" if @snippet
|
|
39
|
+
# Indent each body line so multi-line messages stay aligned.
|
|
40
|
+
if @body
|
|
41
|
+
@body.to_s.each_line do |l|
|
|
42
|
+
parts << " #{l.chomp}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
parts << " #{@suggestion}" if @suggestion
|
|
46
|
+
parts.join("\n")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require_relative 'sfc'
|
|
6
|
+
require_relative 'bytecode_builder'
|
|
7
|
+
require_relative 'html_emitter'
|
|
8
|
+
require_relative 'template_ast_cache'
|
|
9
|
+
require_relative 'build_context'
|
|
10
|
+
require_relative 'bundle_asset_writer'
|
|
11
|
+
require_relative 'package_stager'
|
|
12
|
+
require_relative 'vendor_writer'
|
|
13
|
+
require_relative 'compiled_runtime_resolver'
|
|
14
|
+
require_relative 'full_runtime_resolver'
|
|
15
|
+
require_relative 'page_compiler'
|
|
16
|
+
require_relative '../lint/build_linter'
|
|
17
|
+
|
|
18
|
+
module Lilac
|
|
19
|
+
module CLI
|
|
20
|
+
# Orchestrates a full build: loads components, stages packages,
|
|
21
|
+
# writes the (optional) bundle file, compiles each page via
|
|
22
|
+
# `PageCompiler`, then vendors runtime assets into `dist/vendor/`.
|
|
23
|
+
#
|
|
24
|
+
# The per-page rewriting logic lives in `PageCompiler`; HTML helpers
|
|
25
|
+
# in `HtmlEmitter`; template parse caching in `TemplateASTCache`;
|
|
26
|
+
# all of those share state through the `BuildContext` value object
|
|
27
|
+
# this class assembles.
|
|
28
|
+
class Builder
|
|
29
|
+
class Error < StandardError; end
|
|
30
|
+
|
|
31
|
+
# Filenames that must not land in the build output even when they
|
|
32
|
+
# exist under `public/`. Add to this list as new conventions are
|
|
33
|
+
# encountered (e.g. `.DS_Store`, `Thumbs.db`).
|
|
34
|
+
EXCLUDED_BASENAMES = %w[.gitkeep].freeze
|
|
35
|
+
|
|
36
|
+
# Target-specific public/ subdirectories that should NOT be mirrored
|
|
37
|
+
# into dist/ for the inactive target. Both runtime variants live
|
|
38
|
+
# under their own namespace in `public/vendor/` so a project that
|
|
39
|
+
# ships both (e.g. dev=full, prod=compiled) can keep them side by
|
|
40
|
+
# side and let the CLI prune the inactive one at build time.
|
|
41
|
+
#
|
|
42
|
+
# Paths are relative to `public_dir`, POSIX style. The match is on
|
|
43
|
+
# path prefix + boundary, so `vendor/lilac-full` matches
|
|
44
|
+
# `vendor/lilac-full/lilac-full.wasm` but not `vendor/lilac-full-x`.
|
|
45
|
+
EXCLUDED_DIRS_FOR_TARGET = {
|
|
46
|
+
full: %w[vendor/lilac-compiled].freeze,
|
|
47
|
+
compiled: %w[vendor/lilac-full].freeze
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
# Construct a Builder from a resolved `Config`. Centralises the
|
|
51
|
+
# Config-attr → Builder-kwarg mapping so callers in `Command` and
|
|
52
|
+
# `DevServer` don't drift out of sync. Overridable kwargs cover
|
|
53
|
+
# the per-caller differences:
|
|
54
|
+
#
|
|
55
|
+
# - `target:` — Command picks `config.build_target`, DevServer
|
|
56
|
+
# picks `config.dev_target`. Defaults to the build target.
|
|
57
|
+
# - `delivery:` — either caller can pin a value here; both
|
|
58
|
+
# currently leave it unset so `config.delivery` wins (build
|
|
59
|
+
# and dev share the same delivery path, matching what ships
|
|
60
|
+
# to prod).
|
|
61
|
+
# - `live_reload:` — DevServer turns it on for SSE-driven page
|
|
62
|
+
# reload; `lilac build` leaves it off.
|
|
63
|
+
def self.from_config(config, live_reload: false,
|
|
64
|
+
target: nil, delivery: nil)
|
|
65
|
+
new(
|
|
66
|
+
components_dir: config.components_dir,
|
|
67
|
+
pages_dir: config.pages_dir,
|
|
68
|
+
output_dir: config.output_dir,
|
|
69
|
+
public_dir: config.public_dir,
|
|
70
|
+
target: target || config.build_target,
|
|
71
|
+
mrbc_path: config.mrbc_path,
|
|
72
|
+
lilac_compiled_path: config.lilac_compiled_path,
|
|
73
|
+
lilac_full_path: config.lilac_full_path,
|
|
74
|
+
mruby_wasm_js_path: config.mruby_wasm_js_path,
|
|
75
|
+
packages: config.packages,
|
|
76
|
+
delivery: delivery || config.delivery,
|
|
77
|
+
live_reload: live_reload
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def initialize(components_dir:, pages_dir:, output_dir:, public_dir: nil,
|
|
82
|
+
live_reload: false,
|
|
83
|
+
target: :full, mrbc_path: nil,
|
|
84
|
+
lilac_compiled_path: nil, lilac_full_path: nil,
|
|
85
|
+
mruby_wasm_js_path: nil,
|
|
86
|
+
packages: [],
|
|
87
|
+
disable_gem_discovery: false,
|
|
88
|
+
delivery: :inline)
|
|
89
|
+
@components_dir = components_dir
|
|
90
|
+
@pages_dir = pages_dir
|
|
91
|
+
@output_dir = output_dir
|
|
92
|
+
# public_dir is optional. When nil or absent on disk, the
|
|
93
|
+
# mirroring step is skipped — projects that don't need static
|
|
94
|
+
# passthrough (no vendor bundle, no images) work fine without
|
|
95
|
+
# creating the directory.
|
|
96
|
+
@public_dir = public_dir
|
|
97
|
+
@live_reload = live_reload
|
|
98
|
+
# `:full` — dist HTML loads inline Ruby via lilac-full wasm
|
|
99
|
+
# (vm.evalScript). `:compiled` — Ruby is pre-compiled to
|
|
100
|
+
# `.mrb` bytecode via `mrbc` and loaded by lilac-compiled wasm
|
|
101
|
+
# (vm.loadBytecode). The compiled target shaves ~32% off the brotli
|
|
102
|
+
# bundle but requires `mrbc` available at build time. See
|
|
103
|
+
# `BytecodeBuilder` for path discovery.
|
|
104
|
+
@target = target
|
|
105
|
+
@mrbc_path = mrbc_path
|
|
106
|
+
# Discovery hints for the compiled runtime — wasm + boot helper
|
|
107
|
+
# + JS bridge. Used by `auto_vendor_compiled_runtime!` so the
|
|
108
|
+
# built dist is fully self-contained and no manual cp into
|
|
109
|
+
# `public/vendor/lilac-compiled/` is required.
|
|
110
|
+
@lilac_compiled_path = lilac_compiled_path
|
|
111
|
+
# Discovery hint for the full runtime wasm — parallel to
|
|
112
|
+
# `@lilac_compiled_path`. Used by `auto_vendor_full_runtime!`
|
|
113
|
+
# via `FullRuntimeResolver` so a `:full` build is reliably
|
|
114
|
+
# self-contained (offline-runnable) without the CDN.
|
|
115
|
+
@lilac_full_path = lilac_full_path
|
|
116
|
+
@mruby_wasm_js_path = mruby_wasm_js_path
|
|
117
|
+
# Pre-compiled Lilac package `.mrb` paths (absolute). For both
|
|
118
|
+
# `:compiled` and `:full` builds these get staged under
|
|
119
|
+
# `dist/packages/`; `:compiled` injects loadBytecode into the
|
|
120
|
+
# generated boot module directly, `:full` writes a
|
|
121
|
+
# `dist/lilac.packages.json` manifest the scaffold boot fetches.
|
|
122
|
+
# See decisions §25 / §26.
|
|
123
|
+
@packages = Array(packages).map { |p| File.expand_path(p) }
|
|
124
|
+
# Mirrors `CompiledRuntimeResolver` / `BytecodeBuilder`'s
|
|
125
|
+
# `disable_gem_discovery:` — tests pass `true` so the gem-bundled
|
|
126
|
+
# wasm doesn't satisfy lookups they're trying to isolate. Plumbed
|
|
127
|
+
# through to both resolvers below.
|
|
128
|
+
@disable_gem_discovery = disable_gem_discovery
|
|
129
|
+
# `:inline` (default) — inject component definitions into each
|
|
130
|
+
# page's HTML. `:bundle` — emit a single dist/lilac.bundle.html
|
|
131
|
+
# referenced from pages via `<link rel="lilac-bundle">`. Runtime
|
|
132
|
+
# registry fetches the bundle and injects templates + evals
|
|
133
|
+
# scripts before mount.
|
|
134
|
+
@delivery = delivery
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def build
|
|
138
|
+
components = load_components
|
|
139
|
+
pages = Dir.glob(File.join(@pages_dir, '**', '*.html'))
|
|
140
|
+
raise Error, "No pages found under #{@pages_dir.inspect}" if pages.empty?
|
|
141
|
+
|
|
142
|
+
public_files = mirror_public_files
|
|
143
|
+
|
|
144
|
+
template_cache = TemplateASTCache.new
|
|
145
|
+
build_linter = BuildLinter.new
|
|
146
|
+
# Resolve and stage package `.mrb` files once for the build —
|
|
147
|
+
# the URLs are stable across pages so each page's boot module
|
|
148
|
+
# can reference the same set. PackageStager owns both the
|
|
149
|
+
# discovered + explicit input channels and the :full-only
|
|
150
|
+
# `lilac.packages.json` manifest emission.
|
|
151
|
+
package_dist_urls = PackageStager.new(
|
|
152
|
+
packages: @packages,
|
|
153
|
+
target: @target,
|
|
154
|
+
output_dir: @output_dir,
|
|
155
|
+
bytecode_builder: bytecode_builder
|
|
156
|
+
).run!
|
|
157
|
+
|
|
158
|
+
# In :bundle delivery mode, emit a single dist/lilac.bundle.html
|
|
159
|
+
# containing all components' templates + scripts. Pages then
|
|
160
|
+
# reference it via <link rel="lilac-bundle">. Done once before
|
|
161
|
+
# page processing so PageCompiler can inject the <link>.
|
|
162
|
+
bundle_assets =
|
|
163
|
+
if @delivery == :bundle
|
|
164
|
+
BundleAssetWriter.new(
|
|
165
|
+
components: components,
|
|
166
|
+
template_cache: template_cache,
|
|
167
|
+
target: @target,
|
|
168
|
+
output_dir: @output_dir,
|
|
169
|
+
bytecode_builder: bytecode_builder
|
|
170
|
+
).write!
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
context = BuildContext.new(
|
|
174
|
+
components: components,
|
|
175
|
+
bundle_assets: bundle_assets,
|
|
176
|
+
package_dist_urls: package_dist_urls,
|
|
177
|
+
template_cache: template_cache,
|
|
178
|
+
build_linter: build_linter,
|
|
179
|
+
bytecode_builder: bytecode_builder,
|
|
180
|
+
target: @target,
|
|
181
|
+
delivery: @delivery,
|
|
182
|
+
live_reload: @live_reload,
|
|
183
|
+
output_dir: @output_dir,
|
|
184
|
+
pages_dir: @pages_dir,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
compiler = PageCompiler.new(context)
|
|
188
|
+
pages.each { |page_path| compiler.compile(page_path) }
|
|
189
|
+
|
|
190
|
+
build_linter.warn_cross_page_signature_drift!
|
|
191
|
+
|
|
192
|
+
# `:compiled` target needs the runtime (wasm + bridge + boot
|
|
193
|
+
# helper) sitting under `dist/vendor/lilac-compiled/`. We emit
|
|
194
|
+
# it from the CLI directly so users don't have to vendor the
|
|
195
|
+
# npm package by hand. Skipped when no `.mrb` was actually
|
|
196
|
+
# produced — pages without any Ruby script don't reference the
|
|
197
|
+
# bootstrap module.
|
|
198
|
+
auto_vendor_compiled_runtime! if @target == :compiled && Dir.glob(File.join(@output_dir, '*.mrb')).any?
|
|
199
|
+
# :full vendor — symmetric to :compiled. Strict: raises with an
|
|
200
|
+
# actionable message when no runtime is discoverable (so an
|
|
201
|
+
# offline build can't silently ship without its wasm), unless the
|
|
202
|
+
# assets are already vendored (manual / public-mirror workflow).
|
|
203
|
+
auto_vendor_full_runtime! if @target == :full
|
|
204
|
+
|
|
205
|
+
{ pages: pages.length, components: components.length, public_files: public_files }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
# Mirror `public/**/*` → `output_dir/`. Preserves the relative
|
|
211
|
+
# directory structure (e.g. `public/vendor/x.js` →
|
|
212
|
+
# `output_dir/vendor/x.js`). Returns the number of files copied.
|
|
213
|
+
#
|
|
214
|
+
# `.gitkeep` is filtered so an empty placeholder file doesn't
|
|
215
|
+
# land in the build output. Other dot-prefixed files (e.g.
|
|
216
|
+
# `.well-known/`) are copied so users can publish standard web
|
|
217
|
+
# conventions.
|
|
218
|
+
def mirror_public_files
|
|
219
|
+
return 0 unless @public_dir && File.directory?(@public_dir)
|
|
220
|
+
|
|
221
|
+
excluded_dirs = EXCLUDED_DIRS_FOR_TARGET.fetch(@target, [])
|
|
222
|
+
copied = 0
|
|
223
|
+
Dir.glob(File.join(@public_dir, '**', '*'), File::FNM_DOTMATCH).each do |source|
|
|
224
|
+
# File.file? already filters out the `.` / `..` directory
|
|
225
|
+
# entries that FNM_DOTMATCH surfaces, so no extra guard needed.
|
|
226
|
+
next unless File.file?(source)
|
|
227
|
+
next if EXCLUDED_BASENAMES.include?(File.basename(source))
|
|
228
|
+
|
|
229
|
+
rel = Pathname.new(source).relative_path_from(Pathname.new(@public_dir)).to_s
|
|
230
|
+
next if excluded_dirs.any? { |prefix| rel == prefix || rel.start_with?("#{prefix}/") }
|
|
231
|
+
|
|
232
|
+
dest = File.join(@output_dir, rel)
|
|
233
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
234
|
+
FileUtils.cp(source, dest)
|
|
235
|
+
copied += 1
|
|
236
|
+
end
|
|
237
|
+
copied
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def load_components
|
|
241
|
+
Dir.glob(File.join(@components_dir, '**', '*.lil')).to_h do |path|
|
|
242
|
+
[File.basename(path, '.lil'), SFC.parse_file(path)]
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Lazily instantiated so `:full` builds incur no mrbc resolution
|
|
247
|
+
# cost (`BytecodeBuilder.new` itself is cheap, but keeping the
|
|
248
|
+
# creation lazy keeps the `:full` happy path obviously side-effect-free).
|
|
249
|
+
def bytecode_builder
|
|
250
|
+
@bytecode_builder ||= BytecodeBuilder.new(
|
|
251
|
+
mrbc_path: @mrbc_path,
|
|
252
|
+
output_dir: @output_dir,
|
|
253
|
+
disable_gem_discovery: @disable_gem_discovery
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Lazily resolves the lilac-compiled runtime (wasm + bridge + boot
|
|
258
|
+
# helper). Constructed only when target=:compiled actually emits a
|
|
259
|
+
# `.mrb`, mirroring `bytecode_builder`'s "no cost on the happy
|
|
260
|
+
# :full path" pattern.
|
|
261
|
+
def compiled_runtime_resolver
|
|
262
|
+
@compiled_runtime_resolver ||= CompiledRuntimeResolver.new(
|
|
263
|
+
lilac_compiled_path: @lilac_compiled_path,
|
|
264
|
+
mruby_wasm_js_path: @mruby_wasm_js_path,
|
|
265
|
+
disable_gem_discovery: @disable_gem_discovery
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Lazily resolves the lilac-full runtime (wasm + bridge), mirroring
|
|
270
|
+
# `compiled_runtime_resolver`.
|
|
271
|
+
def full_runtime_resolver
|
|
272
|
+
@full_runtime_resolver ||= FullRuntimeResolver.new(
|
|
273
|
+
lilac_full_path: @lilac_full_path,
|
|
274
|
+
mruby_wasm_js_path: @mruby_wasm_js_path,
|
|
275
|
+
disable_gem_discovery: @disable_gem_discovery
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# :full target: vendor lilac-full.wasm + bridge into
|
|
280
|
+
# `dist/vendor/lilac-full/` so the scaffold-generated page
|
|
281
|
+
# (`import { createVM } from "/vendor/lilac-full/..."`) finds its
|
|
282
|
+
# runtime assets after `lilac build` — making the dist fully
|
|
283
|
+
# self-contained / offline-runnable with no CDN.
|
|
284
|
+
#
|
|
285
|
+
# Strict, symmetric with `auto_vendor_compiled_runtime!`: raises
|
|
286
|
+
# `FullRuntimeResolver::Error` (actionable message) when no runtime
|
|
287
|
+
# can be discovered, rather than silently shipping a dist missing
|
|
288
|
+
# its runtime. The one exception is the manual-vendor workflow: if
|
|
289
|
+
# the assets are already present under the vendor dir (e.g. mirrored
|
|
290
|
+
# from `public/vendor/lilac-full/` — `mirror_public_files` runs
|
|
291
|
+
# before this — or copied by hand), we skip resolution entirely so
|
|
292
|
+
# gem-less self-hosting still works.
|
|
293
|
+
def auto_vendor_full_runtime!
|
|
294
|
+
return if VendorWriter::REQUIRED_ASSETS[:full]
|
|
295
|
+
.each_value.all? { |rel| File.file?(File.join(@output_dir, rel)) }
|
|
296
|
+
|
|
297
|
+
VendorWriter.copy!(
|
|
298
|
+
wasm_src: full_runtime_resolver.resolve_wasm!,
|
|
299
|
+
bridge_src: full_runtime_resolver.resolve_bridge!,
|
|
300
|
+
vendor_dir: File.join(@output_dir, 'vendor', 'lilac-full'),
|
|
301
|
+
wasm_name: 'lilac-full.wasm'
|
|
302
|
+
)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Emits `dist/vendor/lilac-compiled/{lilac.wasm,mruby-wasm-js/...}`
|
|
306
|
+
# from the resolved runtime sources. The boot module itself is
|
|
307
|
+
# rendered inline in the page HTML (see PageCompiler's
|
|
308
|
+
# render_compiled_boot_module), so we don't need to vendor
|
|
309
|
+
# `index.js`: the page imports the bridge directly and calls
|
|
310
|
+
# `loadBytecode` itself.
|
|
311
|
+
#
|
|
312
|
+
# Raises `CompiledRuntimeResolver::Error` if a source is missing,
|
|
313
|
+
# with an actionable message — the caller (the build command) lets
|
|
314
|
+
# it propagate.
|
|
315
|
+
def auto_vendor_compiled_runtime!
|
|
316
|
+
VendorWriter.copy!(
|
|
317
|
+
wasm_src: compiled_runtime_resolver.resolve_wasm!,
|
|
318
|
+
bridge_src: compiled_runtime_resolver.resolve_bridge!,
|
|
319
|
+
vendor_dir: File.join(@output_dir, 'vendor', 'lilac-compiled'),
|
|
320
|
+
wasm_name: 'lilac.wasm'
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|