Class: Lilac::CLI::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/lilac/cli/build/builder.rb

Overview

Orchestrates a full build: loads components, stages packages, writes the (optional) bundle file, compiles each page via PageCompiler, then vendors runtime assets into dist/vendor/.

The per-page rewriting logic lives in PageCompiler; HTML helpers in HtmlEmitter; template parse caching in TemplateASTCache; all of those share state through the BuildContext value object this class assembles.

Defined Under Namespace

Classes: Error

Constant Summary collapse

EXCLUDED_BASENAMES =

Filenames that must not land in the build output even when they exist under public/. Add to this list as new conventions are encountered (e.g. .DS_Store, Thumbs.db).

%w[.gitkeep].freeze
EXCLUDED_DIRS_FOR_TARGET =

Target-specific public/ subdirectories that should NOT be mirrored into dist/ for the inactive target. Both runtime variants live under their own namespace in public/vendor/ so a project that ships both (e.g. dev=full, prod=compiled) can keep them side by side and let the CLI prune the inactive one at build time.

Paths are relative to public_dir, POSIX style. The match is on path prefix + boundary, so vendor/lilac-full matches vendor/lilac-full/lilac-full.wasm but not vendor/lilac-full-x.

{
  full: %w[vendor/lilac-compiled].freeze,
  compiled: %w[vendor/lilac-full].freeze
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(components_dir:, pages_dir:, output_dir:, public_dir: nil, live_reload: false, target: :full, mrbc_path: nil, lilac_compiled_path: nil, lilac_full_path: nil, mruby_wasm_js_path: nil, packages: [], disable_gem_discovery: false, delivery: :inline) ⇒ Builder

Returns a new instance of Builder.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/lilac/cli/build/builder.rb', line 81

def initialize(components_dir:, pages_dir:, output_dir:, public_dir: nil,
               live_reload: false,
               target: :full, mrbc_path: nil,
               lilac_compiled_path: nil, lilac_full_path: nil,
               mruby_wasm_js_path: nil,
               packages: [],
               disable_gem_discovery: false,
               delivery: :inline)
  @components_dir = components_dir
  @pages_dir = pages_dir
  @output_dir = output_dir
  # public_dir is optional. When nil or absent on disk, the
  # mirroring step is skipped — projects that don't need static
  # passthrough (no vendor bundle, no images) work fine without
  # creating the directory.
  @public_dir = public_dir
  @live_reload = live_reload
  # `:full` — dist HTML loads inline Ruby via lilac-full wasm
  # (vm.evalScript). `:compiled` — Ruby is pre-compiled to
  # `.mrb` bytecode via `mrbc` and loaded by lilac-compiled wasm
  # (vm.loadBytecode). The compiled target shaves ~32% off the brotli
  # bundle but requires `mrbc` available at build time. See
  # `BytecodeBuilder` for path discovery.
  @target = target
  @mrbc_path = mrbc_path
  # Discovery hints for the compiled runtime — wasm + boot helper
  # + JS bridge. Used by `auto_vendor_compiled_runtime!` so the
  # built dist is fully self-contained and no manual cp into
  # `public/vendor/lilac-compiled/` is required.
  @lilac_compiled_path = lilac_compiled_path
  # Discovery hint for the full runtime wasm — parallel to
  # `@lilac_compiled_path`. Used by `auto_vendor_full_runtime!`
  # via `FullRuntimeResolver` so a `:full` build is reliably
  # self-contained (offline-runnable) without the CDN.
  @lilac_full_path     = lilac_full_path
  @mruby_wasm_js_path  = mruby_wasm_js_path
  # Pre-compiled Lilac package `.mrb` paths (absolute). For both
  # `:compiled` and `:full` builds these get staged under
  # `dist/packages/`; `:compiled` injects loadBytecode into the
  # generated boot module directly, `:full` writes a
  # `dist/lilac.packages.json` manifest the scaffold boot fetches.
  # See decisions §25 / §26.
  @packages            = Array(packages).map { |p| File.expand_path(p) }
  # Mirrors `CompiledRuntimeResolver` / `BytecodeBuilder`'s
  # `disable_gem_discovery:` — tests pass `true` so the gem-bundled
  # wasm doesn't satisfy lookups they're trying to isolate. Plumbed
  # through to both resolvers below.
  @disable_gem_discovery = disable_gem_discovery
  # `:inline` (default) — inject component definitions into each
  # page's HTML. `:bundle` — emit a single dist/lilac.bundle.html
  # referenced from pages via `<link rel="lilac-bundle">`. Runtime
  # registry fetches the bundle and injects templates + evals
  # scripts before mount.
  @delivery = delivery
end

Class Method Details

.from_config(config, live_reload: false, target: nil, delivery: nil) ⇒ Object

Construct a Builder from a resolved Config. Centralises the Config-attr → Builder-kwarg mapping so callers in Command and DevServer don't drift out of sync. Overridable kwargs cover the per-caller differences:

- `target:` — Command picks `config.build_target`, DevServer
picks `config.dev_target`. Defaults to the build target.
- `delivery:` — either caller can pin a value here; both
currently leave it unset so `config.delivery` wins (build
and dev share the same delivery path, matching what ships
to prod).
- `live_reload:` — DevServer turns it on for SSE-driven page
reload; `lilac build` leaves it off.


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/lilac/cli/build/builder.rb', line 63

def self.from_config(config, live_reload: false,
                     target: nil, delivery: nil)
  new(
    components_dir: config.components_dir,
    pages_dir: config.pages_dir,
    output_dir: config.output_dir,
    public_dir: config.public_dir,
    target: target || config.build_target,
    mrbc_path: config.mrbc_path,
    lilac_compiled_path: config.lilac_compiled_path,
    lilac_full_path: config.lilac_full_path,
    mruby_wasm_js_path: config.mruby_wasm_js_path,
    packages: config.packages,
    delivery: delivery || config.delivery,
    live_reload: live_reload
  )
end

Instance Method Details

#buildObject

Raises:



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/lilac/cli/build/builder.rb', line 137

def build
  components = load_components
  pages = Dir.glob(File.join(@pages_dir, '**', '*.html'))
  raise Error, "No pages found under #{@pages_dir.inspect}" if pages.empty?

  public_files = mirror_public_files

  template_cache = TemplateASTCache.new
  build_linter = BuildLinter.new
  # Resolve and stage package `.mrb` files once for the build —
  # the URLs are stable across pages so each page's boot module
  # can reference the same set. PackageStager owns both the
  # discovered + explicit input channels and the :full-only
  # `lilac.packages.json` manifest emission.
  package_dist_urls = PackageStager.new(
    packages: @packages,
    target: @target,
    output_dir: @output_dir,
    bytecode_builder: bytecode_builder
  ).run!

  # In :bundle delivery mode, emit a single dist/lilac.bundle.html
  # containing all components' templates + scripts. Pages then
  # reference it via <link rel="lilac-bundle">. Done once before
  # page processing so PageCompiler can inject the <link>.
  bundle_assets =
    if @delivery == :bundle
      BundleAssetWriter.new(
        components: components,
        template_cache: template_cache,
        target: @target,
        output_dir: @output_dir,
        bytecode_builder: bytecode_builder
      ).write!
    end

  context = BuildContext.new(
    components: components,
    bundle_assets: bundle_assets,
    package_dist_urls: package_dist_urls,
    template_cache: template_cache,
    build_linter: build_linter,
    bytecode_builder: bytecode_builder,
    target: @target,
    delivery: @delivery,
    live_reload: @live_reload,
    output_dir: @output_dir,
    pages_dir: @pages_dir,
  )

  compiler = PageCompiler.new(context)
  pages.each { |page_path| compiler.compile(page_path) }

  build_linter.warn_cross_page_signature_drift!

  # `:compiled` target needs the runtime (wasm + bridge + boot
  # helper) sitting under `dist/vendor/lilac-compiled/`. We emit
  # it from the CLI directly so users don't have to vendor the
  # npm package by hand. Skipped when no `.mrb` was actually
  # produced — pages without any Ruby script don't reference the
  # bootstrap module.
  auto_vendor_compiled_runtime! if @target == :compiled && Dir.glob(File.join(@output_dir, '*.mrb')).any?
  # :full vendor — symmetric to :compiled. Strict: raises with an
  # actionable message when no runtime is discoverable (so an
  # offline build can't silently ship without its wasm), unless the
  # assets are already vendored (manual / public-mirror workflow).
  auto_vendor_full_runtime! if @target == :full

  { pages: pages.length, components: components.length, public_files: public_files }
end