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,24 @@
1
+ """
2
+ Partial rebuild support for TW Framework.
3
+
4
+ When only one page changes, rebuild only affected pages using the dependency graph.
5
+ """
6
+
7
+ import logging
8
+ from typing import List, Set
9
+
10
+ from . import compiler
11
+ from .dependency_graph import DependencyGraph
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def get_pages_to_rebuild(project_root: str, changed_files: Set[str]) -> List[dict]:
17
+ """Return the list of pages that need to be rebuilt given the changed files."""
18
+ graph = DependencyGraph(project_root)
19
+ affected = graph.get_affected_nodes(changed_files)
20
+ pages = compiler.discover_pages()
21
+ return [p for p in pages if p["path"] in affected]
22
+
23
+
24
+ __all__ = ["get_pages_to_rebuild"]
@@ -0,0 +1,37 @@
1
+ """
2
+ Runtime performance analyzer for TW Framework.
3
+
4
+ Measures render time, SSR time, middleware time, and API execution time.
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ from typing import Any, Dict, Optional
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class PerformanceAnalyzer:
15
+ """Collects runtime performance metrics."""
16
+
17
+ def __init__(self):
18
+ self.metrics: Dict[str, float] = {}
19
+
20
+ def measure(self, name: str, func, *args, **kwargs) -> Any:
21
+ start = time.perf_counter()
22
+ try:
23
+ result = func(*args, **kwargs)
24
+ return result
25
+ finally:
26
+ elapsed = time.perf_counter() - start
27
+ self.metrics[name] = elapsed
28
+ logger.debug("Performance: %s took %.4f seconds", name, elapsed)
29
+
30
+ def get_report(self) -> Dict[str, float]:
31
+ return dict(self.metrics)
32
+
33
+ def reset(self):
34
+ self.metrics.clear()
35
+
36
+
37
+ __all__ = ["PerformanceAnalyzer"]
@@ -0,0 +1,196 @@
1
+ import importlib.util
2
+ import logging
3
+ import os
4
+ import traceback
5
+ from dataclasses import dataclass
6
+
7
+ from .common import content_hash
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ HOOK_ALIASES = {
13
+ "before_build": "beforeBuild",
14
+ "after_build": "afterBuild",
15
+ "before_route": "beforeRoute",
16
+ "after_route": "afterRoute",
17
+ }
18
+
19
+
20
+ @dataclass
21
+ class LoadedExtension:
22
+ name: str
23
+ path: str
24
+ module: object
25
+
26
+
27
+ class PluginAPI:
28
+ def __init__(self, manager, extension_name):
29
+ self.manager = manager
30
+ self.extension_name = extension_name
31
+
32
+ def register_hook(self, event_name, handler):
33
+ self.manager.register_hook(event_name, handler, self.extension_name)
34
+
35
+ def hook(self, event_name, handler):
36
+ self.register_hook(event_name, handler)
37
+
38
+ def get_config(self):
39
+ return dict(self.manager.config or {})
40
+
41
+ def get_env(self):
42
+ return dict(self.manager.env or {})
43
+
44
+ def get_project_root(self):
45
+ return self.manager.project_root
46
+
47
+
48
+ class ExtensionManager:
49
+ def __init__(self, project_root, config=None, env=None):
50
+ self.project_root = os.path.abspath(project_root)
51
+ self.config = dict(config or {})
52
+ self.env = dict(env or {})
53
+ self.extensions = []
54
+ self.hooks = {}
55
+ self.errors = []
56
+
57
+ def refresh(self, config=None, env=None):
58
+ self.config = dict(config or self.config or {})
59
+ self.env = dict(env or self.env or {})
60
+ self.extensions = []
61
+ self.hooks = {}
62
+ self.errors = []
63
+ extension_paths = self.discover_extension_paths()
64
+ self._warn_extension_execution(extension_paths)
65
+ for path in extension_paths:
66
+ self._load_extension(path)
67
+ return self
68
+
69
+ def discover_extension_paths(self):
70
+ paths = []
71
+ for folder in self._extension_roots():
72
+ if not os.path.isdir(folder):
73
+ continue
74
+ for dirpath, _, filenames in os.walk(folder):
75
+ for filename in sorted(filenames):
76
+ if not filename.endswith(".py") or filename.startswith("_"):
77
+ continue
78
+ paths.append(os.path.join(dirpath, filename))
79
+ normalized = sorted(set(os.path.abspath(path) for path in paths))
80
+ return self._apply_allowlist(normalized)
81
+
82
+ def dependency_paths(self):
83
+ return self.discover_extension_paths()
84
+
85
+ def emit(self, event_name, **payload):
86
+ canonical = self._normalize_event_name(event_name)
87
+ event_payload = dict(payload)
88
+ event_payload.setdefault("event", canonical)
89
+ event_payload.setdefault("project_root", self.project_root)
90
+ event_payload.setdefault("config", dict(self.config))
91
+ event_payload.setdefault("env", dict(self.env))
92
+ for hook in self.hooks.get(canonical, []):
93
+ try:
94
+ result = hook["handler"](event_payload)
95
+ if isinstance(result, dict):
96
+ event_payload.update(result)
97
+ except Exception as err:
98
+ tb = traceback.format_exc()
99
+ logger.exception("Extension hook failed: %s::%s", hook.get("extension"), canonical)
100
+ self.errors.append(f"{hook['extension']}::{canonical}: {err}\n{tb}")
101
+ return event_payload
102
+
103
+ def register_hook(self, event_name, handler, extension_name):
104
+ canonical = self._normalize_event_name(event_name)
105
+ if not callable(handler):
106
+ raise TypeError(f"Hook `{canonical}` from `{extension_name}` must be callable")
107
+ self.hooks.setdefault(canonical, []).append({
108
+ "extension": extension_name,
109
+ "handler": handler,
110
+ })
111
+
112
+ def _extension_roots(self):
113
+ home_dir = os.path.join(self.project_root, "[home]")
114
+ return [
115
+ os.path.join(home_dir, "plugins"),
116
+ os.path.join(home_dir, "hooks"),
117
+ ]
118
+
119
+ def _normalize_event_name(self, event_name):
120
+ raw = str(event_name or "").strip()
121
+ return HOOK_ALIASES.get(raw, raw)
122
+
123
+ def _configured_allowlist(self):
124
+ raw = (
125
+ self.config.get("plugin_allowlist")
126
+ or self.config.get("plugins_allowlist")
127
+ or self.config.get("extension_allowlist")
128
+ or self.config.get("extensions_allowlist")
129
+ or []
130
+ )
131
+ if isinstance(raw, str):
132
+ raw = [raw]
133
+ if not isinstance(raw, (list, tuple, set)):
134
+ return set()
135
+ return {
136
+ str(item).strip().replace("\\", "/")
137
+ for item in raw
138
+ if str(item).strip()
139
+ }
140
+
141
+ def _apply_allowlist(self, paths):
142
+ allowlist = self._configured_allowlist()
143
+ if not allowlist:
144
+ return paths
145
+ allowed = []
146
+ blocked = []
147
+ for path in paths:
148
+ rel = os.path.relpath(path, self.project_root).replace("\\", "/")
149
+ base = os.path.basename(path)
150
+ if rel in allowlist or base in allowlist:
151
+ allowed.append(path)
152
+ else:
153
+ blocked.append(rel)
154
+ if blocked:
155
+ logger.warning(
156
+ "Skipping non-allowlisted extensions: %s",
157
+ ", ".join(sorted(blocked)),
158
+ )
159
+ return allowed
160
+
161
+ def _warn_extension_execution(self, paths):
162
+ if not paths:
163
+ return
164
+ rendered = ", ".join(
165
+ sorted(os.path.relpath(path, self.project_root).replace("\\", "/") for path in paths)
166
+ )
167
+ if self._configured_allowlist():
168
+ logger.warning("Executing allowlisted TW extensions: %s", rendered)
169
+ else:
170
+ logger.warning(
171
+ "Executing TW extensions from project code: %s. Review these files carefully or configure `plugin_allowlist` in `tw.config`.",
172
+ rendered,
173
+ )
174
+
175
+ def _load_extension(self, path):
176
+ name = os.path.splitext(os.path.basename(path))[0]
177
+ module_name = f"tw_ext_{name}_{content_hash(path, length=10)}"
178
+ spec = importlib.util.spec_from_file_location(module_name, path)
179
+ if spec is None or spec.loader is None:
180
+ self.errors.append(f"{path}: failed to create import spec")
181
+ return
182
+ module = importlib.util.module_from_spec(spec)
183
+ try:
184
+ spec.loader.exec_module(module)
185
+ api = PluginAPI(self, name)
186
+ if hasattr(module, "register") and callable(module.register):
187
+ module.register(api)
188
+ hooks = getattr(module, "hooks", None)
189
+ if isinstance(hooks, dict):
190
+ for event_name, handler in hooks.items():
191
+ api.register_hook(event_name, handler)
192
+ self.extensions.append(LoadedExtension(name=name, path=path, module=module))
193
+ except Exception as err:
194
+ tb = traceback.format_exc()
195
+ logger.exception("Failed to load extension: %s", path)
196
+ self.errors.append(f"{path}: {err}\n{tb}")
@@ -0,0 +1,82 @@
1
+ """
2
+ Production optimization for TW Framework.
3
+
4
+ Generates production output with minified HTML, CSS, JS, compressed assets,
5
+ and hashed filenames.
6
+ """
7
+
8
+ import gzip
9
+ import hashlib
10
+ import logging
11
+ import os
12
+ import shutil
13
+ from typing import List, Optional
14
+
15
+ from . import compiler
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def optimize_for_production(output_dir: str):
21
+ """Apply production optimizations to the output directory."""
22
+ # Minify HTML files
23
+ for root, _, files in os.walk(output_dir):
24
+ for fname in files:
25
+ if fname.endswith(".html"):
26
+ path = os.path.join(root, fname)
27
+ with open(path, "r", encoding="utf-8") as f:
28
+ content = f.read()
29
+ minified = compiler.minify_html_content(content)
30
+ with open(path, "w", encoding="utf-8") as f:
31
+ f.write(minified)
32
+
33
+ # Minify CSS files
34
+ for root, _, files in os.walk(output_dir):
35
+ for fname in files:
36
+ if fname.endswith(".css"):
37
+ path = os.path.join(root, fname)
38
+ with open(path, "r", encoding="utf-8") as f:
39
+ content = f.read()
40
+ minified = compiler.minify_css_content(content)
41
+ with open(path, "w", encoding="utf-8") as f:
42
+ f.write(minified)
43
+
44
+ # Minify JS files
45
+ for root, _, files in os.walk(output_dir):
46
+ for fname in files:
47
+ if fname.endswith(".js"):
48
+ path = os.path.join(root, fname)
49
+ with open(path, "r", encoding="utf-8") as f:
50
+ content = f.read()
51
+ minified = compiler.minify_js_content(content)
52
+ with open(path, "w", encoding="utf-8") as f:
53
+ f.write(minified)
54
+
55
+ # Gzip compress files
56
+ for root, _, files in os.walk(output_dir):
57
+ for fname in files:
58
+ if fname.endswith((".html", ".css", ".js", ".json", ".xml", ".txt", ".svg")):
59
+ path = os.path.join(root, fname)
60
+ with open(path, "rb") as f:
61
+ data = f.read()
62
+ with gzip.open(path + ".gz", "wb", compresslevel=9) as gz:
63
+ gz.write(data)
64
+
65
+ # Hash filenames for cache busting
66
+ for root, _, files in os.walk(output_dir):
67
+ for fname in files:
68
+ if fname.endswith((".css", ".js")):
69
+ path = os.path.join(root, fname)
70
+ with open(path, "rb") as f:
71
+ data = f.read()
72
+ digest = hashlib.sha256(data).hexdigest()[:12]
73
+ name, ext = os.path.splitext(fname)
74
+ hashed_name = f"{name}.{digest}{ext}"
75
+ hashed_path = os.path.join(root, hashed_name)
76
+ if not os.path.exists(hashed_path):
77
+ shutil.copy2(path, hashed_path)
78
+ # Remove original file
79
+ os.remove(path)
80
+
81
+
82
+ __all__ = ["optimize_for_production"]
@@ -0,0 +1,349 @@
1
+ """
2
+ TW Client Reactivity System
3
+ Generates a lightweight JS runtime injected into pages that use:
4
+ - state { ... } -> reactive variables
5
+ - bind:value -> two-way input binding
6
+ - on:click / on:input -> event handlers
7
+ - show:condition -> conditional display
8
+ - tw-for / tw-each -> client-side list rendering (from JSON endpoint or inline array)
9
+ - tw-ref -> DOM refs
10
+
11
+ This module:
12
+ 1. Parses .tw page/component nodes and detects reactivity usage
13
+ 2. Emits a minimal vanilla-JS runtime (~2KB gzipped) per page
14
+ 3. Hooks into the compiler's render pipeline via page-level `script {}` injection
15
+
16
+ No framework deps. No virtual DOM. Direct DOM updates.
17
+ """
18
+
19
+ import json
20
+ import re
21
+
22
+ # ─── Runtime JS (injected once per page that uses reactivity) ─────────────────
23
+
24
+ _TW_REACTIVE_RUNTIME = r"""
25
+ (function() {
26
+ 'use strict';
27
+
28
+ // ── State store ──────────────────────────────────────────────────────────────
29
+ var __twState = {};
30
+ var __twWatchers = {};
31
+ var __twRefs = {};
32
+
33
+ function __twDefineState(initialState) {
34
+ Object.keys(initialState).forEach(function(key) {
35
+ __twState[key] = initialState[key];
36
+ });
37
+ __twSync();
38
+ }
39
+
40
+ function __twSet(key, value) {
41
+ if (__twState[key] === value) return;
42
+ __twState[key] = value;
43
+ (__twWatchers[key] || []).forEach(function(fn) { try { fn(value); } catch(e){} });
44
+ __twSync();
45
+ }
46
+
47
+ function __twGet(key) {
48
+ return __twState[key];
49
+ }
50
+
51
+ function __twWatch(key, fn) {
52
+ if (!__twWatchers[key]) __twWatchers[key] = [];
53
+ __twWatchers[key].push(fn);
54
+ }
55
+
56
+ // ── DOM sync ──────────────────────────────────────────────────────────────────
57
+ function __twSync() {
58
+ // bind:value — sync input values
59
+ document.querySelectorAll('[data-tw-bind]').forEach(function(el) {
60
+ var key = el.getAttribute('data-tw-bind');
61
+ if (el !== document.activeElement && key in __twState) {
62
+ if (el.type === 'checkbox') {
63
+ el.checked = !!__twState[key];
64
+ } else {
65
+ el.value = __twState[key] !== null && __twState[key] !== undefined ? __twState[key] : '';
66
+ }
67
+ }
68
+ });
69
+
70
+ // show: — toggle visibility
71
+ document.querySelectorAll('[data-tw-show]').forEach(function(el) {
72
+ var expr = el.getAttribute('data-tw-show');
73
+ var val = __twEval(expr);
74
+ el.style.display = val ? '' : 'none';
75
+ });
76
+
77
+ // tw-text — update text content
78
+ document.querySelectorAll('[data-tw-text]').forEach(function(el) {
79
+ var expr = el.getAttribute('data-tw-text');
80
+ var val = __twEval(expr);
81
+ el.textContent = val !== null && val !== undefined ? String(val) : '';
82
+ });
83
+
84
+ // tw-html — update inner HTML (escaped)
85
+ document.querySelectorAll('[data-tw-html]').forEach(function(el) {
86
+ var expr = el.getAttribute('data-tw-html');
87
+ var val = __twEval(expr);
88
+ el.innerHTML = val !== null && val !== undefined ? String(val) : '';
89
+ });
90
+
91
+ // class: — conditional class
92
+ document.querySelectorAll('[data-tw-class]').forEach(function(el) {
93
+ try {
94
+ var pairs = JSON.parse(el.getAttribute('data-tw-class'));
95
+ Object.keys(pairs).forEach(function(cls) {
96
+ el.classList.toggle(cls, !!__twEval(pairs[cls]));
97
+ });
98
+ } catch(e) {}
99
+ });
100
+
101
+ // tw-for — client-side list rendering
102
+ document.querySelectorAll('[data-tw-for]').forEach(function(container) {
103
+ var expr = container.getAttribute('data-tw-for');
104
+ var tmpl = container.querySelector('[data-tw-for-template]');
105
+ if (!tmpl) return;
106
+ var items = __twEval(expr);
107
+ if (!Array.isArray(items)) return;
108
+ var existing = container.querySelectorAll('[data-tw-for-item]');
109
+ existing.forEach(function(el) { el.remove(); });
110
+ items.forEach(function(item, idx) {
111
+ var clone = tmpl.cloneNode(true);
112
+ clone.removeAttribute('data-tw-for-template');
113
+ clone.setAttribute('data-tw-for-item', idx);
114
+ clone.style.display = '';
115
+ // Interpolate {item.*} / {item} in text nodes and attributes
116
+ __twInterpolateNode(clone, { item: item, index: idx });
117
+ container.appendChild(clone);
118
+ });
119
+ });
120
+ }
121
+
122
+ // ── Safe expression evaluator ────────────────────────────────────────────────
123
+ function __twEval(expr) {
124
+ try {
125
+ var fn = new Function(
126
+ Object.keys(__twState).join(','),
127
+ 'try { return (' + expr + '); } catch(e) { return undefined; }'
128
+ );
129
+ return fn.apply(null, Object.keys(__twState).map(function(k) { return __twState[k]; }));
130
+ } catch(e) {
131
+ return undefined;
132
+ }
133
+ }
134
+
135
+ // ── Text interpolation for tw-for clones ─────────────────────────────────────
136
+ function __twInterpolateNode(root, ctx) {
137
+ var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
138
+ var node;
139
+ while ((node = walker.nextNode())) {
140
+ node.textContent = node.textContent.replace(/\{([^{}]+)\}/g, function(_, expr) {
141
+ try {
142
+ var fn = new Function(Object.keys(ctx).join(','), 'try{return('+expr+');}catch(e){return "";}');
143
+ var v = fn.apply(null, Object.keys(ctx).map(function(k){return ctx[k];}));
144
+ return v !== null && v !== undefined ? v : '';
145
+ } catch(e) { return ''; }
146
+ });
147
+ }
148
+ root.querySelectorAll('*').forEach(function(el) {
149
+ Array.from(el.attributes).forEach(function(attr) {
150
+ if (attr.name.startsWith('data-tw-')) return;
151
+ attr.value = attr.value.replace(/\{([^{}]+)\}/g, function(_, expr) {
152
+ try {
153
+ var fn = new Function(Object.keys(ctx).join(','), 'try{return('+expr+');}catch(e){return "";}');
154
+ var v = fn.apply(null, Object.keys(ctx).map(function(k){return ctx[k];}));
155
+ return v !== null && v !== undefined ? v : '';
156
+ } catch(e) { return ''; }
157
+ });
158
+ });
159
+ });
160
+ }
161
+
162
+ // ── Event binding ─────────────────────────────────────────────────────────────
163
+ function __twBindEvents() {
164
+ // bind:value -> two-way binding
165
+ document.querySelectorAll('[data-tw-bind]').forEach(function(el) {
166
+ if (el.__twBound) return;
167
+ el.__twBound = true;
168
+ var key = el.getAttribute('data-tw-bind');
169
+ var eventName = (el.type === 'checkbox' || el.tagName === 'SELECT') ? 'change' : 'input';
170
+ el.addEventListener(eventName, function() {
171
+ var val = el.type === 'checkbox' ? el.checked : el.value;
172
+ __twSet(key, val);
173
+ });
174
+ });
175
+
176
+ // on:* -> event handlers
177
+ document.querySelectorAll('[data-tw-on]').forEach(function(el) {
178
+ if (el.__twOnBound) return;
179
+ el.__twOnBound = true;
180
+ try {
181
+ var handlers = JSON.parse(el.getAttribute('data-tw-on'));
182
+ Object.keys(handlers).forEach(function(eventName) {
183
+ el.addEventListener(eventName, function(event) {
184
+ var expr = handlers[eventName];
185
+ try {
186
+ var fn = new Function(
187
+ Object.keys(__twState).concat(['event', '__twSet', '__twGet']).join(','),
188
+ expr
189
+ );
190
+ fn.apply(null, Object.keys(__twState).map(function(k) { return __twState[k]; }).concat([event, __twSet, __twGet]));
191
+ } catch(e) { console.error('TW event error:', e); }
192
+ __twSync();
193
+ });
194
+ });
195
+ } catch(e) {}
196
+ });
197
+
198
+ // tw-ref
199
+ document.querySelectorAll('[data-tw-ref]').forEach(function(el) {
200
+ __twRefs[el.getAttribute('data-tw-ref')] = el;
201
+ });
202
+ }
203
+
204
+ // ── Fetch helper for API calls ───────────────────────────────────────────────
205
+ window.__twFetch = async function(url, options) {
206
+ options = options || {};
207
+ var method = (options.method || 'GET').toUpperCase();
208
+ var headers = Object.assign({ 'Content-Type': 'application/json' }, options.headers || {});
209
+ var body = options.body !== undefined ? JSON.stringify(options.body) : undefined;
210
+ try {
211
+ var res = await fetch(url, { method: method, headers: headers, body: body });
212
+ var ct = res.headers.get('content-type') || '';
213
+ var data = ct.includes('application/json') ? await res.json() : await res.text();
214
+ return { ok: res.ok, status: res.status, data: data };
215
+ } catch(e) {
216
+ return { ok: false, status: 0, data: null, error: e.message };
217
+ }
218
+ };
219
+
220
+ // ── Public API ────────────────────────────────────────────────────────────────
221
+ window.__tw = {
222
+ state: __twState,
223
+ set: __twSet,
224
+ get: __twGet,
225
+ watch: __twWatch,
226
+ sync: __twSync,
227
+ refs: __twRefs,
228
+ eval: __twEval,
229
+ fetch: __twFetch,
230
+ };
231
+
232
+ // ── Init on DOM ready ─────────────────────────────────────────────────────────
233
+ function __twInit() {
234
+ __twBindEvents();
235
+ __twSync();
236
+ // Re-bind after dynamic DOM changes (tw-for, etc.)
237
+ var observer = new MutationObserver(function() {
238
+ __twBindEvents();
239
+ });
240
+ observer.observe(document.body, { childList: true, subtree: true });
241
+ }
242
+
243
+ if (document.readyState === 'loading') {
244
+ document.addEventListener('DOMContentLoaded', __twInit);
245
+ } else {
246
+ __twInit();
247
+ }
248
+
249
+ })();
250
+ """
251
+
252
+ # ─── Compiler directive parsers ───────────────────────────────────────────────
253
+
254
+ _STATE_BLOCK_RE = re.compile(r'\bstate\s*\{([^}]*)\}', re.DOTALL)
255
+ _STATE_KV_RE = re.compile(r'(\w+)\s+(.+?)(?=\n\s*\w|\Z)', re.DOTALL)
256
+
257
+
258
+ def parse_state_block(source: str) -> dict:
259
+ """
260
+ Parse:
261
+ state {
262
+ count 0
263
+ name "hello"
264
+ items []
265
+ }
266
+ Returns {"count": 0, "name": "hello", "items": []}
267
+ """
268
+ state = {}
269
+ for block_match in _STATE_BLOCK_RE.finditer(source):
270
+ body = block_match.group(1)
271
+ for kv in _STATE_KV_RE.finditer(body.strip()):
272
+ key = kv.group(1).strip()
273
+ raw = kv.group(2).strip()
274
+ from . import compiler as _c
275
+ val = _c.parse_literal_value(raw)
276
+ # Strip outer quotes from string values
277
+ if isinstance(val, str) and len(val) >= 2 and val[0] == val[-1] and val[0] in ('"', "'"):
278
+ val = val[1:-1]
279
+ state[key] = val
280
+ return state
281
+
282
+
283
+ def has_reactivity(source: str) -> bool:
284
+ """Quick check: does this .tw file use any reactive features?"""
285
+ patterns = [
286
+ r'\bstate\s*\{',
287
+ r'\bbind:',
288
+ r'\bon:',
289
+ r'\bshow:',
290
+ r'\btw-for\b',
291
+ r'\btw-text\b',
292
+ r'\btw-ref\b',
293
+ ]
294
+ return any(re.search(p, source) for p in patterns)
295
+
296
+
297
+ def build_state_init_script(state: dict) -> str:
298
+ """Generates the inline script that seeds __twDefineState."""
299
+ if not state:
300
+ return ""
301
+ return f"__twDefineState({json.dumps(state, ensure_ascii=False)});"
302
+
303
+
304
+ def get_reactivity_runtime_js() -> str:
305
+ return _TW_REACTIVE_RUNTIME
306
+
307
+
308
+ # ─── HTML attribute transformer ───────────────────────────────────────────────
309
+ # These convert .tw reactive directives to data-tw-* HTML attributes
310
+ # so the runtime JS can pick them up.
311
+
312
+ def transform_reactive_attrs(attrs: list) -> list:
313
+ """
314
+ Convert:
315
+ bind:value "count" -> data-tw-bind="count"
316
+ show:visible "count > 0" -> data-tw-show="count > 0"
317
+ on:click "count++" -> data-tw-on='{"click":"count++"}'
318
+ tw-ref "myInput" -> data-tw-ref="myInput"
319
+ tw-text "count" -> data-tw-text="count"
320
+ """
321
+ out = []
322
+ on_handlers = {}
323
+ for name, value in attrs:
324
+ nl = name.lower()
325
+ if nl.startswith("bind:"):
326
+ out.append(("data-tw-bind", value))
327
+ elif nl.startswith("show:"):
328
+ out.append(("data-tw-show", value))
329
+ elif nl.startswith("on:"):
330
+ event = nl[3:]
331
+ on_handlers[event] = value
332
+ elif nl in {"tw-ref", "tw:ref"}:
333
+ out.append(("data-tw-ref", value))
334
+ elif nl in {"tw-text", "tw:text"}:
335
+ out.append(("data-tw-text", value))
336
+ elif nl in {"tw-html", "tw:html"}:
337
+ out.append(("data-tw-html", value))
338
+ elif nl in {"tw-for", "tw:for"}:
339
+ out.append(("data-tw-for", value))
340
+ elif nl in {"tw-class", "tw:class"}:
341
+ # Expect JSON string: '{"active": "isActive", "hidden": "!show"}'
342
+ out.append(("data-tw-class", value))
343
+ else:
344
+ out.append((name, value))
345
+
346
+ if on_handlers:
347
+ out.append(("data-tw-on", json.dumps(on_handlers)))
348
+
349
+ return out