Compiles QML to Solid 2 components for the web.
.qml ──oxc (QML mode)──▶ QML tree with Oxc expression leaves
──lower──────────▶ Oxc Program: component function + JSX nodes
──resolve────────▶ QML names bound to JavaScript bindings
──solidjs-compiler▶ Solid's output: components, memos, effects
One parser, one arena, one AST. The QML grammar is a mode of Oxc's own parser,
so binding values, handlers and functions are ordinary Oxc nodes. The lowering
builds the nodes Solid's JSX transform takes as input directly in the arena (no
JSX text is ever printed or parsed), and Solid's transform runs on them
unchanged. Names are resolved at compile time, so nothing interprets QML at run
time: a binding is a getter Solid tracks, and an id is a JavaScript const.
A file that imports Qt's modules is compiled against Qt's own types
(crates/ljos_web/types.txt, generated from the .qmltypes of a Qt
installation), and what it creates are the objects of packages/runtime:
QtQuick written for the DOM on Solid's signals. The yardstick is Qt's own
examples (corpus/qtdoc): mise run gallery compiles each, renders it in a
browser and compares it with the picture real Qt renders.
crates/ljos_web: the compiler (ljos_web::compile,compile_script) and theqmlcbinary.crates/ljos_web_compiler: the same compiler as wasm, whichljos-web/compilerloads the first time a page compiles QML.- The table of Qt's types is made by Ljós's
qmltypesgenerator (mise run types), in the ljos-ui repository the front end comes from. packages/runtime: what compiled output imports, one module per QML module (ljos-web/QtQuick,ljos-web/QtQuick/Layouts,ljos-web/QtQml, ...), the kernel they are written on (ljos-web/object), the Vite plugin (ljos-web/vite), and the site builder (ljos-web/site,ljos-web build-site).examples/gallery: Qt's examples in the browser, and the report on how each does.examples/web: QML rendered in the browser with theimport OpenTUIdialect.examples/wasm-counter: a type written in Rust, compiled to WebAssembly and used from QML in the browser — the first spike of ljos-web (docs/LJOS_WEB.md).corpus/qtdoc: Qt's own examples (a submodule);corpus/examples.jsonsays where each starts and what it needs,corpus/referencehas Qt's pictures of them.mise-tasks/: install, build, run, test, gallery and corpus, see below.vendor/: patched upstream crates, see below.
cargo run --bin qmlc -- File.qml # JavaScript on stdout
cargo run --bin qmlc -- logic.js # a script a QML file imports, as a module
cargo run --bin qmlc -- --emit lowered File.qml # the tree given to Solid, printed
cargo run --bin qmlc -- --out-dir out *.qml
cargo run --bin qmlc -- --root DIR File.qml # where the project's modules are looked for
cargo run --bin qmlc -- -I mocks File.qml # an import path looked in before the root
cargo run --bin qmlc -- --qt DIR File.qml # where the installed Qt's modules are
cargo run --bin qmlc -- --alone File.qml # without the files next to it
cargo run --bin qmlc -- --types QtQuick.Layouts # the types Qt has of a module in C++
cargo run --bin qmlc -- --theme theme.json # a theme file checked: its overrides as JSON, or its mistakes
cargo run --bin qmlc -- --theme-for app/Main.qml --root . # the same for the theme.json nearest an entry
cargo run --bin qmlc -- --inspect src/Main.qml src/Main.qml # saying where things are written, for ljos-web/inspectEverything else is a mise task, a script in
mise-tasks/ that names what it depends on, so each of these first does
whatever it needs:
mise run install # the tooling in mise.toml: Rust (with the wasm target), Node, pnpm
mise run build # build:compiler (qmlc) and build:web (the example's bundle)
mise run dev # the example, with Vite; QML recompiles as it is edited
mise run test # test:compiler, test:web, test:runtime and test:gallery
mise run gallery # Qt's examples in a browser, against Qt's pictures of them
mise run gallery:serve # the same page, to look at
mise run corpus # how much of Qt's examples compiles, and what stops the rest
mise run reference # take Qt's pictures again (needs Qt 6's `qml` tool; `sway` for shader effects)
mise run assets ID... # download what an example's build downloads (its pictures, meshes, fonts)
mise run types # the table of Qt's types, from the Qt installed here
mise run site:build DIR # a static site from a directory with ljos-site.json
mise run build:wasm-compiler # the compiler as wasm, for ljos-web/compiler
mise run controls # copy Ljós's style of the controls again from a ljos-ui checkout (LJOS_UI)packages (pnpm install) and browser (Playwright's Chromium) are tasks the
others depend on.
The table of types covers the modules Qt's examples import, some of which a
distribution packages apart from Qt itself (Charts, Graphs, Quick 3D, Location,
Sensors, Timeline). Where those are not installed with the rest, mise run types -- --qt /usr/lib/qt6/qml --qt elsewhere/lib/qt6/qml reads them from
wherever their packages were unpacked; a table made without them loses their
types. mise run reference finds them as Qt does, by QML_IMPORT_PATH,
QT_PLUGIN_PATH and LD_LIBRARY_PATH.
ljos-web/vite is a Vite plugin that runs qmlc on .qml imports and on the
scripts they import:
import qml from "ljos-web/vite";
export default { plugins: [qml({ qmlc: "path/to/qmlc" })] };import { mount } from "ljos-web/object";
import Main from "./Main.qml";
mount(Main, document.getElementById("app"));A page may also compile QML as it runs, from texts it has, with
ljos-web/compiler (docs/LJOS_WEB.md A15). Nothing of the compiler is
fetched until the first compile:
import { compile } from "ljos-web/compiler";
const made = await compile({ files: { "Main.qml": text }, entry: "Main.qml", aliases: [] });
if (made.diagnostics) show(made.diagnostics); // [{ file, line, column, message }]
else mount(made.component, document.getElementById("app"));A root that is a Window is the size of the element it is mounted in. One that
is an Item keeps its own, and its parent is an item of the element's size,
as the root of a QQuickView is in the view: anchors.fill: parent fills it.
A save in the dev server keeps the page's state, as ljos run --watch does:
the root mounted from the saved file takes the new component, and each of its
objects that is one of the old (by id, objectName, row of a view, else
type and place) keeps what the program and the user gave it — typed text, a
value a handler assigned, which item has the keyboard — unless the file now
says something else of that property. Bindings are the new file's; an object
whose type changed is made anew. A file that does not compile leaves the page
as it was, beside Vite's error. A page that compiles QML itself does the same
without Vite:
import { mount } from "ljos-web/object";
import { reload } from "ljos-web/reload";
const root = mount(Main, element, {}, { reloadable: true });
// later, with the component the new text compiled to
reload(root, NewMain); // throws ReloadError, and changes nothing, if NewMain cannot be madereloadable is on by itself in a dev server; elsewhere a root is not, and
the page pays nothing for it. docs/LJOS_WEB.md (A13) says what is kept and
where the web differs from desktop Ljós.
Much of Qt is QML itself: a style of Qt Quick Controls is a directory of QML
files over the types of QtQuick.Templates. Where no Qt 6 is installed, the
plugin compiles Ljós's own style instead (Basic, Fusion, Material, Universal),
ljos-ui's crates/ljos_controls/qml copied into packages/runtime/controls/
at the commit ljos_design is pinned at (mise run controls copies it again;
the option styles: "ljos" or "qt" says which a build takes, and
docs/LJOS_WEB.md A20 why). Qt's are not rewritten here. The
plugin puts a module of Qt's together from what the runtime has of it (the
types Qt has in C++) and the QML files of the Qt that is installed, which it
compiles as it does the project's. qt is where that Qt keeps its QML modules,
when not where qtpaths6 --query QT_INSTALL_QML says: a directory, or several,
as QML_IMPORT_PATH names more of them when qt is not given; style is what
import QtQuick.Controls is ("Material", "Fusion", "Imagine"), or a function of the
importing file that says. The pictures a style names by qrc:/ are inside
its plugin; Qt's own qml tool reads them out, once, into Vite's cache. Qt's
QML and pictures are read from the installation and are not part of this
repository: what is built from them carries Qt's licence.
A type Qt has in C++ and the runtime does not have yet is in its module all the
same (qmlc --types says which those are): a style names every control, and a
program that uses three of them needs those three. Using one that is not there
is an error that names it, QtQuick.Templates: Dial is not in ljos-web yet.
Likewise a type that is QML of Qt's own (qmlc --qml-types), where that module
of Qt's is not installed: QtQuick3D.Helpers: OrbitCameraController is QML of Qt's own, and Qt's QtQuick3D.Helpers is not installed here. The gallery tells
of such an example and does not hold it to what it rendered before.
Some of Qt's examples have their pictures, meshes and fonts downloaded when
they are built, not in their sources. The manifest says where from (assets),
and mise run assets ID fetches them into ~/.cache/ljos-web/assets (or
LJOS_WEB_ASSETS): the gallery serves them from there as if they were in the
example, and mise run reference puts them where the build would. An example
without them still renders, with what it has, and is not held to Qt's picture.
One that downloads them itself as it starts (car-configurator) asks a browser
for them one by one, at the address they are kept at: with a network it has
them from there, and the gallery's test, which has none, is given what was
fetched. FX_Material_Showroom's main.cpp downloads a list of files to beside
the program, and its QML reads them from there: what stands in for main.cpp
says they are at the address it downloads from, and mise run assets fetches
each file of the list.
A path in QML is relative to a file: the one that has the object that loads
it. Image { source: "a.png" } is next to the file that says so, and so is
the source of a component that is an Image; a path given to a property a
component declares (property url shown, property alias source: image.source) is next to the component's file, where it is used. A program
whose main.cpp sets QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT has them all next
to the file they are written in: qmlc --urls-on-assignment, which the
plugin's args may give by the file compiled. QML that stands in for a type
in C++ (--with) has a path as that type does: next to the file that makes
the object.
ljos-web build-site DIR [--out DIR] [--with DIR]... [--qt DIR]... (the
bin of packages/runtime, mise run site:build DIR, or
buildSite({ dir, out }) of ljos-web/site) builds a static site from a
directory that holds ljos-site.json, the manifest of what it is:
{
"format": "ljos-site", "version": 1, "target": "web",
"entry": "Shell.qml", "theme": "theme.json",
"files": [{ "path": "Shell.qml", "bytes": 209 }, ...],
"modules": [{ "module": "App.Media", "qmldir": "App/Media/qmldir" }],
"imports": ["Ljos.Design", "QtQuick"],
"blocks": [], "left_out": [], "ljos_web": "<the commit it was checked against>"
}Every file of the directory is listed with its size and nothing else is
in it; anything the manifest does not say (a field, a file, a link, a
module, an import ljos-web does not provide) is refused before anything
is compiled. The entry is compiled with the directory as the root of the
project, and the site goes to DIR-site unless --out says where: a
site it built there is replaced, anything else is not. Its controls are
Ljós's own style, so no Qt is needed for QtQuick.Controls; --styles qt
builds it with the installed Qt's style instead, and a style Ljós has none of
(Imagine) needs that Qt. Ljos.Design is Ljós's QML,
given with --with (ljos-ui's crates/ljos_design_qml/qml). The theme
is the manifest's theme, or the theme.json nearest the entry among the
listed files; it is applied, and a theme that is refused refuses the site
with each mistake as file:line:column: message.
ljos-web modules --json prints packages/runtime/modules.json: the
modules this commit provides, where each one's types come from and what
building with it needs (qt: an installed Qt's QML; with: a directory
of the module's QML). mise run test:site builds bundles into sites,
opens one in headless Chromium and tries each refusal.
A page reads a theme as ljos-ui's docs/theme.md says: other values for
Ljos.Design's roles, in the theme.json nearest the entry (from its
directory up to the project's), or in a file that is named. Whatever binds
to a role follows, a colour's variants follow Colors.dark and
Colors.highContrast, and nothing is made again: a field keeps its text.
// vite.config.js: the nearest theme.json, or one named, or none.
qml({ qmlc, root: "app", theme: "themes/dark.json" /* or false */, seedHue: 200 });In the dev server a change of the file is applied without a remount; a refused one shows Vite's error and leaves the page's theme. A host that holds files in memory applies one itself:
import { applyTheme, removeTheme, findTheme } from "ljos-web/theme";
const file = findTheme(Object.keys(files), "app/Main.qml"); // "app/theme.json", or null
const said = await applyTheme(files[file], { file, check }); // { ok: true } | { diagnostics }
removeTheme(); // the module's own roles againcheck({ file, text }) answers as qmlc --theme does: the overrides it
prints, or { diagnostics: [{ file, line, column, message }] }. A refused
theme is refused whole. Under it is override(object, key, { value } | { binding }) of ljos-web/object, for any property.
QML lets an instance set any property of a component's root object, handle its
signals and give it children. A component is therefore compiled with the files
next to it and the modules it imports (ljos_web::Project; qmlc and the
Vite plugin read the file's directory, its qmldir and the directories it
imports): what a type is, what its properties are and which names are ids of
an enclosing file are all known when the file is compiled. A name nothing
declares is a compile error where it is used, and so is the import of a
module nobody has: the project, Qt's table, the runtime or the Qt that is
installed (--qt). qmlc --alone compiles a file by
itself: unknown types are taken to be components.
A file the build keeps in the program is named by where it is in there:
qrc:/qt/qml/Thermostat/images/icon.png. qmlc reads what the project's
CMakeLists.txt says those are (the RESOURCES of qt_add_qml_module, the
FILES of qt_add_resources), and a module that writes such a name, or the
start of one it puts together as the program runs, says where a browser finds
the file. The program still reads the name it wrote.
A file the program has beside it as it runs is named without saying from
where, file:content/images/dust.png: Qt looks in the directory the program
was started in. The program put it there itself, so what stands in for that
part of it says where a browser finds the directory:
beside("file:content/", url) of ljos-web/object.
A file is a function that makes its root object:
export default function Panel($props) {
const root = $props.$self ?? $object();
return <Rectangle $self={root} $given={$props} $declare={Panel$} color={...}>{$props.children}</Rectangle>;
}(as nodes, not text). $declare is what the file declares on top of its root
type: properties, signals, functions, aliases. Enums are members of the
function, a pragma Singleton file exports the one object.
import "logic.js" as Logic is a module import: qmlc makes an ES module of
the script, exporting what it declares, with .import lines as imports.
Qt compiles a QML file when the program first names it by path. Here files are
compiled ahead of time, so a path is turned into what it names when the file is
compiled: Qt.createComponent("Block.qml"), source: "Page.qml" and
Qt.resolvedUrl("Page.qml") import the file and give the component, there at
once, with Qt's status and createObject. A path put together at run time
("towers/" + name + ".qml") is looked up among the files of the directory
that it could name.
inspect(root) of ljos-web/inspect reads and writes a mounted root's
properties by id, clicks an object or a point, lists the object tree,
says which object is drawn at a point and where it is written, and keeps
the console's lines with their file and line. The answers are shaped as
Ljós's own inspector gives them (docs/LJOS_WEB.md, A16).
import { mount } from "ljos-web/object";
import { inspect } from "ljos-web/inspect";
const { object } = mount(Main, element);
const inspector = inspect(object);
inspector.write("root", "count", 4);
inspector.click({ at: [10, 60] });
inspector.pick(10, 60).source; // { file: "src/Main.qml", line: 22, column: 5 }Places come from qmlc, only when asked: --inspect, or the Vite plugin's
inspect: true in a dev server's config. A build without it carries none of
them and is the same size as before.
inspector.screenshot({ max_size: 1280 }) is a PNG of the root (or of the part
an object covers, target), at the page's device pixel ratio: { png, width, height, scale, bytes, area }. The browser draws a copy of the root's elements
with their computed styles; what it cannot draw (an image from an origin that
does not allow reading it, a WebGL canvas that does not keep its drawing, a
frame) refuses the picture by name instead of leaving it out. Its code,
ljos-web/picture, is loaded only when a picture is asked for (A17).
A program's Rust types, as its ljostypes.json describes them, stand in when
QML is previewed without the program (docs/LJOS_WEB.md, A18): each is a type
with the dump's properties, enums and signals, and methods that do nothing.
cargo run --bin qmlc -- --types ljostypes.json --root . Main.qml # repeatableqml({ qmlc, root: "app" }); // every ljostypes.json under app
qml({ qmlc, types: ["rust/ljostypes.json"] }); // or the ones named
await compile({ files, entry: "Main.qml", aliases: [], types: ["native/ljostypes.json"] });A dump that does not read stops the compile with the reason, and a member a
described type does not have is an error where it is written. A mock of the
same type (-I mocks) or the project's own QML wins. A site build takes none.
import Ljos.Models gives JsonListModel, a list whose rows are the objects
of a JSON array in json or in the file source names, as Ljós has it
natively. A dev server reads the file again when it is saved, a built site
when reload() is called, and a page that compiles QML reads it from the
files given to compile. Ljos.Observer is refused by qmlc: a page inspects
itself with ljos-web/inspect (docs/LJOS_WEB.md, A19).
import QtQuick
import Ljos.Models
ListView {
model: JsonListModel { id: people; source: "data/people.json" }
delegate: Text { required property string name; text: name }
}import Ljos.Blocks gives a page Ljós's Contribution, Slot and
ShellLoader (docs/LJOS_WEB.md A21). A ShellLoader whose source is a
file of the build shows it as any compiled QML; one whose source is not
(a user's own Shell.qml at a URL) fetches and compiles it in the page
with ljos-web/compiler, loaded only then, and shows fallback with
error saying file:line: message when it does not work. reload()
compiles it again and keeps the state of what is in both.
import QtQuick
import Ljos.Blocks
Item {
Contribution { slot: "toolbar"; Rectangle { width: 20; height: 20 } }
ShellLoader {
anchors.fill: parent
source: "user/Shell.qml" // not in the build: compiled in the page
fallback: "Default.qml" // in the build
}
}A site built with qml({ liveShell: true }) (or buildSite({ liveShell: true }))
lets its visitors drop a Shell.qml of their own over its blocks
(docs/LJOS_WEB.md A9). A block is a type its module's qmldir marks:
module Shop
Basket 1.0 Basket.qml
# block Basket 1.0 Basket.qml Shop "Holds what the visitor buys"
Its read-only properties are its state, its functions its actions, its
signals its events. The page mounts its entry with liveShell:
import { liveShell } from "ljos-web/live";
import Shell from "./Shell.qml";
liveShell(Shell, document.getElementById("app"), {
publish: async (text, { blocks, hash }) => ({ url: await save(text) }), // optional
});A dropped shell is compiled in a worker by the page's compiler, fetched on
the first drop, to data the page runs (never to JavaScript); a name outside
its scope (window, eval, x["constructor"], a URL of its own, an import
beyond QtQuick and the block modules) is an error at its line and column,
shown over the shell that keeps running. The blocks keep their state across
shells. The visitor's shell is kept in localStorage for the site until
Reset. Until then, on every visit, what blocks write to Settings and
QtQuick.LocalStorage lasts only as long as the page, and the first such write
warns (docs/LJOS_WEB.md, What a kept shell costs a site). Under a CSP of
script-src 'self', serve shell-worker-*.js with
script-src 'self' 'wasm-unsafe-eval'. A site without liveShell has none
of it and is the same size.
Of the language: property declarations (with required, readonly,
default, aliases of properties and of objects), ids, bindings and handlers,
functions, signals, inline components, enums, singletons, attached and grouped
properties, objects and lists of objects as property values, Component,
value sources and interceptors (NumberAnimation on x, Behavior on x),
Connections, PropertyChanges, qualified imports and type names, modules of
the project found through qmldir, scripts.
Not yet: scripts that are not .pragma library share their state between the
objects that import them and do not see those objects' names; Qt.createQmlObject
(there is no compiler in the page); types registered from C++, which need a
stand-in. Anything unsupported is a compile error that names what is missing.
Of Qt's modules, in packages/runtime: QtQuick (items, rectangles and
gradients, transforms, positioners, anchors, states and transitions,
animations and behaviors, timers, windows and screens, palettes, models,
Repeater, ListView, GridView, Flickable, Loader), QtQuick.Layouts,
QtQuick.Window, QtQml (the Qt object, locales, Component), QtCore and
Qt.labs.settings. mise run gallery says, for each of Qt's examples, which
modules it still lacks.
Of the styles of Qt Quick Controls, Imagine too: its controls are pictures,
which NinePatchImage stretches as their marks say and the image selectors
of QtQuick.Controls.impl choose by a control's states, both as Qt does. The
style's own pictures are read out of its plugin. Pictures of the project's own,
named with Imagine.path or the [Imagine] group of qtquickcontrols2.conf,
are found once the page says which files there are (the kernel's resources,
an address for each), since a page cannot look into a folder. The @2x
pictures are not used, and the font is Qt's own, not Open Sans.
The import OpenTUI dialect (crates/ljos_web/src/dialects) is the other
way a module can be given: a table that says what its types and properties
mean on the DOM, compiled to Solid's templates with no runtime types at all
(Item, Rectangle, Text, Span, Bold, Repeater; a cell is 1ch wide
and 1lh tall). The fixtures in crates/ljos_web/tests/fixtures/tui use it.
- The QML parser is not vendored here: it is Ljós's
qml_syntaxcrate (a git dependency on the ljos-ui repository), whose oxc fork carries the QML mode. There is one copy of that fork, and it lives there. vendor/solidjs-compiler:packages/compilerof solidjs/solid ate44b2e4(2.0.0-rc.13, MIT). Addedcompile_program, which takes an OxcPrograminstead of source text; default features are empty so nothing needs Node.