ourui 0.1.0__py3-none-any.whl

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.
ourui/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """OurUI authoring surface and compiler package."""
2
+
3
+ from ourui.ui import Component, State, component, server, ui
4
+
5
+ __all__ = ["ui", "server", "State", "Component", "component"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,337 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ourui.analysis.components import (
9
+ collect_components,
10
+ component_call_name,
11
+ expand_component_call,
12
+ )
13
+ from ourui.node import THEME_ATTR_KEYS, Node
14
+ from ourui.parse import call_kind, literal_value, parse_file, span_for
15
+ from ourui.theme import apply_theme_overrides, default_tokens, theme_kwargs_to_overrides
16
+
17
+
18
+ @dataclass
19
+ class SemanticGraph:
20
+ nodes: dict[str, Node] = field(default_factory=dict)
21
+ roots: list[str] = field(default_factory=list)
22
+ edges: list[dict[str, str]] = field(default_factory=list)
23
+ handlers: dict[str, dict[str, Any]] = field(default_factory=dict)
24
+ states: dict[str, dict[str, Any]] = field(default_factory=dict)
25
+ components: dict[str, dict[str, Any]] = field(default_factory=dict)
26
+ routes: dict[str, str] = field(default_factory=dict)
27
+ tokens: dict[str, dict[str, str]] = field(default_factory=dict)
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return {
31
+ "nodes": {nid: n.to_dict() for nid, n in sorted(self.nodes.items())},
32
+ "roots": list(self.roots),
33
+ "routes": {k: self.routes[k] for k in sorted(self.routes)},
34
+ "edges": sorted(self.edges, key=lambda e: (e["from"], e["to"], e["kind"])),
35
+ "handlers": {k: self.handlers[k] for k in sorted(self.handlers)},
36
+ "states": {k: self.states[k] for k in sorted(self.states)},
37
+ "components": {k: self.components[k] for k in sorted(self.components)},
38
+ "tokens": {
39
+ "light": {k: self.tokens.get("light", {})[k] for k in sorted(self.tokens.get("light", {}))},
40
+ "dark": {k: self.tokens.get("dark", {})[k] for k in sorted(self.tokens.get("dark", {}))},
41
+ },
42
+ }
43
+
44
+
45
+ @dataclass
46
+ class DependencyGraph:
47
+ edges: list[dict[str, str]] = field(default_factory=list)
48
+
49
+ def to_dict(self) -> dict[str, Any]:
50
+ return {
51
+ "edges": sorted(self.edges, key=lambda e: (e["from"], e["to"], e["kind"])),
52
+ }
53
+
54
+
55
+ def _is_server_decorator(dec: ast.AST) -> bool:
56
+ if isinstance(dec, ast.Name) and dec.id == "server":
57
+ return True
58
+ if isinstance(dec, ast.Attribute) and dec.attr == "server":
59
+ return True
60
+ return False
61
+
62
+
63
+ def _is_state_call(call: ast.Call) -> bool:
64
+ if isinstance(call.func, ast.Name) and call.func.id == "State":
65
+ return True
66
+ if isinstance(call.func, ast.Attribute) and call.func.attr == "State":
67
+ return True
68
+ return False
69
+
70
+
71
+ def _is_theme_call(call: ast.Call) -> bool:
72
+ if isinstance(call.func, ast.Attribute) and call.func.attr == "Theme":
73
+ if isinstance(call.func.value, ast.Name) and call.func.value.id == "ui":
74
+ return True
75
+ if isinstance(call.func, ast.Name) and call.func.id == "Theme":
76
+ return True
77
+ return False
78
+
79
+
80
+ class _GraphBuilder:
81
+ def __init__(self, path: str, components: dict) -> None:
82
+ self.path = path
83
+ self.counter = 0
84
+ self.graph = SemanticGraph(tokens=default_tokens())
85
+ self.dep = DependencyGraph()
86
+ self._theme_nodes: dict[str, str] = {}
87
+ self._page_route_by_node: dict[str, str] = {}
88
+ self.components = components
89
+ for name, cdef in components.items():
90
+ self.graph.components[name] = {
91
+ "name": name,
92
+ "style": cdef.style,
93
+ "params": list(cdef.params),
94
+ "line": cdef.span_line,
95
+ }
96
+
97
+ def next_id(self, prefix: str = "n") -> str:
98
+ self.counter += 1
99
+ return f"{prefix}{self.counter:04d}"
100
+
101
+ def theme_node(self, token: str, at: ast.AST) -> str:
102
+ if token in self._theme_nodes:
103
+ return self._theme_nodes[token]
104
+ nid = self.next_id("theme")
105
+ node = Node(
106
+ id=nid,
107
+ kind="ThemeToken",
108
+ span=span_for(self.path, at),
109
+ attributes={"token": token},
110
+ provenance=["analyze:theme_token"],
111
+ ).with_hash()
112
+ self.graph.nodes[nid] = node
113
+ self._theme_nodes[token] = nid
114
+ return nid
115
+
116
+ def register_handler(self, name: str, *, kind: str, at: ast.AST) -> None:
117
+ self.graph.handlers[name] = {
118
+ "name": name,
119
+ "kind": kind,
120
+ "span": span_for(self.path, at).to_dict(),
121
+ }
122
+
123
+ def register_state(self, name: str, call: ast.Call) -> None:
124
+ initial: Any = None
125
+ if call.args:
126
+ initial = literal_value(call.args[0])
127
+ self.graph.states[name] = {
128
+ "name": name,
129
+ "initial": initial,
130
+ "span": span_for(self.path, call).to_dict(),
131
+ }
132
+
133
+ def register_theme(self, call: ast.Call) -> None:
134
+ attrs: dict[str, Any] = {}
135
+ for kw in call.keywords:
136
+ if kw.arg is None:
137
+ continue
138
+ attrs[kw.arg] = literal_value(kw.value)
139
+ light, dark = theme_kwargs_to_overrides(attrs)
140
+ self.graph.tokens = apply_theme_overrides(self.graph.tokens, light=light, dark=dark)
141
+
142
+ def _maybe_state_ref(self, node: ast.AST) -> dict[str, str] | None:
143
+ if isinstance(node, ast.Name) and node.id in self.graph.states:
144
+ return {"__state__": node.id}
145
+ return None
146
+
147
+ def _resolve_call(self, call: ast.Call, trail: list[str]) -> tuple[ast.Call, list[str]]:
148
+ """Expand user components until a ui.* call remains."""
149
+ while component_call_name(call, self.components):
150
+ name = component_call_name(call, self.components)
151
+ assert name is not None
152
+ expanded = expand_component_call(call, self.components)
153
+ if expanded is None:
154
+ break
155
+ trail = [*trail, name]
156
+ call = expanded
157
+ return call, trail
158
+
159
+ def build_call(
160
+ self,
161
+ call: ast.Call,
162
+ parent_id: str | None = None,
163
+ expansion_trail: list[str] | None = None,
164
+ ) -> str | None:
165
+ trail = list(expansion_trail or [])
166
+ call, trail = self._resolve_call(call, trail)
167
+
168
+ kind = call_kind(call)
169
+ if kind is None:
170
+ return None
171
+
172
+ nid = self.next_id()
173
+ attrs: dict[str, Any] = {}
174
+ child_ids: list[str] = []
175
+
176
+ for arg in call.args:
177
+ if isinstance(arg, ast.Call):
178
+ cid = self.build_call(arg, parent_id=nid, expansion_trail=trail)
179
+ if cid:
180
+ child_ids.append(cid)
181
+ continue
182
+ if isinstance(arg, ast.List):
183
+ for elt in arg.elts:
184
+ if isinstance(elt, ast.Call):
185
+ cid = self.build_call(elt, parent_id=nid, expansion_trail=trail)
186
+ if cid:
187
+ child_ids.append(cid)
188
+ continue
189
+ state_ref = self._maybe_state_ref(arg)
190
+ if state_ref and kind in {"Button", "Text", "Card"} and "text" not in attrs:
191
+ attrs["text"] = state_ref
192
+ continue
193
+ val = literal_value(arg)
194
+ if isinstance(val, str):
195
+ if kind in {"Button", "Text", "Card"} and "text" not in attrs:
196
+ attrs["text"] = val
197
+ elif "title" not in attrs:
198
+ attrs["title"] = val
199
+ else:
200
+ attrs.setdefault("_args", []).append(val)
201
+ else:
202
+ attrs.setdefault("_args", []).append(val)
203
+
204
+ for kw in call.keywords:
205
+ if kw.arg is None:
206
+ continue
207
+ if kw.arg == "children":
208
+ val_node = kw.value
209
+ if isinstance(val_node, (ast.List, ast.Tuple)):
210
+ for elt in val_node.elts:
211
+ if isinstance(elt, ast.Call):
212
+ cid = self.build_call(elt, parent_id=nid, expansion_trail=trail)
213
+ if cid:
214
+ child_ids.append(cid)
215
+ elif isinstance(val_node, ast.Call):
216
+ cid = self.build_call(val_node, parent_id=nid, expansion_trail=trail)
217
+ if cid:
218
+ child_ids.append(cid)
219
+ continue
220
+ if kw.arg == "route" and kind == "Page":
221
+ route_val = literal_value(kw.value)
222
+ if isinstance(route_val, str):
223
+ self._page_route_by_node[nid] = route_val
224
+ continue
225
+ if kw.arg == "on_click":
226
+ if isinstance(kw.value, ast.Name):
227
+ attrs["on_click"] = {"__handler__": kw.value.id}
228
+ elif isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
229
+ attrs["on_click"] = {"__handler__": kw.value.value}
230
+ else:
231
+ attrs["on_click"] = literal_value(kw.value)
232
+ continue
233
+ if kw.arg in {"text", "title", "subtitle", "bind"}:
234
+ state_ref = self._maybe_state_ref(kw.value)
235
+ if state_ref:
236
+ if kw.arg == "bind":
237
+ attrs["text"] = state_ref
238
+ else:
239
+ attrs[kw.arg] = state_ref
240
+ continue
241
+ if isinstance(kw.value, ast.Call):
242
+ cid = self.build_call(kw.value, parent_id=nid, expansion_trail=trail)
243
+ if cid:
244
+ attrs[kw.arg] = {"__node__": cid}
245
+ child_ids.append(cid)
246
+ continue
247
+ attrs[kw.arg] = literal_value(kw.value)
248
+
249
+ provenance = ["parse:ui_call", "analyze:semantic_graph", *[f"expand:{n}" for n in trail]]
250
+ node = Node(
251
+ id=nid,
252
+ kind=kind,
253
+ span=span_for(self.path, call),
254
+ attributes=attrs,
255
+ children=child_ids,
256
+ provenance=provenance,
257
+ metadata={"expanded_from": list(trail)} if trail else {},
258
+ ).with_hash()
259
+ self.graph.nodes[nid] = node
260
+
261
+ if parent_id:
262
+ self.graph.edges.append({"from": parent_id, "to": nid, "kind": "contains"})
263
+ else:
264
+ self.graph.roots.append(nid)
265
+ if kind == "Page" and nid in self._page_route_by_node:
266
+ route_path = self._page_route_by_node[nid]
267
+ if route_path in self.graph.routes:
268
+ raise ValueError(f"Duplicate route: {route_path!r}")
269
+ self.graph.routes[route_path] = nid
270
+
271
+ for key, value in attrs.items():
272
+ if key in THEME_ATTR_KEYS and isinstance(value, str):
273
+ tid = self.theme_node(value, call)
274
+ self.dep.edges.append({"from": nid, "to": tid, "kind": "uses_theme"})
275
+ self.graph.edges.append({"from": nid, "to": tid, "kind": "uses_theme"})
276
+ if key == "on_click" and isinstance(value, dict) and "__handler__" in value:
277
+ handler = value["__handler__"]
278
+ self.dep.edges.append({"from": nid, "to": handler, "kind": "on_click"})
279
+ self.graph.edges.append({"from": nid, "to": handler, "kind": "calls_handler"})
280
+ if isinstance(value, dict) and "__state__" in value:
281
+ state_name = value["__state__"]
282
+ self.dep.edges.append({"from": nid, "to": state_name, "kind": "reads_state"})
283
+ self.graph.edges.append({"from": nid, "to": state_name, "kind": "reads_state"})
284
+
285
+ return nid
286
+
287
+ def visit_module(self, module: ast.Module) -> None:
288
+ for stmt in module.body:
289
+ if isinstance(stmt, ast.FunctionDef):
290
+ if any(_is_server_decorator(d) for d in stmt.decorator_list):
291
+ self.register_handler(stmt.name, kind="server", at=stmt)
292
+ elif stmt.name not in self.components:
293
+ self.register_handler(stmt.name, kind="client", at=stmt)
294
+ elif isinstance(stmt, ast.Assign):
295
+ if isinstance(stmt.value, ast.Call) and _is_state_call(stmt.value):
296
+ for target in stmt.targets:
297
+ if isinstance(target, ast.Name):
298
+ self.register_state(target.id, stmt.value)
299
+ elif isinstance(stmt.value, ast.Call) and _is_theme_call(stmt.value):
300
+ self.register_theme(stmt.value)
301
+ elif isinstance(stmt.value, ast.Call):
302
+ self.build_call(stmt.value)
303
+ elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
304
+ if _is_theme_call(stmt.value):
305
+ self.register_theme(stmt.value)
306
+ else:
307
+ self.build_call(stmt.value)
308
+ self._finalize_routes()
309
+
310
+ def _finalize_routes(self) -> None:
311
+ page_roots = [nid for nid in self.graph.roots if self.graph.nodes[nid].kind == "Page"]
312
+ unrouted = [nid for nid in page_roots if nid not in self._page_route_by_node]
313
+ if len(page_roots) == 1 and not self.graph.routes:
314
+ self.graph.routes["/"] = page_roots[0]
315
+ elif unrouted:
316
+ raise ValueError(
317
+ "Multiple ui.Page definitions require route= on each page "
318
+ f"(missing route on {len(unrouted)} page(s))"
319
+ )
320
+
321
+
322
+ def _display_path(path: Path) -> str:
323
+ path = path.resolve()
324
+ try:
325
+ return path.relative_to(Path.cwd().resolve()).as_posix()
326
+ except ValueError:
327
+ return path.as_posix()
328
+
329
+
330
+ def build_semantic_graph(path: str | Path) -> tuple[SemanticGraph, DependencyGraph]:
331
+ path = Path(path)
332
+ module = parse_file(path)
333
+ components = collect_components(module)
334
+ builder = _GraphBuilder(_display_path(path), components)
335
+ builder.visit_module(module)
336
+ builder.graph.roots = sorted(set(builder.graph.roots))
337
+ return builder.graph, builder.dep
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import copy
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class ComponentDef:
11
+ name: str
12
+ params: list[str]
13
+ template: ast.Call
14
+ style: str # "function" | "class"
15
+ span_line: int = 0
16
+
17
+
18
+ def _is_ui_call(call: ast.Call) -> bool:
19
+ func = call.func
20
+ if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
21
+ return func.value.id == "ui"
22
+ return False
23
+
24
+
25
+ def _inherits_component(bases: list[ast.expr]) -> bool:
26
+ for base in bases:
27
+ if isinstance(base, ast.Name) and base.id == "Component":
28
+ return True
29
+ if isinstance(base, ast.Attribute) and base.attr == "Component":
30
+ return True
31
+ return False
32
+
33
+
34
+ def _return_call(fn: ast.FunctionDef) -> ast.Call | None:
35
+ body = [s for s in fn.body if not isinstance(s, (ast.Pass, ast.Expr))]
36
+ # allow docstring Expr as first
37
+ stmts = list(fn.body)
38
+ if stmts and isinstance(stmts[0], ast.Expr) and isinstance(stmts[0].value, ast.Constant):
39
+ stmts = stmts[1:]
40
+ if len(stmts) != 1 or not isinstance(stmts[0], ast.Return):
41
+ return None
42
+ value = stmts[0].value
43
+ if isinstance(value, ast.Call):
44
+ return value
45
+ return None
46
+
47
+
48
+ def collect_components(module: ast.Module) -> dict[str, ComponentDef]:
49
+ components: dict[str, ComponentDef] = {}
50
+
51
+ for stmt in module.body:
52
+ if isinstance(stmt, ast.FunctionDef):
53
+ # skip @server handlers
54
+ if any(
55
+ (isinstance(d, ast.Name) and d.id == "server")
56
+ or (isinstance(d, ast.Attribute) and d.attr == "server")
57
+ for d in stmt.decorator_list
58
+ ):
59
+ continue
60
+ ret = _return_call(stmt)
61
+ if ret is None:
62
+ continue
63
+ # Must be ui.* or another component name (resolved later)
64
+ params = [a.arg for a in stmt.args.args]
65
+ components[stmt.name] = ComponentDef(
66
+ name=stmt.name,
67
+ params=params,
68
+ template=ret,
69
+ style="function",
70
+ span_line=stmt.lineno,
71
+ )
72
+ elif isinstance(stmt, ast.ClassDef) and _inherits_component(stmt.bases):
73
+ init_params: list[str] = []
74
+ build_fn: ast.FunctionDef | None = None
75
+ for item in stmt.body:
76
+ if isinstance(item, ast.FunctionDef) and item.name == "__init__":
77
+ # skip self
78
+ init_params = [a.arg for a in item.args.args[1:]]
79
+ if isinstance(item, ast.FunctionDef) and item.name == "build":
80
+ build_fn = item
81
+ if build_fn is None:
82
+ continue
83
+ ret = _return_call(build_fn)
84
+ if ret is None:
85
+ continue
86
+ # Prefer __init__ params; else build params without self
87
+ params = init_params or [a.arg for a in build_fn.args.args[1:]]
88
+ components[stmt.name] = ComponentDef(
89
+ name=stmt.name,
90
+ params=params,
91
+ template=ret,
92
+ style="class",
93
+ span_line=stmt.lineno,
94
+ )
95
+
96
+ return components
97
+
98
+
99
+ class _Subst(ast.NodeTransformer):
100
+ def __init__(self, mapping: dict[str, ast.AST], *, class_style: bool) -> None:
101
+ self.mapping = mapping
102
+ self.class_style = class_style
103
+
104
+ def visit_Name(self, node: ast.Name) -> ast.AST:
105
+ if node.id in self.mapping:
106
+ return copy.deepcopy(self.mapping[node.id])
107
+ return node
108
+
109
+ def visit_Attribute(self, node: ast.Attribute) -> ast.AST:
110
+ self.generic_visit(node)
111
+ if (
112
+ self.class_style
113
+ and isinstance(node.value, ast.Name)
114
+ and node.value.id == "self"
115
+ and node.attr in self.mapping
116
+ ):
117
+ return copy.deepcopy(self.mapping[node.attr])
118
+ return node
119
+
120
+
121
+ def bind_call_args(call: ast.Call, params: list[str]) -> dict[str, ast.AST]:
122
+ mapping: dict[str, ast.AST] = {}
123
+ for i, arg in enumerate(call.args):
124
+ if i < len(params):
125
+ mapping[params[i]] = arg
126
+ for kw in call.keywords:
127
+ if kw.arg and kw.arg in params:
128
+ mapping[kw.arg] = kw.value
129
+ return mapping
130
+
131
+
132
+ def expand_component_call(
133
+ call: ast.Call,
134
+ components: dict[str, ComponentDef],
135
+ *,
136
+ depth: int = 0,
137
+ ) -> ast.Call | None:
138
+ """If call is a user component, return expanded ui.* Call (fully expanded)."""
139
+ if depth > 16:
140
+ raise RecursionError("component expansion exceeded depth limit")
141
+ if not isinstance(call.func, ast.Name):
142
+ return None
143
+ name = call.func.id
144
+ if name not in components:
145
+ return None
146
+ cdef = components[name]
147
+ mapping = bind_call_args(call, cdef.params)
148
+ template = copy.deepcopy(cdef.template)
149
+ substituted = _Subst(mapping, class_style=cdef.style == "class").visit(template)
150
+ ast.fix_missing_locations(substituted)
151
+ if not isinstance(substituted, ast.Call):
152
+ return None
153
+ # Recurse if still a component
154
+ while isinstance(substituted.func, ast.Name) and substituted.func.id in components:
155
+ nested = expand_component_call(substituted, components, depth=depth + 1)
156
+ if nested is None:
157
+ break
158
+ substituted = nested
159
+ return substituted
160
+
161
+
162
+ def component_call_name(call: ast.Call, components: dict[str, ComponentDef]) -> str | None:
163
+ if isinstance(call.func, ast.Name) and call.func.id in components:
164
+ return call.func.id
165
+ return None
ourui/cli.py ADDED
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from ourui.lsp.server import run_stdio_server
8
+ from ourui.pipeline import dump_json, emit_html
9
+ from ourui.runtime import serve
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> int:
13
+ parser = argparse.ArgumentParser(prog="ourui", description="OurUI compiler CLI")
14
+ sub = parser.add_subparsers(dest="command", required=True)
15
+
16
+ dump_p = sub.add_parser(
17
+ "dump",
18
+ help="Dump Semantic Graph, Dependency Graph, IIR, LTR, and RTR as JSON",
19
+ )
20
+ dump_p.add_argument("source", type=Path, help="Path to a Python OurUI module")
21
+ dump_p.add_argument("-o", "--output", type=Path, default=None, help="Write JSON to file instead of stdout")
22
+
23
+ emit_p = sub.add_parser(
24
+ "emit",
25
+ help="Emit HTML from HostNode RTR (does not consume Python AST in emitter)",
26
+ )
27
+ emit_p.add_argument("source", type=Path, help="Path to a Python OurUI module")
28
+ emit_p.add_argument("-o", "--output", type=Path, default=None, help="Write HTML to file instead of stdout")
29
+ emit_p.add_argument("--title", default=None, help="HTML document title")
30
+
31
+ serve_p = sub.add_parser(
32
+ "serve",
33
+ help="Serve app: GET / emits HTML; POST /__ourui/call/<handler> runs @server fns",
34
+ )
35
+ serve_p.add_argument("source", type=Path, help="Path to a Python OurUI module")
36
+ serve_p.add_argument("--host", default="127.0.0.1")
37
+ serve_p.add_argument("--port", type=int, default=8765)
38
+ serve_p.add_argument("--title", default=None)
39
+ serve_p.add_argument(
40
+ "--prod",
41
+ action="store_true",
42
+ help="Production mode: no HMR, session State, safe errors, /__ourui/health",
43
+ )
44
+ serve_p.add_argument(
45
+ "--workers",
46
+ type=int,
47
+ default=1,
48
+ help="Worker processes (requires --prod; uses file session store when >1)",
49
+ )
50
+ serve_p.add_argument(
51
+ "--session-dir",
52
+ type=Path,
53
+ default=None,
54
+ help="Directory for file-backed sessions (or set OURUI_SESSION_DIR)",
55
+ )
56
+
57
+ sub.add_parser(
58
+ "lsp",
59
+ help="Start the OurUI Language Server (stdio JSON-RPC for editors)",
60
+ )
61
+
62
+ args = parser.parse_args(argv)
63
+
64
+ if args.command == "dump":
65
+ if not args.source.exists():
66
+ print(f"error: file not found: {args.source}", file=sys.stderr)
67
+ return 1
68
+ text = dump_json(args.source)
69
+ if args.output:
70
+ args.output.write_text(text, encoding="utf-8")
71
+ else:
72
+ sys.stdout.write(text)
73
+ return 0
74
+
75
+ if args.command == "emit":
76
+ if not args.source.exists():
77
+ print(f"error: file not found: {args.source}", file=sys.stderr)
78
+ return 1
79
+ text = emit_html(args.source, title=args.title)
80
+ if args.output:
81
+ args.output.write_text(text, encoding="utf-8")
82
+ else:
83
+ sys.stdout.write(text)
84
+ return 0
85
+
86
+ if args.command == "serve":
87
+ if not args.source.exists():
88
+ print(f"error: file not found: {args.source}", file=sys.stderr)
89
+ return 1
90
+ serve(
91
+ args.source,
92
+ host=args.host,
93
+ port=args.port,
94
+ title=args.title,
95
+ prod=args.prod,
96
+ workers=args.workers,
97
+ session_dir=args.session_dir,
98
+ )
99
+ return 0
100
+
101
+ if args.command == "lsp":
102
+ run_stdio_server()
103
+ return 0
104
+
105
+ return 2
106
+
107
+
108
+ if __name__ == "__main__":
109
+ raise SystemExit(main())
ourui/emit/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from ourui.emit.html import emit_bundle, emit_css, emit_html_document
2
+ from ourui.emit.js import emit_js
3
+
4
+ __all__ = ["emit_bundle", "emit_css", "emit_html_document", "emit_js"]