tw-framework 0.2.3__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.
- tw_framework/__init__.py +13 -0
- tw_framework/advanced_diagnostics.py +25 -0
- tw_framework/asset_optimizer.py +30 -0
- tw_framework/ast_nodes.py +283 -0
- tw_framework/build.py +7 -0
- tw_framework/build_performance.py +26 -0
- tw_framework/build_report.py +84 -0
- tw_framework/cli.py +1258 -0
- tw_framework/code_splitting.py +76 -0
- tw_framework/common.py +22 -0
- tw_framework/compiler.py +4817 -0
- tw_framework/compiler_stats.py +51 -0
- tw_framework/dead_code.py +44 -0
- tw_framework/dependency_graph.py +91 -0
- tw_framework/deploy.py +7 -0
- tw_framework/dev.py +9 -0
- tw_framework/diagnostics.py +98 -0
- tw_framework/dynamic_imports.py +51 -0
- tw_framework/error_formatter.py +53 -0
- tw_framework/framework.py +3216 -0
- tw_framework/hydration.py +129 -0
- tw_framework/incremental_cache.py +53 -0
- tw_framework/interpreter.py +42 -0
- tw_framework/ir.py +171 -0
- tw_framework/lexer.py +52 -0
- tw_framework/lowering.py +68 -0
- tw_framework/parser.py +167 -0
- tw_framework/partial_rebuild.py +24 -0
- tw_framework/performance_analyzer.py +37 -0
- tw_framework/plugin_runtime.py +196 -0
- tw_framework/production_optimizer.py +82 -0
- tw_framework/reactivity.py +349 -0
- tw_framework/render_css.py +22 -0
- tw_framework/render_html.py +259 -0
- tw_framework/route_optimizer.py +40 -0
- tw_framework/router.py +7 -0
- tw_framework/runtime_model.py +23 -0
- tw_framework/runtime_values.py +31 -0
- tw_framework/semantic.py +162 -0
- tw_framework/server.py +532 -0
- tw_framework/signature.py +51 -0
- tw_framework/static_dynamic_auto.py +32 -0
- tw_framework/streaming.py +135 -0
- tw_framework/test_build_cli_pytest.py +1078 -0
- tw_framework/tree_shaking.py +48 -0
- tw_framework/twm_parser.py +274 -0
- tw_framework-0.2.3.dist-info/METADATA +8 -0
- tw_framework-0.2.3.dist-info/RECORD +51 -0
- tw_framework-0.2.3.dist-info/WHEEL +5 -0
- tw_framework-0.2.3.dist-info/entry_points.txt +2 -0
- tw_framework-0.2.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Partial hydration / islands architecture for TW Framework.
|
|
3
|
+
|
|
4
|
+
Interactive components are wrapped with data attributes and a small runtime
|
|
5
|
+
that hydrates only those components, leaving static HTML untouched.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import html
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from .ir import IRComponent, IRElement, IRProgram
|
|
14
|
+
from .runtime_values import RuntimeEnvironment
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_interactive(node) -> bool:
|
|
20
|
+
"""Check if a node has interactive attributes (events, router, or lazy component)."""
|
|
21
|
+
if isinstance(node, IRElement):
|
|
22
|
+
if node.events or node.router:
|
|
23
|
+
return True
|
|
24
|
+
for child in node.children:
|
|
25
|
+
if _is_interactive(child):
|
|
26
|
+
return True
|
|
27
|
+
if isinstance(node, IRComponent):
|
|
28
|
+
for prop in node.props:
|
|
29
|
+
if prop.get("name") == "lazy" and prop.get("value") in (True, "true"):
|
|
30
|
+
return True
|
|
31
|
+
for child in node.children:
|
|
32
|
+
if _is_interactive(child):
|
|
33
|
+
return True
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _collect_interactive_nodes(node, path: str, interactive: List[Dict]):
|
|
38
|
+
"""Recursively collect interactive nodes with their paths."""
|
|
39
|
+
if isinstance(node, IRElement):
|
|
40
|
+
if node.events or node.router:
|
|
41
|
+
interactive.append({
|
|
42
|
+
"path": path,
|
|
43
|
+
"tag": node.tag,
|
|
44
|
+
"events": [{"name": e["name"], "value": e["value"]} for e in node.events],
|
|
45
|
+
"router": dict(node.router),
|
|
46
|
+
})
|
|
47
|
+
for i, child in enumerate(node.children):
|
|
48
|
+
_collect_interactive_nodes(child, f"{path}.children[{i}]", interactive)
|
|
49
|
+
if isinstance(node, IRComponent):
|
|
50
|
+
for prop in node.props:
|
|
51
|
+
if prop.get("name") == "lazy" and prop.get("value") in (True, "true"):
|
|
52
|
+
interactive.append({
|
|
53
|
+
"path": path,
|
|
54
|
+
"tag": f"component:{node.name}",
|
|
55
|
+
"lazy": True,
|
|
56
|
+
})
|
|
57
|
+
for i, child in enumerate(node.children):
|
|
58
|
+
_collect_interactive_nodes(child, f"{path}.children[{i}]", interactive)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def wrap_interactive_nodes(html: str, program: IRProgram, context: Optional[Dict] = None) -> str:
|
|
62
|
+
"""Wrap interactive nodes with data-tw-hydrate attributes and inject hydration script."""
|
|
63
|
+
interactive = []
|
|
64
|
+
for i, node in enumerate(program.body):
|
|
65
|
+
_collect_interactive_nodes(node, f"body[{i}]", interactive)
|
|
66
|
+
if not interactive:
|
|
67
|
+
return html
|
|
68
|
+
|
|
69
|
+
# Inject data attributes
|
|
70
|
+
for item in interactive:
|
|
71
|
+
path = item["path"]
|
|
72
|
+
tag = item["tag"]
|
|
73
|
+
if item.get("lazy"):
|
|
74
|
+
html = html.replace(
|
|
75
|
+
f"<{tag}",
|
|
76
|
+
f'<{tag} data-tw-hydrate="lazy" data-tw-path="{html.escape(path, quote=True)}"',
|
|
77
|
+
1,
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
html = html.replace(
|
|
81
|
+
f"<{tag}",
|
|
82
|
+
f'<{tag} data-tw-hydrate="interactive" data-tw-path="{html.escape(path, quote=True)}"',
|
|
83
|
+
1,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Inject hydration runtime
|
|
87
|
+
runtime_js = """
|
|
88
|
+
(function() {
|
|
89
|
+
var hydratable = document.querySelectorAll('[data-tw-hydrate]');
|
|
90
|
+
if (!hydratable.length) return;
|
|
91
|
+
var observer = new IntersectionObserver(function(entries) {
|
|
92
|
+
entries.forEach(function(entry) {
|
|
93
|
+
if (entry.isIntersecting) {
|
|
94
|
+
var el = entry.target;
|
|
95
|
+
var type = el.getAttribute('data-tw-hydrate');
|
|
96
|
+
if (type === 'lazy') {
|
|
97
|
+
// Lazy component: load chunk and hydrate
|
|
98
|
+
var path = el.getAttribute('data-tw-path');
|
|
99
|
+
import('/_tw/chunks/' + path + '.js').then(function(mod) {
|
|
100
|
+
if (mod.hydrate) mod.hydrate(el);
|
|
101
|
+
}).catch(function(err) {
|
|
102
|
+
console.warn('TW hydration failed for', path, err);
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
// Interactive element: attach events
|
|
106
|
+
var events = JSON.parse(el.getAttribute('data-tw-events') || '[]');
|
|
107
|
+
events.forEach(function(ev) {
|
|
108
|
+
el.addEventListener(ev.name, function(event) {
|
|
109
|
+
try {
|
|
110
|
+
var fn = new Function('event', ev.value);
|
|
111
|
+
fn(event);
|
|
112
|
+
} catch(e) {
|
|
113
|
+
console.warn('TW event handler error', e);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
observer.unobserve(el);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}, { rootMargin: '200px' });
|
|
122
|
+
hydratable.forEach(function(el) { observer.observe(el); });
|
|
123
|
+
})();
|
|
124
|
+
"""
|
|
125
|
+
html = html.replace("</body>", f"<script>{runtime_js}</script></body>", 1)
|
|
126
|
+
return html
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
__all__ = ["wrap_interactive_nodes", "_is_interactive"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Incremental compiler cache for TW Framework.
|
|
3
|
+
|
|
4
|
+
Caches parsed AST, compiled output, dependency graph, CSS, and assets.
|
|
5
|
+
Only rebuilds changed nodes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IncrementalCache:
|
|
18
|
+
"""Persistent cache for compiler artifacts."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, project_root: str):
|
|
21
|
+
self.cache_dir = os.path.join(project_root, ".tw", "cache")
|
|
22
|
+
os.makedirs(self.cache_dir, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
def _key_path(self, key: str) -> str:
|
|
25
|
+
return os.path.join(self.cache_dir, f"{key}.json")
|
|
26
|
+
|
|
27
|
+
def get(self, key: str) -> Optional[Any]:
|
|
28
|
+
path = self._key_path(key)
|
|
29
|
+
if not os.path.exists(path):
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
33
|
+
return json.load(f)
|
|
34
|
+
except (json.JSONDecodeError, OSError):
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def set(self, key: str, value: Any):
|
|
38
|
+
path = self._key_path(key)
|
|
39
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
40
|
+
json.dump(value, f, indent=2)
|
|
41
|
+
|
|
42
|
+
def invalidate(self, key: str):
|
|
43
|
+
path = self._key_path(key)
|
|
44
|
+
if os.path.exists(path):
|
|
45
|
+
os.remove(path)
|
|
46
|
+
|
|
47
|
+
def clear(self):
|
|
48
|
+
for fname in os.listdir(self.cache_dir):
|
|
49
|
+
if fname.endswith(".json"):
|
|
50
|
+
os.remove(os.path.join(self.cache_dir, fname))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = ["IncrementalCache"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Dict, Optional
|
|
5
|
+
|
|
6
|
+
from .lowering import lower_program
|
|
7
|
+
from .parser import parse_file, parse_text
|
|
8
|
+
from .render_html import build_runtime_context, render_program_document
|
|
9
|
+
from .runtime_values import RuntimeEnvironment
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class InterpreterResult:
|
|
14
|
+
html: str
|
|
15
|
+
context: Dict
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Interpreter:
|
|
19
|
+
def run_program(self, program, context: Optional[Dict] = None, css_href: Optional[str] = None) -> InterpreterResult:
|
|
20
|
+
ir_program = lower_program(program)
|
|
21
|
+
runtime_context = build_runtime_context(program, context=context)
|
|
22
|
+
html = render_program_document(ir_program, page_program=program, context=runtime_context, css_href=css_href)
|
|
23
|
+
return InterpreterResult(html=html, context=runtime_context)
|
|
24
|
+
|
|
25
|
+
def run_text(self, text: str, *, base_dir: str = ".", file_path: str = "<memory>", context: Optional[Dict] = None, css_href: Optional[str] = None, debug: bool = False) -> InterpreterResult:
|
|
26
|
+
program = parse_text(text, base_dir=base_dir, file_path=file_path, debug=debug)
|
|
27
|
+
return self.run_program(program, context=context, css_href=css_href)
|
|
28
|
+
|
|
29
|
+
def run_file(self, path: str, context: Optional[Dict] = None, css_href: Optional[str] = None, debug: bool = False) -> InterpreterResult:
|
|
30
|
+
program = parse_file(path, debug=debug)
|
|
31
|
+
return self.run_program(program, context=context, css_href=css_href)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_file(path: str, context: Optional[Dict] = None, css_href: Optional[str] = None) -> InterpreterResult:
|
|
35
|
+
return Interpreter().run_file(path, context=context, css_href=css_href)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_runtime_environment(initial: Optional[Dict] = None) -> RuntimeEnvironment:
|
|
39
|
+
return RuntimeEnvironment(values=dict(initial or {}))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["Interpreter", "InterpreterResult", "build_runtime_environment", "run_file"]
|
tw_framework/ir.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class IRNode:
|
|
9
|
+
kind: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class IRText(IRNode):
|
|
14
|
+
value: str
|
|
15
|
+
|
|
16
|
+
def __init__(self, value: str):
|
|
17
|
+
super().__init__("text")
|
|
18
|
+
self.value = value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class IRLet(IRNode):
|
|
23
|
+
name: str
|
|
24
|
+
value: Any
|
|
25
|
+
|
|
26
|
+
def __init__(self, name: str, value: Any):
|
|
27
|
+
super().__init__("let")
|
|
28
|
+
self.name = name
|
|
29
|
+
self.value = value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class IRIf(IRNode):
|
|
34
|
+
condition: str
|
|
35
|
+
children: List[IRNode] = field(default_factory=list)
|
|
36
|
+
else_children: List[IRNode] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
def __init__(self, condition: str, children=None, else_children=None):
|
|
39
|
+
super().__init__("if")
|
|
40
|
+
self.condition = condition
|
|
41
|
+
self.children = children or []
|
|
42
|
+
self.else_children = else_children or []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class IRFor(IRNode):
|
|
47
|
+
var_name: str
|
|
48
|
+
iterable: str
|
|
49
|
+
children: List[IRNode] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
def __init__(self, var_name: str, iterable: str, children=None):
|
|
52
|
+
super().__init__("for")
|
|
53
|
+
self.var_name = var_name
|
|
54
|
+
self.iterable = iterable
|
|
55
|
+
self.children = children or []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class IRScript(IRNode):
|
|
60
|
+
raw_js: str
|
|
61
|
+
|
|
62
|
+
def __init__(self, raw_js: str):
|
|
63
|
+
super().__init__("script")
|
|
64
|
+
self.raw_js = raw_js
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class IRElement(IRNode):
|
|
69
|
+
tag: str
|
|
70
|
+
text: Optional[str] = None
|
|
71
|
+
attrs: List[Dict[str, Any]] = field(default_factory=list)
|
|
72
|
+
styles: List[Dict[str, Any]] = field(default_factory=list)
|
|
73
|
+
events: List[Dict[str, Any]] = field(default_factory=list)
|
|
74
|
+
router: Dict[str, Any] = field(default_factory=dict)
|
|
75
|
+
children: List[IRNode] = field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
def __init__(self, tag: str, text=None, attrs=None, styles=None, events=None, router=None, children=None):
|
|
78
|
+
super().__init__("element")
|
|
79
|
+
self.tag = tag
|
|
80
|
+
self.text = text
|
|
81
|
+
self.attrs = attrs or []
|
|
82
|
+
self.styles = styles or []
|
|
83
|
+
self.events = events or []
|
|
84
|
+
self.router = router or {}
|
|
85
|
+
self.children = children or []
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class IRComponent(IRNode):
|
|
90
|
+
name: str
|
|
91
|
+
props: List[Dict[str, Any]] = field(default_factory=list)
|
|
92
|
+
children: List[IRNode] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
def __init__(self, name: str, props=None, children=None):
|
|
95
|
+
super().__init__("component")
|
|
96
|
+
self.name = name
|
|
97
|
+
self.props = props or []
|
|
98
|
+
self.children = children or []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class IRProgram:
|
|
103
|
+
meta: Dict[str, Any]
|
|
104
|
+
head: Dict[str, Any]
|
|
105
|
+
lets: Dict[str, Any] = field(default_factory=dict)
|
|
106
|
+
state: Dict[str, Any] = field(default_factory=dict)
|
|
107
|
+
body: List[IRNode] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
def to_dict(self):
|
|
110
|
+
return {
|
|
111
|
+
"meta": dict(self.meta),
|
|
112
|
+
"head": dict(self.head),
|
|
113
|
+
"lets": dict(self.lets),
|
|
114
|
+
"state": dict(self.state),
|
|
115
|
+
"body": [ir_node_to_dict(node) for node in self.body],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def ir_node_to_dict(node: IRNode):
|
|
120
|
+
if isinstance(node, IRText):
|
|
121
|
+
return {"kind": node.kind, "value": node.value}
|
|
122
|
+
if isinstance(node, IRLet):
|
|
123
|
+
return {"kind": node.kind, "name": node.name, "value": node.value}
|
|
124
|
+
if isinstance(node, IRIf):
|
|
125
|
+
return {
|
|
126
|
+
"kind": node.kind,
|
|
127
|
+
"condition": node.condition,
|
|
128
|
+
"children": [ir_node_to_dict(child) for child in node.children],
|
|
129
|
+
"else_children": [ir_node_to_dict(child) for child in node.else_children],
|
|
130
|
+
}
|
|
131
|
+
if isinstance(node, IRFor):
|
|
132
|
+
return {
|
|
133
|
+
"kind": node.kind,
|
|
134
|
+
"var_name": node.var_name,
|
|
135
|
+
"iterable": node.iterable,
|
|
136
|
+
"children": [ir_node_to_dict(child) for child in node.children],
|
|
137
|
+
}
|
|
138
|
+
if isinstance(node, IRScript):
|
|
139
|
+
return {"kind": node.kind, "raw_js": node.raw_js}
|
|
140
|
+
if isinstance(node, IRElement):
|
|
141
|
+
return {
|
|
142
|
+
"kind": node.kind,
|
|
143
|
+
"tag": node.tag,
|
|
144
|
+
"text": node.text,
|
|
145
|
+
"attrs": list(node.attrs),
|
|
146
|
+
"styles": list(node.styles),
|
|
147
|
+
"events": list(node.events),
|
|
148
|
+
"router": dict(node.router),
|
|
149
|
+
"children": [ir_node_to_dict(child) for child in node.children],
|
|
150
|
+
}
|
|
151
|
+
if isinstance(node, IRComponent):
|
|
152
|
+
return {
|
|
153
|
+
"kind": node.kind,
|
|
154
|
+
"name": node.name,
|
|
155
|
+
"props": list(node.props),
|
|
156
|
+
"children": [ir_node_to_dict(child) for child in node.children],
|
|
157
|
+
}
|
|
158
|
+
return {"kind": getattr(node, "kind", "unknown"), "repr": repr(node)}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
__all__ = [
|
|
162
|
+
"IRComponent",
|
|
163
|
+
"IRElement",
|
|
164
|
+
"IRFor",
|
|
165
|
+
"IRIf",
|
|
166
|
+
"IRLet",
|
|
167
|
+
"IRProgram",
|
|
168
|
+
"IRScript",
|
|
169
|
+
"IRText",
|
|
170
|
+
"ir_node_to_dict",
|
|
171
|
+
]
|
tw_framework/lexer.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class LexerToken:
|
|
9
|
+
type: str
|
|
10
|
+
value: str
|
|
11
|
+
line: int
|
|
12
|
+
col: int
|
|
13
|
+
|
|
14
|
+
def to_dict(self):
|
|
15
|
+
return {
|
|
16
|
+
"type": self.type,
|
|
17
|
+
"value": self.value,
|
|
18
|
+
"line": self.line,
|
|
19
|
+
"col": self.col,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _legacy():
|
|
24
|
+
from . import compiler
|
|
25
|
+
|
|
26
|
+
return compiler
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def tokenize(code: str, allow_inline_scripts: bool = True) -> List[LexerToken]:
|
|
30
|
+
compiler = _legacy()
|
|
31
|
+
tokens = compiler.tokenize(code, allow_inline_scripts=allow_inline_scripts)
|
|
32
|
+
return [
|
|
33
|
+
LexerToken(
|
|
34
|
+
type=token.type,
|
|
35
|
+
value=token.value,
|
|
36
|
+
line=token.line,
|
|
37
|
+
col=token.col,
|
|
38
|
+
)
|
|
39
|
+
for token in tokens
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def tokenize_tw(code: str) -> List[LexerToken]:
|
|
44
|
+
return tokenize(code, allow_inline_scripts=True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def tokenize_file(path: str) -> List[LexerToken]:
|
|
48
|
+
compiler = _legacy()
|
|
49
|
+
return tokenize_tw(compiler.read_text_file(path))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = ["LexerToken", "tokenize", "tokenize_file", "tokenize_tw"]
|
tw_framework/lowering.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .ast_nodes import ComponentNode, ElementNode, ForNode, IfNode, LetNode, Program, ScriptNode, TextNode
|
|
4
|
+
from .ir import IRComponent, IRElement, IRFor, IRIf, IRLet, IRProgram, IRScript, IRText
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def lower_node(node):
|
|
8
|
+
if isinstance(node, TextNode):
|
|
9
|
+
return IRText(node.value)
|
|
10
|
+
if isinstance(node, LetNode):
|
|
11
|
+
return IRLet(node.name, node.value)
|
|
12
|
+
if isinstance(node, IfNode):
|
|
13
|
+
return IRIf(
|
|
14
|
+
condition=node.condition,
|
|
15
|
+
children=[lower_node(child) for child in node.children],
|
|
16
|
+
else_children=[lower_node(child) for child in node.else_children],
|
|
17
|
+
)
|
|
18
|
+
if isinstance(node, ForNode):
|
|
19
|
+
return IRFor(
|
|
20
|
+
var_name=node.var_name,
|
|
21
|
+
iterable=node.iterable,
|
|
22
|
+
children=[lower_node(child) for child in node.children],
|
|
23
|
+
)
|
|
24
|
+
if isinstance(node, ScriptNode):
|
|
25
|
+
return IRScript(node.raw_js)
|
|
26
|
+
if isinstance(node, ComponentNode):
|
|
27
|
+
return IRComponent(
|
|
28
|
+
name=node.name,
|
|
29
|
+
props=[{"name": prop.name, "value": prop.value} for prop in node.props],
|
|
30
|
+
children=[lower_node(child) for child in node.children],
|
|
31
|
+
)
|
|
32
|
+
if isinstance(node, ElementNode):
|
|
33
|
+
return IRElement(
|
|
34
|
+
tag=node.tag,
|
|
35
|
+
text=node.text,
|
|
36
|
+
attrs=[{"name": attr.name, "value": attr.value} for attr in node.attrs],
|
|
37
|
+
styles=[{"name": attr.name, "value": attr.value} for attr in node.styles],
|
|
38
|
+
events=[{"name": attr.name, "value": attr.value} for attr in node.events],
|
|
39
|
+
router=dict(node.router),
|
|
40
|
+
children=[lower_node(child) for child in node.children],
|
|
41
|
+
)
|
|
42
|
+
raise TypeError(f"Unsupported AST node: {type(node)!r}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def lower_program(program: Program) -> IRProgram:
|
|
46
|
+
return IRProgram(
|
|
47
|
+
meta={
|
|
48
|
+
"title": program.meta.title,
|
|
49
|
+
"layout": program.meta.layout,
|
|
50
|
+
"layouts": list(program.meta.layouts),
|
|
51
|
+
"render_mode": program.meta.render_mode,
|
|
52
|
+
"revalidate": program.meta.revalidate,
|
|
53
|
+
"redirect_to": program.meta.redirect_to,
|
|
54
|
+
"rewrite_to": program.meta.rewrite_to,
|
|
55
|
+
"responsive": program.meta.responsive,
|
|
56
|
+
},
|
|
57
|
+
head={
|
|
58
|
+
"metas": list(program.head.metas),
|
|
59
|
+
"icon": program.head.icon,
|
|
60
|
+
"seo": dict(program.head.seo),
|
|
61
|
+
},
|
|
62
|
+
lets=dict(program.lets),
|
|
63
|
+
state=dict(program.state),
|
|
64
|
+
body=[lower_node(node) for node in program.body],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = ["lower_node", "lower_program"]
|
tw_framework/parser.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import ast
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Any, List
|
|
7
|
+
|
|
8
|
+
from .ast_nodes import (
|
|
9
|
+
Attribute,
|
|
10
|
+
ComponentNode,
|
|
11
|
+
ElementNode,
|
|
12
|
+
ForNode,
|
|
13
|
+
HeadModel,
|
|
14
|
+
IfNode,
|
|
15
|
+
LetNode,
|
|
16
|
+
PageMeta,
|
|
17
|
+
Program,
|
|
18
|
+
ScriptNode,
|
|
19
|
+
node_to_dict,
|
|
20
|
+
)
|
|
21
|
+
from .lexer import tokenize_tw
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _legacy():
|
|
25
|
+
from . import compiler
|
|
26
|
+
|
|
27
|
+
return compiler
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _convert_node(node: Any):
|
|
31
|
+
tag = getattr(node, "tag", "")
|
|
32
|
+
if tag == "__let__":
|
|
33
|
+
return LetNode(node.name, node.value)
|
|
34
|
+
if tag == "__if__":
|
|
35
|
+
return IfNode(
|
|
36
|
+
condition=node.condition,
|
|
37
|
+
children=[_convert_node(child) for child in getattr(node, "children", [])],
|
|
38
|
+
else_children=[_convert_node(child) for child in getattr(node, "else_children", [])],
|
|
39
|
+
)
|
|
40
|
+
if tag == "__for__":
|
|
41
|
+
return ForNode(
|
|
42
|
+
var_name=node.var_name,
|
|
43
|
+
iterable=node.list_expr,
|
|
44
|
+
children=[_convert_node(child) for child in getattr(node, "children", [])],
|
|
45
|
+
)
|
|
46
|
+
if tag == "__script__":
|
|
47
|
+
return ScriptNode(node.raw_js)
|
|
48
|
+
if tag == "__script_tag__":
|
|
49
|
+
# Declarative external script tag (Next.js-like). In the modular AST we
|
|
50
|
+
# model it as a regular <script> element with a data attribute so the
|
|
51
|
+
# structure is still meaningful for analysis/debugging.
|
|
52
|
+
return ElementNode(
|
|
53
|
+
tag="script",
|
|
54
|
+
text=None,
|
|
55
|
+
attrs=[
|
|
56
|
+
Attribute(name="src", value=getattr(node, "src", "")),
|
|
57
|
+
Attribute(name="data-tw-strategy", value=getattr(node, "strategy", "")),
|
|
58
|
+
],
|
|
59
|
+
styles=[],
|
|
60
|
+
events=[],
|
|
61
|
+
router={},
|
|
62
|
+
children=[],
|
|
63
|
+
)
|
|
64
|
+
if tag == "__component__":
|
|
65
|
+
return ComponentNode(
|
|
66
|
+
name=node.name,
|
|
67
|
+
props=[Attribute(name=name, value=value) for name, value in getattr(node, "props", [])],
|
|
68
|
+
children=[_convert_node(child) for child in getattr(node, "children", [])],
|
|
69
|
+
)
|
|
70
|
+
return ElementNode(
|
|
71
|
+
tag=getattr(node, "tag", "div"),
|
|
72
|
+
text=getattr(node, "text", None),
|
|
73
|
+
attrs=[Attribute(name=name, value=value) for name, value in getattr(node, "attrs", [])],
|
|
74
|
+
styles=[Attribute(name=name, value=value) for name, value in getattr(node, "inline_style", [])],
|
|
75
|
+
events=[Attribute(name=name, value=value) for name, value in getattr(node, "events", [])],
|
|
76
|
+
router=dict(getattr(node, "router", {}) or {}),
|
|
77
|
+
children=[_convert_node(child) for child in getattr(node, "children", [])],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _normalize_scalar(value: Any):
|
|
82
|
+
compiler = _legacy()
|
|
83
|
+
if not isinstance(value, str):
|
|
84
|
+
return value
|
|
85
|
+
stripped = value.strip()
|
|
86
|
+
if len(stripped) >= 2 and stripped[0] == stripped[-1] and stripped[0] in {"'", '"'}:
|
|
87
|
+
try:
|
|
88
|
+
return ast.literal_eval(stripped)
|
|
89
|
+
except Exception as err:
|
|
90
|
+
warnings.warn(
|
|
91
|
+
f"Malformed quoted literal {stripped!r}: {err}. Falling back to raw string content.",
|
|
92
|
+
RuntimeWarning,
|
|
93
|
+
)
|
|
94
|
+
return stripped[1:-1]
|
|
95
|
+
return compiler.parse_literal_value(stripped)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def from_legacy_page(page, source_path: str = "") -> Program:
|
|
99
|
+
program = Program(
|
|
100
|
+
meta=PageMeta(
|
|
101
|
+
title=page.title,
|
|
102
|
+
layout=page.layout,
|
|
103
|
+
layouts=list(getattr(page, "layouts", []) or []),
|
|
104
|
+
render_mode=page.render_mode,
|
|
105
|
+
revalidate=page.revalidate,
|
|
106
|
+
redirect_to=page.redirect_to,
|
|
107
|
+
rewrite_to=page.rewrite_to,
|
|
108
|
+
responsive=bool(getattr(page, "responsive", False)),
|
|
109
|
+
),
|
|
110
|
+
head=HeadModel(
|
|
111
|
+
metas=list(getattr(page.head, "metas", []) or []),
|
|
112
|
+
icon=getattr(page.head, "icon", None),
|
|
113
|
+
seo=dict(getattr(page.head, "seo", {}) or {}),
|
|
114
|
+
),
|
|
115
|
+
lets={key: _normalize_scalar(value) for key, value in dict(getattr(page, "let_vars", {}) or {}).items()},
|
|
116
|
+
state={key: _normalize_scalar(value) for key, value in dict(getattr(page, "state_vars", {}) or {}).items()},
|
|
117
|
+
body=[_convert_node(node) for node in getattr(page, "body", [])],
|
|
118
|
+
loaded_sheets=list(getattr(page, "loaded_sheets", []) or []),
|
|
119
|
+
loaded_json=list(getattr(page, "loaded_json", []) or []),
|
|
120
|
+
source_path=source_path or getattr(page, "_tw_source_path", ""),
|
|
121
|
+
legacy_page=page,
|
|
122
|
+
)
|
|
123
|
+
return program
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def parse_text(text: str, base_dir: str = ".", file_path: str = "<memory>", debug: bool = False) -> Program:
|
|
127
|
+
compiler = _legacy()
|
|
128
|
+
tokens = compiler.tokenize_tw(text)
|
|
129
|
+
page = compiler.build_tw_ast(tokens, base_dir, file_path, text)
|
|
130
|
+
page._tw_source_path = file_path
|
|
131
|
+
return from_legacy_page(page, source_path=file_path)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def parse_file(path: str, debug: bool = False) -> Program:
|
|
135
|
+
compiler = _legacy()
|
|
136
|
+
source = compiler.read_text_file(path)
|
|
137
|
+
return parse_text(source, base_dir=compiler.normalize_path(os.path.dirname(path)), file_path=path, debug=debug)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_tw_ast(tokens, base_dir, file_path, source):
|
|
141
|
+
compiler = _legacy()
|
|
142
|
+
return compiler.build_tw_ast(tokens, base_dir, file_path, source)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def build_tss_ast_from_text(text: str):
|
|
146
|
+
compiler = _legacy()
|
|
147
|
+
return compiler.build_tss_ast_from_text(text)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def program_to_dict(program: Program):
|
|
151
|
+
return program.to_dict()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def nodes_to_dict(nodes: List[Any]):
|
|
155
|
+
return [node_to_dict(node) for node in nodes]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = [
|
|
159
|
+
"build_tss_ast_from_text",
|
|
160
|
+
"build_tw_ast",
|
|
161
|
+
"from_legacy_page",
|
|
162
|
+
"nodes_to_dict",
|
|
163
|
+
"parse_file",
|
|
164
|
+
"parse_text",
|
|
165
|
+
"program_to_dict",
|
|
166
|
+
"tokenize_tw",
|
|
167
|
+
]
|