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,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compiler statistics for TW Framework.
|
|
3
|
+
|
|
4
|
+
Tracks pages compiled, components compiled, files reused from cache,
|
|
5
|
+
files rebuilt, build duration, and cache hit rate.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CompilerStats:
|
|
16
|
+
"""Collects compiler statistics during a build."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.start_time = time.time()
|
|
20
|
+
self.pages_compiled = 0
|
|
21
|
+
self.components_compiled = 0
|
|
22
|
+
self.files_reused_from_cache = 0
|
|
23
|
+
self.files_rebuilt = 0
|
|
24
|
+
self.cache_hits = 0
|
|
25
|
+
self.cache_misses = 0
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def cache_hit_rate(self) -> float:
|
|
29
|
+
total = self.cache_hits + self.cache_misses
|
|
30
|
+
if total == 0:
|
|
31
|
+
return 0.0
|
|
32
|
+
return self.cache_hits / total
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def build_duration(self) -> float:
|
|
36
|
+
return time.time() - self.start_time
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
39
|
+
return {
|
|
40
|
+
"build_duration_seconds": round(self.build_duration, 2),
|
|
41
|
+
"pages_compiled": self.pages_compiled,
|
|
42
|
+
"components_compiled": self.components_compiled,
|
|
43
|
+
"files_reused_from_cache": self.files_reused_from_cache,
|
|
44
|
+
"files_rebuilt": self.files_rebuilt,
|
|
45
|
+
"cache_hits": self.cache_hits,
|
|
46
|
+
"cache_misses": self.cache_misses,
|
|
47
|
+
"cache_hit_rate": round(self.cache_hit_rate, 2),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["CompilerStats"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dead code detection for TW Framework.
|
|
3
|
+
|
|
4
|
+
Detects unused pages, APIs, middleware, components, and layouts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from typing import Dict, List
|
|
10
|
+
|
|
11
|
+
from . import compiler
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def detect_dead_code(project_root: str) -> Dict[str, List[str]]:
|
|
17
|
+
"""Return lists of dead code items."""
|
|
18
|
+
dead = {
|
|
19
|
+
"pages": [],
|
|
20
|
+
"apis": [],
|
|
21
|
+
"middleware": [],
|
|
22
|
+
"components": [],
|
|
23
|
+
"layouts": [],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Discover all pages
|
|
27
|
+
pages = compiler.discover_pages()
|
|
28
|
+
page_paths = {p["path"] for p in pages}
|
|
29
|
+
|
|
30
|
+
# Check for orphaned page files
|
|
31
|
+
pages_dir = compiler.PAGES_DIR
|
|
32
|
+
if os.path.isdir(pages_dir):
|
|
33
|
+
for root, _, files in os.walk(pages_dir):
|
|
34
|
+
for fname in files:
|
|
35
|
+
if fname.endswith(".tw"):
|
|
36
|
+
full_path = os.path.join(root, fname)
|
|
37
|
+
if full_path not in page_paths:
|
|
38
|
+
dead["pages"].append(full_path)
|
|
39
|
+
|
|
40
|
+
# TODO: Add API, middleware, component, and layout detection
|
|
41
|
+
return dead
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = ["detect_dead_code"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Complete project dependency graph for TW Framework.
|
|
3
|
+
|
|
4
|
+
Tracks pages, components, layouts, middleware, APIs, imports, and assets.
|
|
5
|
+
Persisted between builds for incremental rebuilds.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from typing import Dict, List, Optional, Set
|
|
12
|
+
|
|
13
|
+
from . import compiler
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
GRAPH_FILE = "dependency-graph.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DependencyGraph:
|
|
21
|
+
"""Maintains a directed graph of project dependencies."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, project_root: str):
|
|
24
|
+
self.project_root = project_root
|
|
25
|
+
self.graph: Dict[str, List[str]] = {}
|
|
26
|
+
self.reverse: Dict[str, List[str]] = {}
|
|
27
|
+
self._load()
|
|
28
|
+
|
|
29
|
+
def _load(self):
|
|
30
|
+
path = os.path.join(self.project_root, ".tw", GRAPH_FILE)
|
|
31
|
+
if os.path.exists(path):
|
|
32
|
+
try:
|
|
33
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
34
|
+
data = json.load(f)
|
|
35
|
+
self.graph = data.get("forward", {})
|
|
36
|
+
self.reverse = data.get("reverse", {})
|
|
37
|
+
except (json.JSONDecodeError, OSError):
|
|
38
|
+
logger.warning("Failed to load dependency graph; starting fresh")
|
|
39
|
+
self.graph = {}
|
|
40
|
+
self.reverse = {}
|
|
41
|
+
|
|
42
|
+
def save(self):
|
|
43
|
+
path = os.path.join(self.project_root, ".tw", GRAPH_FILE)
|
|
44
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
45
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
46
|
+
json.dump({"forward": self.graph, "reverse": self.reverse}, f, indent=2)
|
|
47
|
+
|
|
48
|
+
def add_dependency(self, source: str, target: str):
|
|
49
|
+
if source not in self.graph:
|
|
50
|
+
self.graph[source] = []
|
|
51
|
+
if target not in self.graph[source]:
|
|
52
|
+
self.graph[source].append(target)
|
|
53
|
+
if target not in self.reverse:
|
|
54
|
+
self.reverse[target] = []
|
|
55
|
+
if source not in self.reverse[target]:
|
|
56
|
+
self.reverse[target].append(source)
|
|
57
|
+
|
|
58
|
+
def get_dependents(self, node: str) -> List[str]:
|
|
59
|
+
"""Return all nodes that depend on the given node."""
|
|
60
|
+
return self.reverse.get(node, [])
|
|
61
|
+
|
|
62
|
+
def get_dependencies(self, node: str) -> List[str]:
|
|
63
|
+
"""Return all nodes that the given node depends on."""
|
|
64
|
+
return self.graph.get(node, [])
|
|
65
|
+
|
|
66
|
+
def get_affected_nodes(self, changed_nodes: Set[str]) -> Set[str]:
|
|
67
|
+
"""Return all nodes that need to be rebuilt when the given nodes change."""
|
|
68
|
+
affected = set(changed_nodes)
|
|
69
|
+
queue = list(changed_nodes)
|
|
70
|
+
while queue:
|
|
71
|
+
current = queue.pop(0)
|
|
72
|
+
for dependent in self.get_dependents(current):
|
|
73
|
+
if dependent not in affected:
|
|
74
|
+
affected.add(dependent)
|
|
75
|
+
queue.append(dependent)
|
|
76
|
+
return affected
|
|
77
|
+
|
|
78
|
+
def build_from_project(self):
|
|
79
|
+
"""Build the dependency graph by scanning the project."""
|
|
80
|
+
self.graph = {}
|
|
81
|
+
self.reverse = {}
|
|
82
|
+
pages = compiler.discover_pages()
|
|
83
|
+
for page in pages:
|
|
84
|
+
page_path = page["path"]
|
|
85
|
+
deps = compiler.collect_page_dependencies(page_path)
|
|
86
|
+
for dep in deps:
|
|
87
|
+
self.add_dependency(page_path, dep)
|
|
88
|
+
self.save()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = ["DependencyGraph"]
|
tw_framework/deploy.py
ADDED
tw_framework/dev.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Iterable, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Diagnostic:
|
|
9
|
+
severity: str
|
|
10
|
+
code: str
|
|
11
|
+
message: str
|
|
12
|
+
file_path: str = ""
|
|
13
|
+
line: int = 0
|
|
14
|
+
col: int = 0
|
|
15
|
+
suggestion: Optional[str] = None
|
|
16
|
+
notes: List[str] = field(default_factory=list)
|
|
17
|
+
phase: Optional[str] = None
|
|
18
|
+
exception_type: Optional[str] = None
|
|
19
|
+
category: str = "compiler"
|
|
20
|
+
relative_path: str = ""
|
|
21
|
+
absolute_path: str = ""
|
|
22
|
+
source_snippet: str = ""
|
|
23
|
+
highlight: str = ""
|
|
24
|
+
reason: str = ""
|
|
25
|
+
expected: str = ""
|
|
26
|
+
found: str = ""
|
|
27
|
+
why: str = ""
|
|
28
|
+
doc_link: str = ""
|
|
29
|
+
parser_state: str = ""
|
|
30
|
+
traceback: str = ""
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_legacy(cls, item):
|
|
34
|
+
return cls(
|
|
35
|
+
severity=getattr(item, "severity", "error"),
|
|
36
|
+
code=getattr(item, "code", "TW0000"),
|
|
37
|
+
message=getattr(item, "message", str(item)),
|
|
38
|
+
file_path=getattr(item, "file_path", "") or "",
|
|
39
|
+
line=getattr(item, "line", 0) or 0,
|
|
40
|
+
col=getattr(item, "col", 0) or 0,
|
|
41
|
+
suggestion=getattr(item, "suggestion", None),
|
|
42
|
+
notes=list(getattr(item, "notes", []) or []),
|
|
43
|
+
category=getattr(item, "category", "compiler") or "compiler",
|
|
44
|
+
relative_path=getattr(item, "relative_path", "") or "",
|
|
45
|
+
absolute_path=getattr(item, "absolute_path", "") or "",
|
|
46
|
+
source_snippet=getattr(item, "source_snippet", "") or "",
|
|
47
|
+
highlight=getattr(item, "highlight", "") or "",
|
|
48
|
+
reason=getattr(item, "reason", "") or "",
|
|
49
|
+
expected=getattr(item, "expected", "") or "",
|
|
50
|
+
found=getattr(item, "found", "") or "",
|
|
51
|
+
why=getattr(item, "why", "") or "",
|
|
52
|
+
doc_link=getattr(item, "doc_link", "") or "",
|
|
53
|
+
parser_state=getattr(item, "parser_state", "") or "",
|
|
54
|
+
traceback=getattr(item, "traceback", "") or "",
|
|
55
|
+
phase=getattr(item, "phase", None),
|
|
56
|
+
exception_type=getattr(item, "exception_type", None),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def to_dict(self):
|
|
60
|
+
return {
|
|
61
|
+
"severity": self.severity,
|
|
62
|
+
"code": self.code,
|
|
63
|
+
"message": self.message,
|
|
64
|
+
"file_path": self.file_path,
|
|
65
|
+
"line": self.line,
|
|
66
|
+
"col": self.col,
|
|
67
|
+
"suggestion": self.suggestion,
|
|
68
|
+
"notes": list(self.notes),
|
|
69
|
+
"phase": self.phase,
|
|
70
|
+
"exception_type": self.exception_type,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def format_advanced_error(diag: Diagnostic, project_root: str = "") -> str:
|
|
75
|
+
"""Format a diagnostic with all required fields."""
|
|
76
|
+
from .error_formatter import format_error
|
|
77
|
+
return format_error(diag, project_root)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DiagnosticBag:
|
|
81
|
+
def __init__(self):
|
|
82
|
+
self.items: List[Diagnostic] = []
|
|
83
|
+
|
|
84
|
+
def add(self, diagnostic: Diagnostic):
|
|
85
|
+
self.items.append(diagnostic)
|
|
86
|
+
|
|
87
|
+
def extend(self, diagnostics: Iterable[Diagnostic]):
|
|
88
|
+
self.items.extend(diagnostics)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def has_errors(self) -> bool:
|
|
92
|
+
return any(item.severity == "error" for item in self.items)
|
|
93
|
+
|
|
94
|
+
def to_list(self):
|
|
95
|
+
return [item.to_dict() for item in self.items]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = ["Diagnostic", "DiagnosticBag"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic imports / lazy loading support for TW Framework.
|
|
3
|
+
|
|
4
|
+
Components with `lazy true` are compiled into separate chunks and loaded on demand.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from . import compiler
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def extract_lazy_components(page_path: str) -> List[str]:
|
|
19
|
+
"""Return a list of component names that are marked as lazy in the given page."""
|
|
20
|
+
raw = compiler.read_text_file(page_path)
|
|
21
|
+
lazy_components = []
|
|
22
|
+
# Simple regex-based detection (in production, use the AST)
|
|
23
|
+
import re
|
|
24
|
+
for match in re.finditer(r'(\w+)\s*\{[^}]*lazy\s+true[^}]*\}', raw):
|
|
25
|
+
lazy_components.append(match.group(1))
|
|
26
|
+
return lazy_components
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def generate_lazy_chunks(page_path: str, output_dir: str) -> Dict[str, str]:
|
|
30
|
+
"""Generate separate chunks for lazy components and return a mapping."""
|
|
31
|
+
lazy_comps = extract_lazy_components(page_path)
|
|
32
|
+
chunk_map = {}
|
|
33
|
+
for comp_name in lazy_comps:
|
|
34
|
+
comp_path = compiler.resolve_component_path(comp_name)
|
|
35
|
+
if comp_path and os.path.exists(comp_path):
|
|
36
|
+
raw = compiler.read_text_file(comp_path)
|
|
37
|
+
# In production, compile the component to JS
|
|
38
|
+
js_content = f"// Lazy component: {comp_name}\n"
|
|
39
|
+
digest = hashlib.sha256(js_content.encode("utf-8")).hexdigest()[:12]
|
|
40
|
+
filename = f"lazy_{comp_name}.{digest}.js"
|
|
41
|
+
chunk_dir = os.path.join(output_dir, "_tw", "chunks")
|
|
42
|
+
os.makedirs(chunk_dir, exist_ok=True)
|
|
43
|
+
chunk_path = os.path.join(chunk_dir, filename)
|
|
44
|
+
if not os.path.exists(chunk_path):
|
|
45
|
+
with open(chunk_path, "w", encoding="utf-8") as f:
|
|
46
|
+
f.write(js_content)
|
|
47
|
+
chunk_map[comp_name] = f"/_tw/chunks/{filename}"
|
|
48
|
+
return chunk_map
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = ["extract_lazy_components", "generate_lazy_chunks"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Better error formatting for TW Framework.
|
|
3
|
+
|
|
4
|
+
Every error includes: error code, category, severity, file, relative path,
|
|
5
|
+
absolute path, line, column, highlighted code, explanation, reason,
|
|
6
|
+
expected, found, suggestions, and documentation link.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .diagnostics import Diagnostic
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_error(diag: Diagnostic, project_root: str) -> str:
|
|
19
|
+
"""Format a diagnostic into a human-readable string with all required fields."""
|
|
20
|
+
lines = []
|
|
21
|
+
code_display = diag.code if str(diag.code).upper().startswith("TW") else f"TW{diag.code}"
|
|
22
|
+
lines.append(f"{code_display} • {diag.category} Error")
|
|
23
|
+
lines.append("")
|
|
24
|
+
lines.append(f"Severity: {diag.severity.upper()}")
|
|
25
|
+
if diag.file_path:
|
|
26
|
+
rel_path = os.path.relpath(diag.file_path, project_root) if project_root else diag.file_path
|
|
27
|
+
lines.append(f"Project: {rel_path}")
|
|
28
|
+
lines.append(f"Absolute: {os.path.abspath(diag.file_path)}")
|
|
29
|
+
if diag.line:
|
|
30
|
+
lines.append(f"Line: {diag.line}")
|
|
31
|
+
if diag.col:
|
|
32
|
+
lines.append(f"Column: {diag.col}")
|
|
33
|
+
if diag.source_snippet:
|
|
34
|
+
lines.append("")
|
|
35
|
+
lines.append("Code:")
|
|
36
|
+
lines.append(diag.source_snippet)
|
|
37
|
+
if diag.reason:
|
|
38
|
+
lines.append("")
|
|
39
|
+
lines.append(f"Reason: {diag.reason}")
|
|
40
|
+
if diag.expected:
|
|
41
|
+
lines.append(f"Expected: {diag.expected}")
|
|
42
|
+
if diag.found:
|
|
43
|
+
lines.append(f"Found: {diag.found}")
|
|
44
|
+
if diag.why:
|
|
45
|
+
lines.append(f"Why: {diag.why}")
|
|
46
|
+
if diag.suggestion:
|
|
47
|
+
lines.append(f"Suggestion: {diag.suggestion}")
|
|
48
|
+
if diag.doc_link:
|
|
49
|
+
lines.append(f"Docs: {diag.doc_link}")
|
|
50
|
+
return "\n".join(lines)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = ["format_error"]
|