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
tw_framework/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""TW Framework package."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.4.0"
|
|
4
|
+
|
|
5
|
+
from .server import run_production_server, SSRCache # noqa: F401
|
|
6
|
+
from .reactivity import ( # noqa: F401
|
|
7
|
+
has_reactivity,
|
|
8
|
+
parse_state_block,
|
|
9
|
+
get_reactivity_runtime_js,
|
|
10
|
+
transform_reactive_attrs,
|
|
11
|
+
)
|
|
12
|
+
from .compiler import compile_file_pipeline, compile_text_pipeline # noqa: F401
|
|
13
|
+
from .interpreter import Interpreter # noqa: F401
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Advanced compiler diagnostics for TW Framework.
|
|
3
|
+
|
|
4
|
+
Detects duplicate attributes, unused variables, circular imports,
|
|
5
|
+
recursive components, invalid nesting, duplicate IDs, duplicate routes,
|
|
6
|
+
and missing exports.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Dict, List
|
|
11
|
+
|
|
12
|
+
from . import compiler
|
|
13
|
+
from .diagnostics import Diagnostic, DiagnosticBag
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_advanced_diagnostics(project_root: str) -> DiagnosticBag:
|
|
19
|
+
"""Run all advanced diagnostics and return the results."""
|
|
20
|
+
bag = DiagnosticBag()
|
|
21
|
+
# TODO: Implement actual detection logic
|
|
22
|
+
return bag
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ["run_advanced_diagnostics"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smart asset optimization for TW Framework.
|
|
3
|
+
|
|
4
|
+
Automatically compresses images, converts to WebP, lazy loads images,
|
|
5
|
+
preloads critical assets, and removes duplicate CSS/JS.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def optimize_assets(project_root: str, output_dir: str) -> Dict[str, List[str]]:
|
|
16
|
+
"""Optimize assets in the output directory."""
|
|
17
|
+
optimized = {
|
|
18
|
+
"images_compressed": [],
|
|
19
|
+
"images_converted_to_webp": [],
|
|
20
|
+
"images_lazy_loaded": [],
|
|
21
|
+
"critical_assets_preloaded": [],
|
|
22
|
+
"duplicate_css_removed": [],
|
|
23
|
+
"duplicate_js_removed": [],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# TODO: Implement actual optimization logic
|
|
27
|
+
return optimized
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ["optimize_assets"]
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass, field, is_dataclass
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Attribute:
|
|
9
|
+
name: str
|
|
10
|
+
value: Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def attribute_to_dict(attribute: Any) -> Dict[str, Any]:
|
|
14
|
+
if isinstance(attribute, Attribute):
|
|
15
|
+
return {
|
|
16
|
+
"name": attribute.name,
|
|
17
|
+
"value": serialize_value(attribute.value),
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
"name": getattr(attribute, "name", ""),
|
|
21
|
+
"value": serialize_value(getattr(attribute, "value", None)),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def serialize_value(value: Any) -> Any:
|
|
26
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
27
|
+
return value
|
|
28
|
+
if isinstance(value, Attribute):
|
|
29
|
+
return attribute_to_dict(value)
|
|
30
|
+
if isinstance(value, dict):
|
|
31
|
+
return {str(key): serialize_value(item) for key, item in value.items()}
|
|
32
|
+
if isinstance(value, (list, tuple, set)):
|
|
33
|
+
return [serialize_value(item) for item in value]
|
|
34
|
+
if is_dataclass(value):
|
|
35
|
+
return serialize_value(asdict(value))
|
|
36
|
+
if hasattr(value, "to_dict") and callable(value.to_dict):
|
|
37
|
+
try:
|
|
38
|
+
return serialize_value(value.to_dict())
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
if hasattr(value, "rules") and isinstance(getattr(value, "rules"), list):
|
|
42
|
+
return {
|
|
43
|
+
"kind": value.__class__.__name__,
|
|
44
|
+
"rules": [serialize_value(rule) for rule in getattr(value, "rules", [])],
|
|
45
|
+
}
|
|
46
|
+
if hasattr(value, "selector") and hasattr(value, "declarations"):
|
|
47
|
+
return {
|
|
48
|
+
"kind": value.__class__.__name__,
|
|
49
|
+
"selector": getattr(value, "selector", ""),
|
|
50
|
+
"declarations": [serialize_value(item) for item in getattr(value, "declarations", [])],
|
|
51
|
+
"children": [serialize_value(item) for item in getattr(value, "children", [])],
|
|
52
|
+
}
|
|
53
|
+
if hasattr(value, "__dict__"):
|
|
54
|
+
raw = {}
|
|
55
|
+
for key, item in vars(value).items():
|
|
56
|
+
if key.startswith("_"):
|
|
57
|
+
continue
|
|
58
|
+
raw[key] = serialize_value(item)
|
|
59
|
+
if raw:
|
|
60
|
+
raw.setdefault("kind", value.__class__.__name__)
|
|
61
|
+
return raw
|
|
62
|
+
return repr(value)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class HeadModel:
|
|
67
|
+
metas: List[Dict[str, Any]] = field(default_factory=list)
|
|
68
|
+
icon: Optional[str] = None
|
|
69
|
+
seo: Dict[str, Any] = field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class PageMeta:
|
|
74
|
+
title: str = ""
|
|
75
|
+
layout: Optional[str] = None
|
|
76
|
+
layouts: List[str] = field(default_factory=list)
|
|
77
|
+
render_mode: str = "static"
|
|
78
|
+
revalidate: Optional[int] = None
|
|
79
|
+
redirect_to: Optional[str] = None
|
|
80
|
+
rewrite_to: Optional[str] = None
|
|
81
|
+
responsive: bool = False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class BaseNode:
|
|
86
|
+
kind: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class TextNode(BaseNode):
|
|
91
|
+
value: str
|
|
92
|
+
|
|
93
|
+
def __init__(self, value: str):
|
|
94
|
+
super().__init__("text")
|
|
95
|
+
self.value = value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class LetNode(BaseNode):
|
|
100
|
+
name: str
|
|
101
|
+
value: Any
|
|
102
|
+
|
|
103
|
+
def __init__(self, name: str, value: Any):
|
|
104
|
+
super().__init__("let")
|
|
105
|
+
self.name = name
|
|
106
|
+
self.value = value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class IfNode(BaseNode):
|
|
111
|
+
condition: str
|
|
112
|
+
children: List[BaseNode] = field(default_factory=list)
|
|
113
|
+
else_children: List[BaseNode] = field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
def __init__(self, condition: str, children: Optional[List[BaseNode]] = None, else_children: Optional[List[BaseNode]] = None):
|
|
116
|
+
super().__init__("if")
|
|
117
|
+
self.condition = condition
|
|
118
|
+
self.children = children or []
|
|
119
|
+
self.else_children = else_children or []
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class ForNode(BaseNode):
|
|
124
|
+
var_name: str
|
|
125
|
+
iterable: str
|
|
126
|
+
children: List[BaseNode] = field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
def __init__(self, var_name: str, iterable: str, children: Optional[List[BaseNode]] = None):
|
|
129
|
+
super().__init__("for")
|
|
130
|
+
self.var_name = var_name
|
|
131
|
+
self.iterable = iterable
|
|
132
|
+
self.children = children or []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class ScriptNode(BaseNode):
|
|
137
|
+
raw_js: str
|
|
138
|
+
|
|
139
|
+
def __init__(self, raw_js: str):
|
|
140
|
+
super().__init__("script")
|
|
141
|
+
self.raw_js = raw_js
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class ElementNode(BaseNode):
|
|
146
|
+
tag: str
|
|
147
|
+
text: Optional[str] = None
|
|
148
|
+
attrs: List[Attribute] = field(default_factory=list)
|
|
149
|
+
styles: List[Attribute] = field(default_factory=list)
|
|
150
|
+
events: List[Attribute] = field(default_factory=list)
|
|
151
|
+
router: Dict[str, Any] = field(default_factory=dict)
|
|
152
|
+
children: List[BaseNode] = field(default_factory=list)
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
tag: str,
|
|
157
|
+
text: Optional[str] = None,
|
|
158
|
+
attrs: Optional[List[Attribute]] = None,
|
|
159
|
+
styles: Optional[List[Attribute]] = None,
|
|
160
|
+
events: Optional[List[Attribute]] = None,
|
|
161
|
+
router: Optional[Dict[str, Any]] = None,
|
|
162
|
+
children: Optional[List[BaseNode]] = None,
|
|
163
|
+
):
|
|
164
|
+
super().__init__("element")
|
|
165
|
+
self.tag = tag
|
|
166
|
+
self.text = text
|
|
167
|
+
self.attrs = attrs or []
|
|
168
|
+
self.styles = styles or []
|
|
169
|
+
self.events = events or []
|
|
170
|
+
self.router = router or {}
|
|
171
|
+
self.children = children or []
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class ComponentNode(BaseNode):
|
|
176
|
+
name: str
|
|
177
|
+
props: List[Attribute] = field(default_factory=list)
|
|
178
|
+
children: List[BaseNode] = field(default_factory=list)
|
|
179
|
+
|
|
180
|
+
def __init__(self, name: str, props: Optional[List[Attribute]] = None, children: Optional[List[BaseNode]] = None):
|
|
181
|
+
super().__init__("component")
|
|
182
|
+
self.name = name
|
|
183
|
+
self.props = props or []
|
|
184
|
+
self.children = children or []
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class Program:
|
|
189
|
+
meta: PageMeta = field(default_factory=PageMeta)
|
|
190
|
+
head: HeadModel = field(default_factory=HeadModel)
|
|
191
|
+
lets: Dict[str, Any] = field(default_factory=dict)
|
|
192
|
+
state: Dict[str, Any] = field(default_factory=dict)
|
|
193
|
+
body: List[BaseNode] = field(default_factory=list)
|
|
194
|
+
loaded_sheets: List[Any] = field(default_factory=list)
|
|
195
|
+
loaded_json: List[Dict[str, Any]] = field(default_factory=list)
|
|
196
|
+
source_path: str = ""
|
|
197
|
+
legacy_page: Any = None
|
|
198
|
+
|
|
199
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
200
|
+
return {
|
|
201
|
+
"meta": {
|
|
202
|
+
"title": self.meta.title,
|
|
203
|
+
"layout": self.meta.layout,
|
|
204
|
+
"layouts": list(self.meta.layouts),
|
|
205
|
+
"render_mode": self.meta.render_mode,
|
|
206
|
+
"revalidate": self.meta.revalidate,
|
|
207
|
+
"redirect_to": self.meta.redirect_to,
|
|
208
|
+
"rewrite_to": self.meta.rewrite_to,
|
|
209
|
+
"responsive": self.meta.responsive,
|
|
210
|
+
},
|
|
211
|
+
"head": {
|
|
212
|
+
"metas": list(self.head.metas),
|
|
213
|
+
"icon": self.head.icon,
|
|
214
|
+
"seo": dict(self.head.seo),
|
|
215
|
+
},
|
|
216
|
+
"lets": serialize_value(self.lets),
|
|
217
|
+
"state": serialize_value(self.state),
|
|
218
|
+
"body": [node_to_dict(node) for node in self.body],
|
|
219
|
+
"loaded_sheets": serialize_value(self.loaded_sheets),
|
|
220
|
+
"loaded_json": serialize_value(self.loaded_json),
|
|
221
|
+
"source_path": self.source_path,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def node_to_dict(node: BaseNode) -> Dict[str, Any]:
|
|
226
|
+
if isinstance(node, TextNode):
|
|
227
|
+
return {"kind": node.kind, "value": node.value}
|
|
228
|
+
if isinstance(node, LetNode):
|
|
229
|
+
return {"kind": node.kind, "name": node.name, "value": serialize_value(node.value)}
|
|
230
|
+
if isinstance(node, IfNode):
|
|
231
|
+
return {
|
|
232
|
+
"kind": node.kind,
|
|
233
|
+
"condition": node.condition,
|
|
234
|
+
"children": [node_to_dict(child) for child in node.children],
|
|
235
|
+
"else_children": [node_to_dict(child) for child in node.else_children],
|
|
236
|
+
}
|
|
237
|
+
if isinstance(node, ForNode):
|
|
238
|
+
return {
|
|
239
|
+
"kind": node.kind,
|
|
240
|
+
"var_name": node.var_name,
|
|
241
|
+
"iterable": node.iterable,
|
|
242
|
+
"children": [node_to_dict(child) for child in node.children],
|
|
243
|
+
}
|
|
244
|
+
if isinstance(node, ScriptNode):
|
|
245
|
+
return {"kind": node.kind, "raw_js": node.raw_js}
|
|
246
|
+
if isinstance(node, ElementNode):
|
|
247
|
+
return {
|
|
248
|
+
"kind": node.kind,
|
|
249
|
+
"tag": node.tag,
|
|
250
|
+
"text": node.text,
|
|
251
|
+
"attrs": [attribute_to_dict(attr) for attr in node.attrs],
|
|
252
|
+
"styles": [attribute_to_dict(attr) for attr in node.styles],
|
|
253
|
+
"events": [attribute_to_dict(attr) for attr in node.events],
|
|
254
|
+
"router": serialize_value(node.router),
|
|
255
|
+
"children": [node_to_dict(child) for child in node.children],
|
|
256
|
+
}
|
|
257
|
+
if isinstance(node, ComponentNode):
|
|
258
|
+
return {
|
|
259
|
+
"kind": node.kind,
|
|
260
|
+
"name": node.name,
|
|
261
|
+
"props": [attribute_to_dict(prop) for prop in node.props],
|
|
262
|
+
"children": [node_to_dict(child) for child in node.children],
|
|
263
|
+
}
|
|
264
|
+
return {"kind": getattr(node, "kind", "unknown"), "repr": repr(node)}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
__all__ = [
|
|
268
|
+
"Attribute",
|
|
269
|
+
"BaseNode",
|
|
270
|
+
"ComponentNode",
|
|
271
|
+
"ElementNode",
|
|
272
|
+
"ForNode",
|
|
273
|
+
"HeadModel",
|
|
274
|
+
"IfNode",
|
|
275
|
+
"LetNode",
|
|
276
|
+
"PageMeta",
|
|
277
|
+
"Program",
|
|
278
|
+
"ScriptNode",
|
|
279
|
+
"TextNode",
|
|
280
|
+
"attribute_to_dict",
|
|
281
|
+
"node_to_dict",
|
|
282
|
+
"serialize_value",
|
|
283
|
+
]
|
tw_framework/build.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build performance optimization for TW Framework.
|
|
3
|
+
|
|
4
|
+
Parallelizes compilation, uses multiple CPU cores, and avoids recompiling unchanged files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from . import compiler
|
|
12
|
+
from .compiler_stats import CompilerStats
|
|
13
|
+
from .incremental_cache import IncrementalCache
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def optimize_build(project_root: str, output_dir: str, force: bool = False, workers: Optional[int] = None) -> CompilerStats:
|
|
19
|
+
"""Run an optimized build with parallelism and caching."""
|
|
20
|
+
stats = CompilerStats()
|
|
21
|
+
cache = IncrementalCache(project_root)
|
|
22
|
+
# TODO: Implement actual parallel build logic
|
|
23
|
+
return stats
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["optimize_build"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build report generation for TW Framework.
|
|
3
|
+
|
|
4
|
+
Generates .tw/build-report.json with bundle sizes, timings, and analysis.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BuildReport:
|
|
17
|
+
"""Collects build metrics and generates a report."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, project_root: str):
|
|
20
|
+
self.project_root = project_root
|
|
21
|
+
self.start_time = time.time()
|
|
22
|
+
self.pages: List[Dict[str, Any]] = []
|
|
23
|
+
self.components: List[Dict[str, Any]] = []
|
|
24
|
+
self.total_bundle_size = 0
|
|
25
|
+
self.total_css_size = 0
|
|
26
|
+
self.total_js_size = 0
|
|
27
|
+
self.total_image_size = 0
|
|
28
|
+
self.unused_code: List[str] = []
|
|
29
|
+
self.largest_components: List[Dict[str, Any]] = []
|
|
30
|
+
self.slowest_pages: List[Dict[str, Any]] = []
|
|
31
|
+
self.build_timings: Dict[str, float] = {}
|
|
32
|
+
|
|
33
|
+
def add_page(self, path: str, size: int, duration: float):
|
|
34
|
+
self.pages.append({"path": path, "size": size, "duration": duration})
|
|
35
|
+
self.total_bundle_size += size
|
|
36
|
+
|
|
37
|
+
def add_component(self, name: str, size: int):
|
|
38
|
+
self.components.append({"name": name, "size": size})
|
|
39
|
+
self.total_bundle_size += size
|
|
40
|
+
|
|
41
|
+
def add_css_size(self, size: int):
|
|
42
|
+
self.total_css_size += size
|
|
43
|
+
|
|
44
|
+
def add_js_size(self, size: int):
|
|
45
|
+
self.total_js_size += size
|
|
46
|
+
|
|
47
|
+
def add_image_size(self, size: int):
|
|
48
|
+
self.total_image_size += size
|
|
49
|
+
|
|
50
|
+
def add_unused_code(self, item: str):
|
|
51
|
+
self.unused_code.append(item)
|
|
52
|
+
|
|
53
|
+
def finalize(self) -> Dict[str, Any]:
|
|
54
|
+
duration = time.time() - self.start_time
|
|
55
|
+
self.pages.sort(key=lambda p: p["duration"], reverse=True)
|
|
56
|
+
self.slowest_pages = self.pages[:10]
|
|
57
|
+
self.components.sort(key=lambda c: c["size"], reverse=True)
|
|
58
|
+
self.largest_components = self.components[:10]
|
|
59
|
+
return {
|
|
60
|
+
"build_duration_seconds": round(duration, 2),
|
|
61
|
+
"total_bundle_size_bytes": self.total_bundle_size,
|
|
62
|
+
"total_css_size_bytes": self.total_css_size,
|
|
63
|
+
"total_js_size_bytes": self.total_js_size,
|
|
64
|
+
"total_image_size_bytes": self.total_image_size,
|
|
65
|
+
"pages_compiled": len(self.pages),
|
|
66
|
+
"components_compiled": len(self.components),
|
|
67
|
+
"unused_code": self.unused_code,
|
|
68
|
+
"largest_components": self.largest_components,
|
|
69
|
+
"slowest_pages": self.slowest_pages,
|
|
70
|
+
"build_timings": self.build_timings,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def save(self):
|
|
74
|
+
report = self.finalize()
|
|
75
|
+
report_dir = os.path.join(self.project_root, ".tw")
|
|
76
|
+
os.makedirs(report_dir, exist_ok=True)
|
|
77
|
+
report_path = os.path.join(report_dir, "build-report.json")
|
|
78
|
+
with open(report_path, "w", encoding="utf-8") as f:
|
|
79
|
+
json.dump(report, f, indent=2)
|
|
80
|
+
logger.info("Build report saved to %s", report_path)
|
|
81
|
+
return report
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__all__ = ["BuildReport"]
|