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.
Files changed (51) hide show
  1. tw_framework/__init__.py +13 -0
  2. tw_framework/advanced_diagnostics.py +25 -0
  3. tw_framework/asset_optimizer.py +30 -0
  4. tw_framework/ast_nodes.py +283 -0
  5. tw_framework/build.py +7 -0
  6. tw_framework/build_performance.py +26 -0
  7. tw_framework/build_report.py +84 -0
  8. tw_framework/cli.py +1258 -0
  9. tw_framework/code_splitting.py +76 -0
  10. tw_framework/common.py +22 -0
  11. tw_framework/compiler.py +4817 -0
  12. tw_framework/compiler_stats.py +51 -0
  13. tw_framework/dead_code.py +44 -0
  14. tw_framework/dependency_graph.py +91 -0
  15. tw_framework/deploy.py +7 -0
  16. tw_framework/dev.py +9 -0
  17. tw_framework/diagnostics.py +98 -0
  18. tw_framework/dynamic_imports.py +51 -0
  19. tw_framework/error_formatter.py +53 -0
  20. tw_framework/framework.py +3216 -0
  21. tw_framework/hydration.py +129 -0
  22. tw_framework/incremental_cache.py +53 -0
  23. tw_framework/interpreter.py +42 -0
  24. tw_framework/ir.py +171 -0
  25. tw_framework/lexer.py +52 -0
  26. tw_framework/lowering.py +68 -0
  27. tw_framework/parser.py +167 -0
  28. tw_framework/partial_rebuild.py +24 -0
  29. tw_framework/performance_analyzer.py +37 -0
  30. tw_framework/plugin_runtime.py +196 -0
  31. tw_framework/production_optimizer.py +82 -0
  32. tw_framework/reactivity.py +349 -0
  33. tw_framework/render_css.py +22 -0
  34. tw_framework/render_html.py +259 -0
  35. tw_framework/route_optimizer.py +40 -0
  36. tw_framework/router.py +7 -0
  37. tw_framework/runtime_model.py +23 -0
  38. tw_framework/runtime_values.py +31 -0
  39. tw_framework/semantic.py +162 -0
  40. tw_framework/server.py +532 -0
  41. tw_framework/signature.py +51 -0
  42. tw_framework/static_dynamic_auto.py +32 -0
  43. tw_framework/streaming.py +135 -0
  44. tw_framework/test_build_cli_pytest.py +1078 -0
  45. tw_framework/tree_shaking.py +48 -0
  46. tw_framework/twm_parser.py +274 -0
  47. tw_framework-0.2.3.dist-info/METADATA +8 -0
  48. tw_framework-0.2.3.dist-info/RECORD +51 -0
  49. tw_framework-0.2.3.dist-info/WHEEL +5 -0
  50. tw_framework-0.2.3.dist-info/entry_points.txt +2 -0
  51. tw_framework-0.2.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def _legacy():
5
+ from . import compiler
6
+
7
+ return compiler
8
+
9
+
10
+ def parse_stylesheet(text: str):
11
+ compiler = _legacy()
12
+ return compiler.build_tss_ast_from_text(text)
13
+
14
+
15
+ def render_stylesheet(text: str, context=None, minify: bool = False) -> str:
16
+ compiler = _legacy()
17
+ sheet = parse_stylesheet(text)
18
+ css = compiler.render_css(sheet, context=context or {})
19
+ return compiler.minify_css_content(css) if minify else css
20
+
21
+
22
+ __all__ = ["parse_stylesheet", "render_stylesheet"]
@@ -0,0 +1,259 @@
1
+ from __future__ import annotations
2
+
3
+ import html
4
+ import logging
5
+ import os
6
+ from typing import Dict, Generator, Iterable, List, Optional
7
+
8
+ from .ir import IRComponent, IRElement, IRFor, IRIf, IRLet, IRProgram, IRScript, IRText
9
+ from .runtime_values import RuntimeEnvironment
10
+ from .signature import build_signature_banner, build_signature_meta_tag, compute_tw_signature
11
+ from .hydration import wrap_interactive_nodes
12
+ from .streaming import render_program_streaming
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _legacy():
18
+ from . import compiler
19
+
20
+ return compiler
21
+
22
+
23
+ VOID_TAGS = {"br", "hr", "img", "input", "meta", "link"}
24
+
25
+ _COMPONENT_PROGRAM_CACHE: Dict[str, IRProgram] = {}
26
+
27
+
28
+ def _interpolate(value, context):
29
+ compiler = _legacy()
30
+ if value is None:
31
+ return ""
32
+ if not isinstance(value, str):
33
+ return value
34
+ return compiler.interpolate(value, context)
35
+
36
+
37
+ def _evaluate(expr: str, context):
38
+ compiler = _legacy()
39
+ return compiler.evaluate_expression(expr, context)
40
+
41
+
42
+ def build_runtime_context(program, context: Optional[Dict] = None) -> Dict:
43
+ compiler = _legacy()
44
+ runtime_context: Dict = {}
45
+ runtime_context.update(dict(program.lets))
46
+ runtime_context.update(dict(program.state))
47
+ if getattr(program, "legacy_page", None) is not None and getattr(program, "source_path", ""):
48
+ try:
49
+ base_context = compiler.create_base_context(program.legacy_page, program.source_path)
50
+ runtime_context.update(base_context)
51
+ except Exception:
52
+ logger.exception("Failed to create base runtime context for %s", getattr(program, "source_path", ""))
53
+ runtime_context.update(dict(context or {}))
54
+ return runtime_context
55
+
56
+
57
+ def _render_attrs(attrs: Iterable[Dict], env: RuntimeEnvironment) -> str:
58
+ parts: List[str] = []
59
+ context = env.to_context()
60
+ for attr in attrs:
61
+ value = _interpolate(attr["value"], context)
62
+ if value is True:
63
+ parts.append(attr["name"])
64
+ continue
65
+ if value in {False, None}:
66
+ continue
67
+ parts.append(f'{attr["name"]}="{html.escape(str(value), quote=True)}"')
68
+ return f" {' '.join(parts)}" if parts else ""
69
+
70
+
71
+ def _render_style(styles: Iterable[Dict], env: RuntimeEnvironment) -> str:
72
+ resolved = []
73
+ context = env.to_context()
74
+ for item in styles:
75
+ value = _interpolate(item["value"], context)
76
+ resolved.append(f'{item["name"]}: {value}')
77
+ return f' style="{html.escape("; ".join(resolved), quote=True)}"' if resolved else ""
78
+
79
+
80
+ def _load_component_ir(name: str) -> Optional[IRProgram]:
81
+ compiler = _legacy()
82
+ try:
83
+ path = compiler.resolve_component_path(name)
84
+ except Exception:
85
+ logger.exception("Failed to resolve component path for `%s`", name)
86
+ return None
87
+
88
+ if not path or not os.path.exists(path):
89
+ return None
90
+
91
+ cached = _COMPONENT_PROGRAM_CACHE.get(path)
92
+ if cached is not None:
93
+ return cached
94
+
95
+ from .lowering import lower_program
96
+ from .parser import parse_file
97
+
98
+ try:
99
+ program = parse_file(path)
100
+ ir_program = lower_program(program)
101
+ except Exception:
102
+ logger.exception("Failed to parse/lower component `%s` at %s", name, path)
103
+ return None
104
+
105
+ _COMPONENT_PROGRAM_CACHE[path] = ir_program
106
+ return ir_program
107
+
108
+
109
+ def render_node(node, env: RuntimeEnvironment) -> str:
110
+ context = env.to_context()
111
+ if isinstance(node, IRText):
112
+ return html.escape(str(_interpolate(node.value, context)))
113
+ if isinstance(node, IRLet):
114
+ env.set(node.name, _interpolate(node.value, context))
115
+ return ""
116
+ if isinstance(node, IRIf):
117
+ branch = node.children if _evaluate(node.condition, context) else node.else_children
118
+ return "".join(render_node(child, env.child()) for child in branch)
119
+ if isinstance(node, IRFor):
120
+ rendered = []
121
+ items = _evaluate(node.iterable, context)
122
+ for item in items or []:
123
+ loop_env = env.child({node.var_name: item})
124
+ rendered.append("".join(render_node(child, loop_env) for child in node.children))
125
+ return "".join(rendered)
126
+ if isinstance(node, IRScript):
127
+ return f"<script>{node.raw_js}</script>"
128
+ if isinstance(node, IRComponent):
129
+ props = {prop["name"]: _interpolate(prop["value"], context) for prop in node.props}
130
+
131
+ stack = context.get("__tw_component_stack__", ())
132
+ if node.name in stack:
133
+ chain = " -> ".join(stack + (node.name,))
134
+ logger.error("Circular component reference detected: %s", chain)
135
+ return (
136
+ f'<div data-tw-component-error="circular" data-tw-component="{html.escape(node.name, quote=True)}">'
137
+ f"Circular component reference: {html.escape(chain)}"
138
+ f"</div>"
139
+ )
140
+
141
+ component_ir = _load_component_ir(node.name)
142
+ if component_ir is None:
143
+ children_html = "".join(render_node(child, env.child()) for child in node.children)
144
+ props_html = "".join(
145
+ f'<li><strong>{html.escape(str(key))}</strong>: {html.escape(str(value))}</li>'
146
+ for key, value in props.items()
147
+ )
148
+ logger.warning("Component `%s` could not be resolved; rendering placeholder.", node.name)
149
+ return (
150
+ f'<div data-tw-component-error="not-found" data-tw-component="{html.escape(node.name, quote=True)}">'
151
+ f"<div>Component not found: {html.escape(node.name)}</div>"
152
+ f"<ul>{props_html}</ul>{children_html}</div>"
153
+ )
154
+
155
+ component_env = RuntimeEnvironment(
156
+ values={
157
+ **dict(component_ir.lets),
158
+ **dict(component_ir.state),
159
+ **props,
160
+ "__tw_component_stack__": stack + (node.name,),
161
+ }
162
+ )
163
+ return "".join(render_node(child, component_env) for child in component_ir.body)
164
+ if isinstance(node, IRElement):
165
+ attrs = _render_attrs(node.attrs, env)
166
+ style_attr = _render_style(node.styles, env)
167
+ event_attrs = _render_attrs(node.events, env)
168
+ text = html.escape(str(_interpolate(node.text, context))) if node.text is not None else ""
169
+ children = "".join(render_node(child, env.child()) for child in node.children)
170
+ if node.tag in VOID_TAGS:
171
+ return f"<{node.tag}{attrs}{style_attr}{event_attrs}>"
172
+ return f"<{node.tag}{attrs}{style_attr}{event_attrs}>{text}{children}</{node.tag}>"
173
+ return ""
174
+
175
+
176
+ def render_program(program: IRProgram, context: Optional[Dict] = None) -> str:
177
+ env = RuntimeEnvironment(values={**dict(program.lets), **dict(program.state), **dict(context or {})})
178
+ body = "".join(render_node(node, env) for node in program.body)
179
+ title = html.escape(str(program.meta.get("title") or "TW Program"))
180
+
181
+ signature = compute_tw_signature(program)
182
+ banner = build_signature_banner(signature, title=title)
183
+ meta_tags = build_signature_meta_tag(signature)
184
+
185
+ html_output = (
186
+ "<!DOCTYPE html>"
187
+ f"{banner}"
188
+ "<html>"
189
+ "<head>"
190
+ f"<title>{title}</title>"
191
+ f"{meta_tags}"
192
+ "</head>"
193
+ f"<body>{body}</body>"
194
+ "</html>"
195
+ )
196
+
197
+ # Apply hydration markers for interactive components
198
+ html_output = wrap_interactive_nodes(html_output, program, context=context)
199
+ return html_output
200
+
201
+
202
+ def _inject_tw_signature(html_doc: str, banner: str, meta_tag: str) -> str:
203
+ """
204
+ Legacy renderer सीधे HTML string return करता है, IR tree नहीं, इसलिए
205
+ यहां हम एक best-effort post-processing करते हैं: document के सबसे ऊपर
206
+ एक comment banner डालते हैं, और अगर `<head>` tag मिल जाए तो उसके ठीक
207
+ बाद <meta> tags भी डाल देते हैं। अगर `<head>` न मिले, तो signature फिर
208
+ भी silently गायब नहीं होगी — banner के बाद meta tags जोड़ दी जाती हैं।
209
+ """
210
+ lowered = html_doc.lower()
211
+ head_idx = lowered.find("<head>")
212
+ if head_idx != -1:
213
+ insert_at = head_idx + len("<head>")
214
+ html_doc = html_doc[:insert_at] + "\n" + meta_tag + html_doc[insert_at:]
215
+ return banner + "\n" + html_doc
216
+ return banner + "\n" + meta_tag + "\n" + html_doc
217
+
218
+
219
+ def render_streaming(ir_program: IRProgram, *, page_program=None, context: Optional[Dict] = None, css_href: Optional[str] = None) -> Generator[str, None, None]:
220
+ """Yield HTML chunks for streaming SSR."""
221
+ from .streaming import render_program_streaming
222
+ yield from render_program_streaming(ir_program, context=context)
223
+
224
+
225
+ def render_program_document(ir_program: IRProgram, *, page_program=None, context: Optional[Dict] = None, css_href: Optional[str] = None, debug: bool = False) -> str:
226
+ compiler = _legacy()
227
+ merged_context = build_runtime_context(page_program, context=context) if page_program is not None else dict(context or {})
228
+
229
+ # Check if streaming is enabled via config
230
+ config = merged_context.get("config", {})
231
+ use_streaming = bool(config.get("streaming", False))
232
+
233
+ if use_streaming:
234
+ chunks = list(render_program_streaming(ir_program, context=merged_context))
235
+ html_output = "".join(chunks)
236
+ # Apply hydration markers
237
+ html_output = wrap_interactive_nodes(html_output, ir_program, context=merged_context)
238
+ return html_output
239
+
240
+ if page_program is not None and getattr(page_program, "legacy_page", None) is not None:
241
+ resolved_css = css_href
242
+ if resolved_css is None:
243
+ try:
244
+ resolved_css, _ = compiler.read_global_stylesheet()
245
+ except Exception:
246
+ logger.exception("Failed to read global stylesheet")
247
+ resolved_css = ""
248
+ raw_html = compiler.render_html(page_program.legacy_page, merged_context, resolved_css or "")
249
+
250
+ signature = compute_tw_signature(page_program)
251
+ title = str(getattr(page_program.meta, "title", "") or "")
252
+ banner = build_signature_banner(signature, title=title)
253
+ meta_tag = build_signature_meta_tag(signature)
254
+ return _inject_tw_signature(raw_html, banner, meta_tag)
255
+
256
+ return render_program(ir_program, context=merged_context)
257
+
258
+
259
+ __all__ = ["build_runtime_context", "render_node", "render_program", "render_program_document"]
@@ -0,0 +1,40 @@
1
+ """
2
+ Automatic route optimization for TW Framework.
3
+
4
+ Detects duplicate routes, unreachable routes, route conflicts, and shadowed routes.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict, List
9
+
10
+ from . import compiler
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def optimize_routes(project_root: str) -> Dict[str, List[str]]:
16
+ """Analyze routes and return optimization suggestions."""
17
+ issues = {
18
+ "duplicate_routes": [],
19
+ "unreachable_routes": [],
20
+ "route_conflicts": [],
21
+ "shadowed_routes": [],
22
+ }
23
+
24
+ pages = compiler.discover_pages()
25
+ route_map: Dict[str, List[str]] = {}
26
+ for page in pages:
27
+ route = compiler.route_path_from_page_info(page)
28
+ if route not in route_map:
29
+ route_map[route] = []
30
+ route_map[route].append(page["path"])
31
+
32
+ for route, paths in route_map.items():
33
+ if len(paths) > 1:
34
+ issues["duplicate_routes"].append({"route": route, "paths": paths})
35
+
36
+ # TODO: Add unreachable, conflict, and shadow detection
37
+ return issues
38
+
39
+
40
+ __all__ = ["optimize_routes"]
tw_framework/router.py ADDED
@@ -0,0 +1,7 @@
1
+ from .framework import ( # noqa: F401
2
+ RouteMatch,
3
+ normalize_url_path,
4
+ route_from_dynamic_page,
5
+ route_from_static_page,
6
+ strip_trailing_slash,
7
+ )
@@ -0,0 +1,23 @@
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 CompilerArtifacts:
9
+ source_path: str = ""
10
+ tokens: List[Dict[str, Any]] = field(default_factory=list)
11
+ ast: Optional[Dict[str, Any]] = None
12
+ diagnostics: List[Dict[str, Any]] = field(default_factory=list)
13
+ ir: Optional[Dict[str, Any]] = None
14
+ html: Optional[str] = None
15
+ program: Any = None
16
+ runtime_context: Dict[str, Any] = field(default_factory=dict)
17
+ dependencies: List[str] = field(default_factory=list)
18
+ route_path: str = "/"
19
+ pipeline: str = "modular"
20
+ metadata: Dict[str, Any] = field(default_factory=dict)
21
+
22
+
23
+ __all__ = ["CompilerArtifacts"]
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Dict, Optional
5
+
6
+
7
+ @dataclass
8
+ class RuntimeEnvironment:
9
+ values: Dict[str, Any] = field(default_factory=dict)
10
+ parent: Optional["RuntimeEnvironment"] = None
11
+
12
+ def get(self, name: str, default=None):
13
+ if name in self.values:
14
+ return self.values[name]
15
+ if self.parent is not None:
16
+ return self.parent.get(name, default)
17
+ return default
18
+
19
+ def set(self, name: str, value: Any):
20
+ self.values[name] = value
21
+
22
+ def to_context(self) -> Dict[str, Any]:
23
+ data = self.parent.to_context() if self.parent is not None else {}
24
+ data.update(self.values)
25
+ return data
26
+
27
+ def child(self, extra: Optional[Dict[str, Any]] = None) -> "RuntimeEnvironment":
28
+ return RuntimeEnvironment(values=dict(extra or {}), parent=self)
29
+
30
+
31
+ __all__ = ["RuntimeEnvironment"]
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Dict, Optional
5
+
6
+ from .ast_nodes import ComponentNode, ElementNode, ForNode, IfNode, LetNode, Program
7
+ from .diagnostics import Diagnostic, DiagnosticBag
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def _legacy():
13
+ from . import compiler
14
+
15
+ return compiler
16
+
17
+
18
+ class SemanticAnalyzer:
19
+ def _check_expression(self, expr, scope, diagnostics: DiagnosticBag, source_path: str, label: str):
20
+ compiler = _legacy()
21
+ try:
22
+ names = compiler.collect_expression_names(expr)
23
+ except Exception:
24
+ logger.exception("Failed to collect expression names (%s) in %s: %r", source_path, label, expr)
25
+ names = set()
26
+ for name in sorted(names):
27
+ if name not in scope:
28
+ diagnostics.add(
29
+ Diagnostic(
30
+ severity="error",
31
+ code="TW2101",
32
+ message=f"Unknown name `{name}` in {label}.",
33
+ file_path=source_path,
34
+ )
35
+ )
36
+
37
+ def _check_interpolations(self, text, scope, diagnostics: DiagnosticBag, source_path: str, label: str):
38
+ compiler = _legacy()
39
+ if not isinstance(text, str):
40
+ return
41
+ for expr in compiler.extract_placeholder_expressions(text):
42
+ self._check_expression(expr, scope, diagnostics, source_path, label)
43
+
44
+ def _walk_nodes(self, nodes, scope, diagnostics: DiagnosticBag, source_path: str):
45
+ next_scope = dict(scope)
46
+ for node in nodes:
47
+ if isinstance(node, LetNode):
48
+ next_scope[node.name] = node.value
49
+ self._check_interpolations(node.value, next_scope, diagnostics, source_path, f"`let {node.name}`")
50
+ continue
51
+ if isinstance(node, IfNode):
52
+ self._check_expression(node.condition, next_scope, diagnostics, source_path, "`if` condition")
53
+ self._walk_nodes(node.children, dict(next_scope), diagnostics, source_path)
54
+ self._walk_nodes(node.else_children, dict(next_scope), diagnostics, source_path)
55
+ continue
56
+ if isinstance(node, ForNode):
57
+ self._check_expression(node.iterable, next_scope, diagnostics, source_path, "`for` iterable")
58
+ child_scope = dict(next_scope)
59
+ child_scope[node.var_name] = True
60
+ self._walk_nodes(node.children, child_scope, diagnostics, source_path)
61
+ continue
62
+ if isinstance(node, ElementNode):
63
+ self._check_interpolations(node.text, next_scope, diagnostics, source_path, f"`{node.tag}` text")
64
+ for attr in node.attrs + node.styles + node.events:
65
+ self._check_interpolations(attr.value, next_scope, diagnostics, source_path, f"`{node.tag}` `{attr.name}`")
66
+ for value in node.router.values():
67
+ self._check_interpolations(value, next_scope, diagnostics, source_path, f"`{node.tag}` router")
68
+ self._walk_nodes(node.children, dict(next_scope), diagnostics, source_path)
69
+ continue
70
+ if isinstance(node, ComponentNode):
71
+ for prop in node.props:
72
+ self._check_interpolations(prop.value, next_scope, diagnostics, source_path, f"`{node.name}` prop `{prop.name}`")
73
+ self._walk_nodes(node.children, dict(next_scope), diagnostics, source_path)
74
+
75
+ def _check_page_meta(self, program: Program, diagnostics: DiagnosticBag, source_path: str):
76
+ meta = program.meta
77
+
78
+ valid_render_modes = {"static", "server", "edge"}
79
+ if meta.render_mode not in valid_render_modes:
80
+ diagnostics.add(
81
+ Diagnostic(
82
+ severity="error",
83
+ code="TW2201",
84
+ message=f'Invalid `render` mode "{meta.render_mode}". Use one of: static, server, edge.',
85
+ file_path=source_path,
86
+ )
87
+ )
88
+
89
+ if meta.render_mode == "static" and meta.revalidate is not None:
90
+ diagnostics.add(
91
+ Diagnostic(
92
+ severity="warning",
93
+ code="TW2202",
94
+ message="`revalidate` has no effect on `render static` pages. "
95
+ "Use `render server` or `render edge` for ISR-style revalidation.",
96
+ file_path=source_path,
97
+ )
98
+ )
99
+
100
+ if meta.revalidate is not None and meta.revalidate < 0:
101
+ diagnostics.add(
102
+ Diagnostic(
103
+ severity="error",
104
+ code="TW2203",
105
+ message=f"`revalidate` must be a non-negative number, got {meta.revalidate}.",
106
+ file_path=source_path,
107
+ )
108
+ )
109
+
110
+ if meta.redirect_to and meta.rewrite_to:
111
+ diagnostics.add(
112
+ Diagnostic(
113
+ severity="error",
114
+ code="TW2204",
115
+ message="A page cannot define both `redirect` and `rewrite` at the same time.",
116
+ file_path=source_path,
117
+ )
118
+ )
119
+
120
+ def analyze(self, program: Program, context: Optional[Dict] = None) -> DiagnosticBag:
121
+ diagnostics = DiagnosticBag()
122
+ compiler = _legacy()
123
+
124
+ if program.legacy_page and program.source_path:
125
+ try:
126
+ analysis_context = context or compiler.create_base_context(program.legacy_page, program.source_path)
127
+ except Exception:
128
+ logger.exception("Failed to create base context for %s", program.source_path)
129
+ analysis_context = context or {}
130
+ legacy_items = compiler.analyze_page_semantics(
131
+ program.legacy_page,
132
+ analysis_context,
133
+ program.source_path,
134
+ )
135
+ diagnostics.extend(Diagnostic.from_legacy(item) for item in legacy_items)
136
+
137
+ if not program.meta.title:
138
+ diagnostics.add(
139
+ Diagnostic(
140
+ severity="warning",
141
+ code="TW2001",
142
+ message='Page title is empty. Consider setting `page { title "..." }`.',
143
+ file_path=program.source_path,
144
+ )
145
+ )
146
+
147
+ self._check_page_meta(program, diagnostics, program.source_path)
148
+
149
+ scope = {}
150
+ scope.update(program.lets)
151
+ scope.update(program.state)
152
+ scope.update(context or {})
153
+ self._walk_nodes(program.body, scope, diagnostics, program.source_path)
154
+
155
+ return diagnostics
156
+
157
+
158
+ def analyze_program(program: Program, context: Optional[Dict] = None) -> DiagnosticBag:
159
+ return SemanticAnalyzer().analyze(program, context=context)
160
+
161
+
162
+ __all__ = ["SemanticAnalyzer", "analyze_program"]