evenis 0.2.0__tar.gz
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.
- evenis-0.2.0/PKG-INFO +86 -0
- evenis-0.2.0/README.md +67 -0
- evenis-0.2.0/evenis/__init__.py +151 -0
- evenis-0.2.0/evenis/__main__.py +4 -0
- evenis-0.2.0/evenis/cli.py +129 -0
- evenis-0.2.0/evenis/compiler/__init__.py +153 -0
- evenis-0.2.0/evenis/compiler/html.py +178 -0
- evenis-0.2.0/evenis/compiler/runtime.py +660 -0
- evenis-0.2.0/evenis/components/__init__.py +247 -0
- evenis-0.2.0/evenis/components/widgets.py +562 -0
- evenis-0.2.0/evenis/core/__init__.py +421 -0
- evenis-0.2.0/evenis/layout/__init__.py +283 -0
- evenis-0.2.0/evenis/router/__init__.py +36 -0
- evenis-0.2.0/evenis/server/__init__.py +239 -0
- evenis-0.2.0/evenis/server/runner.py +254 -0
- evenis-0.2.0/evenis/state/__init__.py +220 -0
- evenis-0.2.0/evenis/style/__init__.py +116 -0
- evenis-0.2.0/evenis/style/css.py +417 -0
- evenis-0.2.0/evenis.egg-info/PKG-INFO +86 -0
- evenis-0.2.0/evenis.egg-info/SOURCES.txt +24 -0
- evenis-0.2.0/evenis.egg-info/dependency_links.txt +1 -0
- evenis-0.2.0/evenis.egg-info/entry_points.txt +2 -0
- evenis-0.2.0/evenis.egg-info/requires.txt +6 -0
- evenis-0.2.0/evenis.egg-info/top_level.txt +1 -0
- evenis-0.2.0/pyproject.toml +42 -0
- evenis-0.2.0/setup.cfg +4 -0
evenis-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: evenis
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python-only fullstack UI framework: UI, состояние, анимации и бэкенд — только на Python.
|
|
5
|
+
Author: evenis
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://example.com/evenis
|
|
8
|
+
Keywords: ui,framework,fullstack,spa,virtual-dom,fastapi
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Provides-Extra: server
|
|
15
|
+
Requires-Dist: fastapi>=0.100; extra == "server"
|
|
16
|
+
Requires-Dist: uvicorn>=0.23; extra == "server"
|
|
17
|
+
Requires-Dist: websockets>=11; extra == "server"
|
|
18
|
+
Requires-Dist: pyjwt>=2.6; extra == "server"
|
|
19
|
+
|
|
20
|
+
# evenis
|
|
21
|
+
|
|
22
|
+
**Python-only fullstack UI framework.** Весь UI, состояние, анимации и бэкенд
|
|
23
|
+
описываются только на Python — ни строчки HTML/CSS/JS в пользовательском коде.
|
|
24
|
+
Компилятор выдаёт SPA с настоящим Virtual DOM, а бэкенд-функции автоматически
|
|
25
|
+
становятся API.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import evenis as ui
|
|
29
|
+
from evenis.server import server, action, LiveState
|
|
30
|
+
|
|
31
|
+
@server
|
|
32
|
+
async def get_users():
|
|
33
|
+
return await User.all() # → GET /__evenis/api/get_users
|
|
34
|
+
|
|
35
|
+
@action
|
|
36
|
+
async def add_user(name: str):
|
|
37
|
+
... # → POST /__evenis/api/add_user
|
|
38
|
+
return await User.all()
|
|
39
|
+
|
|
40
|
+
online = LiveState(0, name="online") # WebSocket-состояние
|
|
41
|
+
|
|
42
|
+
app = ui.App(title="App", state={"users": []})
|
|
43
|
+
|
|
44
|
+
@app.page("/", load=get_users.load(into="users"))
|
|
45
|
+
def home():
|
|
46
|
+
return ui.Column(
|
|
47
|
+
ui.Text("Онлайн: {{ online }}"),
|
|
48
|
+
ui.For("users", lambda u, i: ui.Text(u.attr("name"))),
|
|
49
|
+
ui.PrimaryButton("Добавить", on_click=add_user(into="users", name="{{ draft }}")),
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Установка
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install evenis # ядро (сборка статических SPA)
|
|
57
|
+
pip install "evenis[server]" # + fullstack (FastAPI/WebSocket)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Запуск
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python -m evenis build app.py # собрать статический сайт в dist/
|
|
64
|
+
python -m evenis dev app.py --port 8000 # dev-сервер + hot reload
|
|
65
|
+
python -m evenis serve app.py --port 8000 # fullstack: фронт + API + WebSocket
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Возможности
|
|
69
|
+
|
|
70
|
+
- **Только Python** — никакого HTML/CSS/JS в пользовательском коде.
|
|
71
|
+
- **Virtual DOM** — обновляется только изменившийся узел, фокус в полях сохраняется.
|
|
72
|
+
- **Компоненты и раскладка** — `Button`, `Card`, `Input`, `Row`, `Column`, `Grid`,
|
|
73
|
+
`AdaptiveLayout` (сам выбирает число колонок), `Motion`.
|
|
74
|
+
- **Состояние** — реактивный store, `computed`-сигналы, декларативные действия,
|
|
75
|
+
middleware-pipeline.
|
|
76
|
+
- **Анимации** — Motion, FLIP layout-переходы, настраиваемые кривые и длительности.
|
|
77
|
+
- **Fullstack** — `@server` / `@action` превращают функции в API, `LiveState` —
|
|
78
|
+
в WebSocket-состояние с push-обновлениями. Транспорт выбирается автоматически.
|
|
79
|
+
|
|
80
|
+
## Примеры
|
|
81
|
+
|
|
82
|
+
См. каталог `examples/`: `hello_world.py`, `dashboard.py`, `ecommerce.py`,
|
|
83
|
+
`demo_app.py`, `fullstack.py`, `docs_site.py` (эта документация, написанная на
|
|
84
|
+
самом evenis).
|
|
85
|
+
|
|
86
|
+
MIT License.
|
evenis-0.2.0/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# evenis
|
|
2
|
+
|
|
3
|
+
**Python-only fullstack UI framework.** Весь UI, состояние, анимации и бэкенд
|
|
4
|
+
описываются только на Python — ни строчки HTML/CSS/JS в пользовательском коде.
|
|
5
|
+
Компилятор выдаёт SPA с настоящим Virtual DOM, а бэкенд-функции автоматически
|
|
6
|
+
становятся API.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
import evenis as ui
|
|
10
|
+
from evenis.server import server, action, LiveState
|
|
11
|
+
|
|
12
|
+
@server
|
|
13
|
+
async def get_users():
|
|
14
|
+
return await User.all() # → GET /__evenis/api/get_users
|
|
15
|
+
|
|
16
|
+
@action
|
|
17
|
+
async def add_user(name: str):
|
|
18
|
+
... # → POST /__evenis/api/add_user
|
|
19
|
+
return await User.all()
|
|
20
|
+
|
|
21
|
+
online = LiveState(0, name="online") # WebSocket-состояние
|
|
22
|
+
|
|
23
|
+
app = ui.App(title="App", state={"users": []})
|
|
24
|
+
|
|
25
|
+
@app.page("/", load=get_users.load(into="users"))
|
|
26
|
+
def home():
|
|
27
|
+
return ui.Column(
|
|
28
|
+
ui.Text("Онлайн: {{ online }}"),
|
|
29
|
+
ui.For("users", lambda u, i: ui.Text(u.attr("name"))),
|
|
30
|
+
ui.PrimaryButton("Добавить", on_click=add_user(into="users", name="{{ draft }}")),
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Установка
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install evenis # ядро (сборка статических SPA)
|
|
38
|
+
pip install "evenis[server]" # + fullstack (FastAPI/WebSocket)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Запуск
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python -m evenis build app.py # собрать статический сайт в dist/
|
|
45
|
+
python -m evenis dev app.py --port 8000 # dev-сервер + hot reload
|
|
46
|
+
python -m evenis serve app.py --port 8000 # fullstack: фронт + API + WebSocket
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Возможности
|
|
50
|
+
|
|
51
|
+
- **Только Python** — никакого HTML/CSS/JS в пользовательском коде.
|
|
52
|
+
- **Virtual DOM** — обновляется только изменившийся узел, фокус в полях сохраняется.
|
|
53
|
+
- **Компоненты и раскладка** — `Button`, `Card`, `Input`, `Row`, `Column`, `Grid`,
|
|
54
|
+
`AdaptiveLayout` (сам выбирает число колонок), `Motion`.
|
|
55
|
+
- **Состояние** — реактивный store, `computed`-сигналы, декларативные действия,
|
|
56
|
+
middleware-pipeline.
|
|
57
|
+
- **Анимации** — Motion, FLIP layout-переходы, настраиваемые кривые и длительности.
|
|
58
|
+
- **Fullstack** — `@server` / `@action` превращают функции в API, `LiveState` —
|
|
59
|
+
в WebSocket-состояние с push-обновлениями. Транспорт выбирается автоматически.
|
|
60
|
+
|
|
61
|
+
## Примеры
|
|
62
|
+
|
|
63
|
+
См. каталог `examples/`: `hello_world.py`, `dashboard.py`, `ecommerce.py`,
|
|
64
|
+
`demo_app.py`, `fullstack.py`, `docs_site.py` (эта документация, написанная на
|
|
65
|
+
самом evenis).
|
|
66
|
+
|
|
67
|
+
MIT License.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""evenis — production-grade Python UI framework.
|
|
2
|
+
|
|
3
|
+
Двухслойная система:
|
|
4
|
+
• compile-time (Python): UI-дерево (AST) → layout → theme→CSS → HTML → JS inject;
|
|
5
|
+
• runtime (browser JS): SPA-роутинг, реактивный store, DOM-обновления, анимации.
|
|
6
|
+
|
|
7
|
+
Пользователь описывает UI ТОЛЬКО на Python — без HTML/CSS/JS.
|
|
8
|
+
|
|
9
|
+
Пример:
|
|
10
|
+
import evenis as ui
|
|
11
|
+
|
|
12
|
+
app = ui.App(title="Demo", state={"count": 0})
|
|
13
|
+
|
|
14
|
+
@app.page("/", title="Home")
|
|
15
|
+
def home():
|
|
16
|
+
return ui.Column(
|
|
17
|
+
ui.Heading("Счётчик"),
|
|
18
|
+
ui.Text("Значение: {{ count }}"),
|
|
19
|
+
ui.Button("+1", on_click=ui.increment("count"), variant="primary"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
app.build("dist")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from .core import App, Component, Element, For, Fragment, If, Node, Page, to_vnode
|
|
26
|
+
from .layout import (
|
|
27
|
+
AdaptiveLayout,
|
|
28
|
+
Column,
|
|
29
|
+
Container,
|
|
30
|
+
Grid,
|
|
31
|
+
Motion,
|
|
32
|
+
Responsive,
|
|
33
|
+
Row,
|
|
34
|
+
Scrollable,
|
|
35
|
+
Spacer,
|
|
36
|
+
Stack,
|
|
37
|
+
)
|
|
38
|
+
from .components import (
|
|
39
|
+
Avatar,
|
|
40
|
+
Badge,
|
|
41
|
+
Breadcrumb,
|
|
42
|
+
Button,
|
|
43
|
+
Card,
|
|
44
|
+
Chart,
|
|
45
|
+
Checkbox,
|
|
46
|
+
CodeBlock,
|
|
47
|
+
DangerButton,
|
|
48
|
+
DatePicker,
|
|
49
|
+
Divider,
|
|
50
|
+
Drawer,
|
|
51
|
+
Field,
|
|
52
|
+
FileUpload,
|
|
53
|
+
Form,
|
|
54
|
+
GhostButton,
|
|
55
|
+
Heading,
|
|
56
|
+
Html,
|
|
57
|
+
Image,
|
|
58
|
+
Input,
|
|
59
|
+
Link,
|
|
60
|
+
Markdown,
|
|
61
|
+
Modal,
|
|
62
|
+
Pagination,
|
|
63
|
+
PrimaryButton,
|
|
64
|
+
Progress,
|
|
65
|
+
Select,
|
|
66
|
+
Skeleton,
|
|
67
|
+
Slider,
|
|
68
|
+
Spinner,
|
|
69
|
+
Switch,
|
|
70
|
+
Table,
|
|
71
|
+
Tabs,
|
|
72
|
+
Text,
|
|
73
|
+
Textarea,
|
|
74
|
+
Tooltip,
|
|
75
|
+
)
|
|
76
|
+
from .style import Theme, default_theme, light_theme
|
|
77
|
+
from .state import (
|
|
78
|
+
append,
|
|
79
|
+
batch,
|
|
80
|
+
batch_if,
|
|
81
|
+
clipboard,
|
|
82
|
+
decrement,
|
|
83
|
+
debounce,
|
|
84
|
+
download_pdf,
|
|
85
|
+
emit,
|
|
86
|
+
fetch_json,
|
|
87
|
+
geolocate,
|
|
88
|
+
increment,
|
|
89
|
+
js,
|
|
90
|
+
log,
|
|
91
|
+
navigate,
|
|
92
|
+
notify,
|
|
93
|
+
prefetch,
|
|
94
|
+
redirect_if,
|
|
95
|
+
remove_at,
|
|
96
|
+
scroll_to,
|
|
97
|
+
set_from_event,
|
|
98
|
+
set_theme,
|
|
99
|
+
set_value,
|
|
100
|
+
shortcut,
|
|
101
|
+
sound,
|
|
102
|
+
throttle,
|
|
103
|
+
toast,
|
|
104
|
+
toggle,
|
|
105
|
+
toggle_theme,
|
|
106
|
+
vibrate,
|
|
107
|
+
)
|
|
108
|
+
from .router import Router
|
|
109
|
+
from .compiler import build_app, build_manifest
|
|
110
|
+
from .server import (
|
|
111
|
+
LiveState,
|
|
112
|
+
action,
|
|
113
|
+
enable_auth,
|
|
114
|
+
protected,
|
|
115
|
+
serve,
|
|
116
|
+
server,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
__version__ = "0.1.0"
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
# core
|
|
123
|
+
"App", "Page", "Component", "Element", "Node", "Fragment", "If", "For", "to_vnode",
|
|
124
|
+
# layout
|
|
125
|
+
"Row", "Column", "Grid", "Stack", "Container", "Spacer", "Responsive",
|
|
126
|
+
"AdaptiveLayout", "Scrollable", "Motion",
|
|
127
|
+
# components
|
|
128
|
+
"Text", "Heading", "Button", "PrimaryButton", "DangerButton", "GhostButton",
|
|
129
|
+
"Card", "Input", "Link", "Image", "Divider",
|
|
130
|
+
# widgets
|
|
131
|
+
"Textarea", "Select", "Switch", "Checkbox", "Slider", "DatePicker", "FileUpload",
|
|
132
|
+
"Field", "Form", "Badge", "Spinner", "Progress", "Skeleton", "Avatar",
|
|
133
|
+
"Tooltip", "Breadcrumb", "CodeBlock", "Markdown", "Html", "Table",
|
|
134
|
+
"Tabs", "Modal", "Drawer", "Pagination", "Chart",
|
|
135
|
+
# style
|
|
136
|
+
"Theme", "default_theme", "light_theme",
|
|
137
|
+
# state / actions
|
|
138
|
+
"set_value", "increment", "decrement", "toggle", "set_from_event",
|
|
139
|
+
"append", "remove_at", "navigate", "js", "fetch_json", "batch", "batch_if",
|
|
140
|
+
# ui side-effects
|
|
141
|
+
"toast", "notify", "clipboard", "sound", "vibrate", "geolocate",
|
|
142
|
+
"set_theme", "toggle_theme", "scroll_to", "emit", "download_pdf",
|
|
143
|
+
"debounce", "throttle", "shortcut",
|
|
144
|
+
# middleware
|
|
145
|
+
"redirect_if", "log", "prefetch",
|
|
146
|
+
# router / compiler
|
|
147
|
+
"Router", "build_app", "build_manifest",
|
|
148
|
+
# server (fullstack)
|
|
149
|
+
"server", "action", "LiveState", "protected", "enable_auth", "serve",
|
|
150
|
+
"__version__",
|
|
151
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""evenis.cli — одна команда для сборки и dev-режима.
|
|
2
|
+
|
|
3
|
+
evenis build app.py [--out dist] — собрать сайт в каталог.
|
|
4
|
+
evenis dev app.py [--out dist] [--port 8000]
|
|
5
|
+
— собрать с dev=True (hot reload), поднять статический сервер и
|
|
6
|
+
пересобирать при изменении исходников (polling watcher).
|
|
7
|
+
|
|
8
|
+
`app.py` должен экспортировать переменную `app` (экземпляр evenis.App) либо
|
|
9
|
+
функцию `app()`/`create_app()`, возвращающую App.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import importlib.util
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_app(path: str):
|
|
22
|
+
spec = importlib.util.spec_from_file_location("_evenis_user_app", path)
|
|
23
|
+
if spec is None or spec.loader is None:
|
|
24
|
+
raise SystemExit(f"Не удалось загрузить модуль: {path}")
|
|
25
|
+
mod = importlib.util.module_from_spec(spec)
|
|
26
|
+
spec.loader.exec_module(mod)
|
|
27
|
+
candidate = getattr(mod, "app", None) or getattr(mod, "create_app", None)
|
|
28
|
+
if candidate is None:
|
|
29
|
+
raise SystemExit("В модуле нет `app` (экземпляр App) или фабрики create_app().")
|
|
30
|
+
app = candidate() if callable(candidate) else candidate
|
|
31
|
+
return app
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _snapshot(root: str) -> float:
|
|
35
|
+
"""Максимальное mtime по дереву *.py — простой watcher без зависимостей."""
|
|
36
|
+
latest = 0.0
|
|
37
|
+
for dirpath, _dirs, files in os.walk(root):
|
|
38
|
+
if "__pycache__" in dirpath or "/dist" in dirpath:
|
|
39
|
+
continue
|
|
40
|
+
for f in files:
|
|
41
|
+
if f.endswith(".py"):
|
|
42
|
+
try:
|
|
43
|
+
latest = max(latest, os.path.getmtime(os.path.join(dirpath, f)))
|
|
44
|
+
except OSError:
|
|
45
|
+
pass
|
|
46
|
+
return latest
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_build(args) -> None:
|
|
50
|
+
app = _load_app(args.entry)
|
|
51
|
+
out = app.build(args.out)
|
|
52
|
+
print(f"evenis: собрано → {out}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cmd_dev(args) -> None:
|
|
56
|
+
import functools
|
|
57
|
+
import http.server
|
|
58
|
+
import socketserver
|
|
59
|
+
import threading
|
|
60
|
+
|
|
61
|
+
entry = os.path.abspath(args.entry)
|
|
62
|
+
watch_root = os.path.dirname(entry)
|
|
63
|
+
|
|
64
|
+
def rebuild():
|
|
65
|
+
app = _load_app(entry)
|
|
66
|
+
app.dev = True
|
|
67
|
+
app.build(args.out)
|
|
68
|
+
|
|
69
|
+
rebuild()
|
|
70
|
+
out_abs = os.path.abspath(args.out)
|
|
71
|
+
print(f"evenis: dev-сборка → {out_abs}")
|
|
72
|
+
|
|
73
|
+
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=out_abs)
|
|
74
|
+
httpd = socketserver.TCPServer(("", args.port), handler)
|
|
75
|
+
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
|
76
|
+
print(f"evenis: http://localhost:{args.port} (Ctrl+C для выхода)")
|
|
77
|
+
|
|
78
|
+
last = _snapshot(watch_root)
|
|
79
|
+
try:
|
|
80
|
+
while True:
|
|
81
|
+
time.sleep(0.6)
|
|
82
|
+
cur = _snapshot(watch_root)
|
|
83
|
+
if cur > last:
|
|
84
|
+
last = cur
|
|
85
|
+
try:
|
|
86
|
+
rebuild()
|
|
87
|
+
print("evenis: пересобрано, обновляю браузер…")
|
|
88
|
+
except Exception as e: # noqa: BLE001 - dev-цикл не должен падать
|
|
89
|
+
print(f"evenis: ошибка сборки: {e}", file=sys.stderr)
|
|
90
|
+
except KeyboardInterrupt:
|
|
91
|
+
print("\nevenis: остановлено.")
|
|
92
|
+
httpd.shutdown()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_serve(args) -> None:
|
|
96
|
+
from .server import serve
|
|
97
|
+
|
|
98
|
+
app = _load_app(args.entry)
|
|
99
|
+
serve(app, outdir=args.out, port=args.port, host=args.host)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv=None) -> None:
|
|
103
|
+
parser = argparse.ArgumentParser(prog="evenis", description="evenis — Python UI framework")
|
|
104
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
105
|
+
|
|
106
|
+
b = sub.add_parser("build", help="собрать сайт в каталог")
|
|
107
|
+
b.add_argument("entry", help="путь к app.py (экспортирует app)")
|
|
108
|
+
b.add_argument("--out", default="dist", help="каталог вывода (по умолчанию dist)")
|
|
109
|
+
b.set_defaults(func=cmd_build)
|
|
110
|
+
|
|
111
|
+
d = sub.add_parser("dev", help="dev-сервер с hot reload")
|
|
112
|
+
d.add_argument("entry", help="путь к app.py (экспортирует app)")
|
|
113
|
+
d.add_argument("--out", default="dist", help="каталог вывода")
|
|
114
|
+
d.add_argument("--port", type=int, default=8000, help="порт (по умолчанию 8000)")
|
|
115
|
+
d.set_defaults(func=cmd_dev)
|
|
116
|
+
|
|
117
|
+
s = sub.add_parser("serve", help="fullstack-сервер (фронт + FastAPI API + WebSocket)")
|
|
118
|
+
s.add_argument("entry", help="путь к app.py (экспортирует app)")
|
|
119
|
+
s.add_argument("--out", default="dist", help="каталог вывода")
|
|
120
|
+
s.add_argument("--port", type=int, default=8000, help="желаемый порт (свободный подберётся рядом)")
|
|
121
|
+
s.add_argument("--host", default="127.0.0.1", help="хост (по умолчанию 127.0.0.1)")
|
|
122
|
+
s.set_defaults(func=cmd_serve)
|
|
123
|
+
|
|
124
|
+
args = parser.parse_args(argv)
|
|
125
|
+
args.func(args)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""evenis.compiler — конвейер компиляции (compile-time слой).
|
|
2
|
+
|
|
3
|
+
Этапы:
|
|
4
|
+
1. AST: собрать VNode-дерево каждой страницы (App.pages → manifest маршрутов).
|
|
5
|
+
2. layout resolve: уже выполнен при построении VNode (Row/Column/... → классы).
|
|
6
|
+
3. CSS generator: theme → style.css.
|
|
7
|
+
4. HTML generator: SSR начального маршрута → index.html (первый кадр).
|
|
8
|
+
5. JS runtime injector: runtime + манифест приложения → app.js.
|
|
9
|
+
|
|
10
|
+
Результат пишется в каталог `outdir` (по умолчанию dist/):
|
|
11
|
+
dist/index.html, dist/style.css, dist/app.js
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from ..router import build_routes
|
|
21
|
+
from ..style.css import generate_css
|
|
22
|
+
from .html import render_page_html
|
|
23
|
+
from .runtime import RUNTIME_JS
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
26
|
+
from ..core import App
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_manifest(app: "App") -> dict:
|
|
30
|
+
"""Собрать JSON-манифест приложения для рантайма."""
|
|
31
|
+
routes = build_routes(app.pages)
|
|
32
|
+
if not routes:
|
|
33
|
+
raise ValueError("В приложении нет страниц: добавьте хотя бы одну Page")
|
|
34
|
+
fallback = app.pages[0].path
|
|
35
|
+
computed = [{"name": k, "expr": v} for k, v in app.computed.items()]
|
|
36
|
+
manifest = {
|
|
37
|
+
"title": app.title,
|
|
38
|
+
"mount": "app",
|
|
39
|
+
"state": app.state,
|
|
40
|
+
"routes": routes,
|
|
41
|
+
"fallback": fallback,
|
|
42
|
+
"breakpoint": app.theme.breakpoint,
|
|
43
|
+
"computed": computed,
|
|
44
|
+
"middleware": app.middleware,
|
|
45
|
+
"features": app.features,
|
|
46
|
+
"dev": app.dev,
|
|
47
|
+
"theme": getattr(app, "theme_mode", "dark"),
|
|
48
|
+
}
|
|
49
|
+
if getattr(app, "persist", None):
|
|
50
|
+
manifest["persist"] = app.persist
|
|
51
|
+
if getattr(app, "shortcuts", None):
|
|
52
|
+
manifest["shortcuts"] = app.shortcuts
|
|
53
|
+
if getattr(app, "i18n", None):
|
|
54
|
+
manifest["i18n"] = app.i18n
|
|
55
|
+
if getattr(app, "on_error", None):
|
|
56
|
+
oe = app.on_error
|
|
57
|
+
manifest["onError"] = oe if isinstance(oe, list) else [oe]
|
|
58
|
+
# Серверный слой (если использовались @server/@action/LiveState).
|
|
59
|
+
try:
|
|
60
|
+
from ..server import REGISTRY, server_manifest
|
|
61
|
+
|
|
62
|
+
if REGISTRY.endpoints or REGISTRY.livestates:
|
|
63
|
+
sm = server_manifest()
|
|
64
|
+
manifest["server"] = sm
|
|
65
|
+
# начальные значения LiveState кладём прямо в state, чтобы первый кадр
|
|
66
|
+
# уже показывал актуальные данные ещё до WebSocket-подключения.
|
|
67
|
+
for k, v in sm.get("live", {}).items():
|
|
68
|
+
manifest["state"].setdefault(k, v)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
# Загрузка данных при входе на страницу (Page.load → server endpoint).
|
|
72
|
+
for path, route in routes.items():
|
|
73
|
+
page = next((p for p in app.pages if p.path == path), None)
|
|
74
|
+
if page is not None and getattr(page, "load", None):
|
|
75
|
+
route["load"] = page.load
|
|
76
|
+
return manifest
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_HOT_RELOAD = """
|
|
80
|
+
<script>
|
|
81
|
+
(function(){
|
|
82
|
+
var cur=null;
|
|
83
|
+
setInterval(function(){
|
|
84
|
+
fetch('__evenis_buildid?ts='+Date.now()).then(function(r){return r.text();}).then(function(id){
|
|
85
|
+
if(cur===null){cur=id;return;} if(id!==cur){location.reload();}
|
|
86
|
+
}).catch(function(){});
|
|
87
|
+
}, 800);
|
|
88
|
+
})();
|
|
89
|
+
</script>"""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _index_html(app: "App", manifest: dict) -> str:
|
|
93
|
+
initial_path = manifest["fallback"]
|
|
94
|
+
initial_page = manifest["routes"][initial_path]
|
|
95
|
+
# SSR-scope: state + i18n t() (если включён), чтобы первый кадр был локализован
|
|
96
|
+
scope = dict(app.state)
|
|
97
|
+
i18n = getattr(app, "i18n", None)
|
|
98
|
+
if i18n:
|
|
99
|
+
lang = i18n.get("lang", "en")
|
|
100
|
+
messages = i18n.get("messages", {}).get(lang, {})
|
|
101
|
+
scope["t"] = lambda k: messages.get(k, k)
|
|
102
|
+
ssr = render_page_html(initial_page, scope)
|
|
103
|
+
title = manifest["routes"][initial_path].get("title", app.title)
|
|
104
|
+
theme = manifest.get("theme", "dark")
|
|
105
|
+
lang_attr = (i18n or {}).get("lang", "ru")
|
|
106
|
+
hot = _HOT_RELOAD if app.dev else ""
|
|
107
|
+
return f"""<!doctype html>
|
|
108
|
+
<html lang="{lang_attr}" data-theme="{theme}">
|
|
109
|
+
<head>
|
|
110
|
+
<meta charset="utf-8" />
|
|
111
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
112
|
+
<title>{title} · {app.title}</title>
|
|
113
|
+
<link rel="stylesheet" href="style.css" />
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<div id="app">{ssr}</div>
|
|
117
|
+
<script src="app.js"></script>{hot}
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _app_js(manifest: dict) -> str:
|
|
124
|
+
data = json.dumps(manifest, ensure_ascii=False)
|
|
125
|
+
return RUNTIME_JS + "\n;window.EVENIS.start(" + data + ");\n"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_app(app: "App", outdir: str = "dist") -> str:
|
|
129
|
+
"""Скомпилировать App в готовый сайт. Возвращает абсолютный путь к outdir."""
|
|
130
|
+
os.makedirs(outdir, exist_ok=True)
|
|
131
|
+
|
|
132
|
+
manifest = build_manifest(app)
|
|
133
|
+
|
|
134
|
+
css = generate_css(app.theme, features=app.features)
|
|
135
|
+
html_doc = _index_html(app, manifest)
|
|
136
|
+
js = _app_js(manifest)
|
|
137
|
+
|
|
138
|
+
with open(os.path.join(outdir, "style.css"), "w", encoding="utf-8") as f:
|
|
139
|
+
f.write(css)
|
|
140
|
+
with open(os.path.join(outdir, "index.html"), "w", encoding="utf-8") as f:
|
|
141
|
+
f.write(html_doc)
|
|
142
|
+
with open(os.path.join(outdir, "app.js"), "w", encoding="utf-8") as f:
|
|
143
|
+
f.write(js)
|
|
144
|
+
if app.dev:
|
|
145
|
+
import time
|
|
146
|
+
|
|
147
|
+
with open(os.path.join(outdir, "__evenis_buildid"), "w", encoding="utf-8") as f:
|
|
148
|
+
f.write(str(time.time()))
|
|
149
|
+
|
|
150
|
+
return os.path.abspath(outdir)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
__all__ = ["build_app", "build_manifest"]
|