tw-framework 0.5.2__tar.gz → 0.6.2__tar.gz
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-0.5.2 → tw_framework-0.6.2}/PKG-INFO +1 -1
- {tw_framework-0.5.2 → tw_framework-0.6.2}/pyproject.toml +3 -3
- tw_framework-0.6.2/tests/test_component_classifier.py +90 -0
- tw_framework-0.6.2/tests/test_error_boundaries.py +75 -0
- tw_framework-0.6.2/tests/test_js_interop.py +68 -0
- tw_framework-0.6.2/tests/test_module_boundaries.py +164 -0
- tw_framework-0.6.2/tests/test_runtime_loader.py +96 -0
- tw_framework-0.6.2/tests/test_server_actions.py +119 -0
- tw_framework-0.6.2/tests/test_tw_auth.py +180 -0
- tw_framework-0.6.2/tests/test_tw_fetch.py +76 -0
- tw_framework-0.6.2/tests/test_tw_form.py +147 -0
- tw_framework-0.6.2/tests/test_tw_realtime.py +104 -0
- tw_framework-0.6.2/tests/test_tw_router.py +101 -0
- tw_framework-0.6.2/tests/test_tw_state.py +118 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/__init__.py +1 -1
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/compiler.py +23 -0
- tw_framework-0.6.2/tw_framework/component_classifier.py +216 -0
- tw_framework-0.6.2/tw_framework/error_boundaries.py +254 -0
- tw_framework-0.6.2/tw_framework/js_interop.py +293 -0
- tw_framework-0.6.2/tw_framework/module_boundaries.py +276 -0
- tw_framework-0.6.2/tw_framework/runtime_loader.py +663 -0
- tw_framework-0.6.2/tw_framework/server_actions.py +226 -0
- tw_framework-0.6.2/tw_framework/tw_auth/__init__.py +11 -0
- tw_framework-0.6.2/tw_framework/tw_auth/client.py +46 -0
- tw_framework-0.6.2/tw_framework/tw_auth/middleware.py +169 -0
- tw_framework-0.6.2/tw_framework/tw_auth/runtime.py +79 -0
- tw_framework-0.6.2/tw_framework/tw_auth/session.py +148 -0
- tw_framework-0.6.2/tw_framework/tw_fetch/__init__.py +5 -0
- tw_framework-0.6.2/tw_framework/tw_fetch/fetch.py +142 -0
- tw_framework-0.6.2/tw_framework/tw_fetch/runtime.py +58 -0
- tw_framework-0.6.2/tw_framework/tw_font/__init__.py +4 -0
- tw_framework-0.6.2/tw_framework/tw_font/loader.py +70 -0
- tw_framework-0.6.2/tw_framework/tw_form/__init__.py +6 -0
- tw_framework-0.6.2/tw_framework/tw_form/form.py +118 -0
- tw_framework-0.6.2/tw_framework/tw_form/runtime.py +136 -0
- tw_framework-0.6.2/tw_framework/tw_form/validation.py +139 -0
- tw_framework-0.6.2/tw_framework/tw_image/__init__.py +10 -0
- tw_framework-0.6.2/tw_framework/tw_image/cache.py +42 -0
- tw_framework-0.6.2/tw_framework/tw_image/component.py +144 -0
- tw_framework-0.6.2/tw_framework/tw_image/formats.py +21 -0
- tw_framework-0.6.2/tw_framework/tw_image/optimizer.py +67 -0
- tw_framework-0.6.2/tw_framework/tw_metadata/__init__.py +4 -0
- tw_framework-0.6.2/tw_framework/tw_metadata/meta.py +137 -0
- tw_framework-0.6.2/tw_framework/tw_realtime/__init__.py +6 -0
- tw_framework-0.6.2/tw_framework/tw_realtime/client.py +73 -0
- tw_framework-0.6.2/tw_framework/tw_realtime/runtime.py +85 -0
- tw_framework-0.6.2/tw_framework/tw_realtime/server.py +167 -0
- tw_framework-0.6.2/tw_framework/tw_router/__init__.py +5 -0
- tw_framework-0.6.2/tw_framework/tw_router/router.py +114 -0
- tw_framework-0.6.2/tw_framework/tw_router/runtime.py +99 -0
- tw_framework-0.6.2/tw_framework/tw_state/__init__.py +21 -0
- tw_framework-0.6.2/tw_framework/tw_state/runtime.py +105 -0
- tw_framework-0.6.2/tw_framework/tw_state/store.py +163 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/PKG-INFO +1 -1
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/SOURCES.txt +50 -1
- {tw_framework-0.5.2 → tw_framework-0.6.2}/LICENSE +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/README.md +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/setup.cfg +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_cache_invalidation.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_deployment.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_inline_json_and_tailwind.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_lib_system.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_parallel_compilation.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_tw_image.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_type_safety.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_zero_js.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/adapters/__init__.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/adapters/cloudflare.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/adapters/netlify.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/adapters/vercel.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/adapters/vercel_functions.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/advanced_diagnostics.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/asset_optimizer.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/ast_nodes.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/build.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/build_performance.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/build_report.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/cli.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/code_splitting.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/common.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/compiler_stats.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/dead_code.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/dependency_graph.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/deploy.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/dev.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/diagnostics.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/dynamic_imports.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/error_formatter.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/framework.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/hydration.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/incremental_cache.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/interpreter.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/ir.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/lexer.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/lib_executor.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/lowering.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/lsp_server.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/parser.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/partial_rebuild.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/performance_analyzer.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/plugin_runtime.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/production_optimizer.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/reactivity.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/render_css.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/render_html.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/route_optimizer.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/router.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/runtime_model.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/runtime_values.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/semantic.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/server.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/signature.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/static_dynamic_auto.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/streaming.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/tailwind_map.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/test_build_cli_pytest.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/tree_shaking.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/twm_api_runner.js +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/twm_parser.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/websocket.py +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/dependency_links.txt +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/entry_points.txt +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/requires.txt +0 -0
- {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tw-framework"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.2"
|
|
8
8
|
description = "TW framework CLI with create, dev, build, and deploy commands"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -26,8 +26,8 @@ dev = [
|
|
|
26
26
|
[project.scripts]
|
|
27
27
|
tw = "tw_framework.cli:main"
|
|
28
28
|
|
|
29
|
-
[tool.setuptools]
|
|
30
|
-
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
include = ["tw_framework*"]
|
|
31
31
|
|
|
32
32
|
[tool.setuptools.package-data]
|
|
33
33
|
tw_framework = ["*.js"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Tests for the component classifier."""
|
|
2
|
+
import pytest
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
7
|
+
|
|
8
|
+
from tw_framework.component_classifier import (
|
|
9
|
+
ComponentClassifier, ComponentClassification, STATIC,
|
|
10
|
+
CLIENT, SERVER,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestComponentClassification:
|
|
15
|
+
def test_static_component(self):
|
|
16
|
+
c = ComponentClassifier()
|
|
17
|
+
source = """div { class "card" } {
|
|
18
|
+
h1 { "Hello World" }
|
|
19
|
+
p { "This is static content" }
|
|
20
|
+
}"""
|
|
21
|
+
result = c.classify("Card", source)
|
|
22
|
+
assert result.classification == STATIC
|
|
23
|
+
assert result.needs_client_js is False
|
|
24
|
+
|
|
25
|
+
def test_client_component_with_state(self):
|
|
26
|
+
c = ComponentClassifier()
|
|
27
|
+
source = 'state { count 0 }\nbutton { on:click "count++" } { "{count}" }'
|
|
28
|
+
result = c.classify("Counter", source)
|
|
29
|
+
assert result.classification == CLIENT
|
|
30
|
+
assert result.needs_client_js is True
|
|
31
|
+
assert result.needs_state is True
|
|
32
|
+
|
|
33
|
+
def test_client_component_with_events(self):
|
|
34
|
+
c = ComponentClassifier()
|
|
35
|
+
source = 'button { on:click "submit" } { "Submit" }'
|
|
36
|
+
result = c.classify("SubmitButton", source)
|
|
37
|
+
assert result.classification == CLIENT
|
|
38
|
+
assert result.needs_client_js is True
|
|
39
|
+
|
|
40
|
+
def test_client_component_with_bind(self):
|
|
41
|
+
c = ComponentClassifier()
|
|
42
|
+
source = 'input { bind:value "name" }'
|
|
43
|
+
result = c.classify("Input", source)
|
|
44
|
+
assert result.classification == CLIENT
|
|
45
|
+
|
|
46
|
+
def test_client_component_with_tw_state_import(self):
|
|
47
|
+
c = ComponentClassifier()
|
|
48
|
+
source = 'import { store } from "tw/state"'
|
|
49
|
+
result = c.classify("StateComp", source)
|
|
50
|
+
assert result.classification == CLIENT
|
|
51
|
+
assert result.needs_state is True
|
|
52
|
+
|
|
53
|
+
def test_client_component_with_tw_router_import(self):
|
|
54
|
+
c = ComponentClassifier()
|
|
55
|
+
source = 'import { Link } from "tw/router"'
|
|
56
|
+
result = c.classify("NavLink", source)
|
|
57
|
+
assert result.classification == CLIENT
|
|
58
|
+
assert result.needs_router is True
|
|
59
|
+
|
|
60
|
+
def test_client_component_with_tw_realtime_import(self):
|
|
61
|
+
c = ComponentClassifier()
|
|
62
|
+
source = 'import { socket } from "tw/realtime"'
|
|
63
|
+
result = c.classify("LiveCounter", source)
|
|
64
|
+
assert result.classification == CLIENT
|
|
65
|
+
assert result.needs_realtime is True
|
|
66
|
+
|
|
67
|
+
def test_explicit_static(self):
|
|
68
|
+
c = ComponentClassifier()
|
|
69
|
+
source = 'static true\nbutton { on:click "x" } { "Click" }'
|
|
70
|
+
result = c.classify("ForcedStatic", source)
|
|
71
|
+
assert result.classification == STATIC
|
|
72
|
+
assert result.needs_client_js is False
|
|
73
|
+
|
|
74
|
+
def test_explicit_client(self):
|
|
75
|
+
c = ComponentClassifier()
|
|
76
|
+
source = 'client true\nh1 { "Hello" }'
|
|
77
|
+
result = c.classify("ForcedClient", source)
|
|
78
|
+
assert result.classification == CLIENT
|
|
79
|
+
assert result.needs_client_js is True
|
|
80
|
+
|
|
81
|
+
def test_classify_page(self):
|
|
82
|
+
c = ComponentClassifier()
|
|
83
|
+
source = """page { title "Home" }
|
|
84
|
+
state { count 0 }
|
|
85
|
+
div {
|
|
86
|
+
button { on:click "count++" } { "Count: {count}" }
|
|
87
|
+
}"""
|
|
88
|
+
result = c.classify_page(source)
|
|
89
|
+
assert result.classification == CLIENT
|
|
90
|
+
assert result.needs_state is True
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Tests for error boundaries."""
|
|
2
|
+
import pytest
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
7
|
+
|
|
8
|
+
from tw_framework.error_boundaries import (
|
|
9
|
+
ErrorInfo, render_error_page, render_404, render_500,
|
|
10
|
+
render_error_from_exception, render_loading, get_error_boundary_js,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestErrorPages:
|
|
15
|
+
def test_render_404(self):
|
|
16
|
+
html = render_404("/nonexistent")
|
|
17
|
+
assert "404" in html
|
|
18
|
+
assert "Not Found" in html
|
|
19
|
+
|
|
20
|
+
def test_render_500(self):
|
|
21
|
+
html = render_500("Something broke")
|
|
22
|
+
assert "500" in html
|
|
23
|
+
assert "Server Error" in html
|
|
24
|
+
|
|
25
|
+
def test_render_404_production_safe(self):
|
|
26
|
+
html = render_404("/path", is_dev=False)
|
|
27
|
+
assert "does not exist" in html
|
|
28
|
+
assert "Stack Trace" not in html
|
|
29
|
+
|
|
30
|
+
def test_render_error_with_dev_details(self):
|
|
31
|
+
error = ErrorInfo(
|
|
32
|
+
code=500,
|
|
33
|
+
title="Test Error",
|
|
34
|
+
message="Something went wrong",
|
|
35
|
+
stack="Error: at line 1\n at test()",
|
|
36
|
+
is_dev=True,
|
|
37
|
+
)
|
|
38
|
+
html = render_error_page(error)
|
|
39
|
+
assert "Stack Trace" in html
|
|
40
|
+
assert "Error: at line 1" in html
|
|
41
|
+
|
|
42
|
+
def test_render_error_production_no_stack(self):
|
|
43
|
+
error = ErrorInfo(
|
|
44
|
+
code=500,
|
|
45
|
+
title="Test Error",
|
|
46
|
+
message="Internal error",
|
|
47
|
+
stack="secret stack trace",
|
|
48
|
+
is_dev=False,
|
|
49
|
+
)
|
|
50
|
+
html = render_error_page(error)
|
|
51
|
+
assert "secret stack trace" not in html
|
|
52
|
+
|
|
53
|
+
def test_render_error_from_exception(self):
|
|
54
|
+
try:
|
|
55
|
+
raise ValueError("Test error")
|
|
56
|
+
except ValueError as e:
|
|
57
|
+
html = render_error_from_exception(e, is_dev=True)
|
|
58
|
+
assert "ValueError" in html
|
|
59
|
+
assert "Test error" in html
|
|
60
|
+
|
|
61
|
+
def test_render_loading(self):
|
|
62
|
+
html = render_loading()
|
|
63
|
+
assert "tw-loading" in html
|
|
64
|
+
|
|
65
|
+
def test_get_error_boundary_js(self):
|
|
66
|
+
js = get_error_boundary_js()
|
|
67
|
+
assert "__tw.errorBoundary" in js
|
|
68
|
+
assert "catch" in js
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestErrorInfo:
|
|
72
|
+
def test_error_info_defaults(self):
|
|
73
|
+
info = ErrorInfo()
|
|
74
|
+
assert info.code == 500
|
|
75
|
+
assert info.is_dev is False
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Tests for JS/NPM interop."""
|
|
2
|
+
import pytest
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
7
|
+
|
|
8
|
+
from tw_framework.js_interop import JSInterop, NPMPackage
|
|
9
|
+
from tw_framework.module_boundaries import CLIENT, SERVER
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestNPMPackageResolution:
|
|
13
|
+
def test_client_safe_package(self):
|
|
14
|
+
interop = JSInterop()
|
|
15
|
+
pkg = interop.resolve_npm_package("chart.js")
|
|
16
|
+
assert pkg is not None
|
|
17
|
+
assert pkg.boundary == CLIENT
|
|
18
|
+
|
|
19
|
+
def test_server_only_package(self):
|
|
20
|
+
interop = JSInterop()
|
|
21
|
+
pkg = interop.resolve_npm_package("express")
|
|
22
|
+
assert pkg is not None
|
|
23
|
+
assert pkg.boundary == SERVER
|
|
24
|
+
|
|
25
|
+
def test_unknown_package_defaults_to_client(self):
|
|
26
|
+
interop = JSInterop()
|
|
27
|
+
pkg = interop.resolve_npm_package("some-unknown-lib")
|
|
28
|
+
assert pkg is not None
|
|
29
|
+
assert pkg.boundary == CLIENT
|
|
30
|
+
|
|
31
|
+
def test_scoped_package_resolution(self):
|
|
32
|
+
interop = JSInterop()
|
|
33
|
+
pkg = interop.resolve_npm_package("@tiptap/core")
|
|
34
|
+
assert pkg is not None
|
|
35
|
+
assert pkg.boundary == CLIENT
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestDynamicImportDetection:
|
|
39
|
+
def test_detect_dynamic_import(self):
|
|
40
|
+
interop = JSInterop()
|
|
41
|
+
source = 'const mod = import("chart.js")'
|
|
42
|
+
results = interop.detect_dynamic_imports(source)
|
|
43
|
+
assert len(results) == 1
|
|
44
|
+
assert results[0]["path"] == "chart.js"
|
|
45
|
+
assert results[0]["dynamic"] is True
|
|
46
|
+
|
|
47
|
+
def test_no_dynamic_import(self):
|
|
48
|
+
interop = JSInterop()
|
|
49
|
+
source = 'import Chart from "chart.js"'
|
|
50
|
+
results = interop.detect_dynamic_imports(source)
|
|
51
|
+
assert len(results) == 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestServerIsolation:
|
|
55
|
+
def test_server_package_in_client_context_is_error(self):
|
|
56
|
+
interop = JSInterop()
|
|
57
|
+
from tw_framework.module_boundaries import ImportInfo
|
|
58
|
+
imports = [ImportInfo(path="express", context="client", line=1, file="test.tw")]
|
|
59
|
+
errors = interop.validate_server_isolation(imports)
|
|
60
|
+
assert len(errors) == 1
|
|
61
|
+
assert errors[0]["code"] == "TW3000"
|
|
62
|
+
|
|
63
|
+
def test_client_package_in_client_context_is_ok(self):
|
|
64
|
+
interop = JSInterop()
|
|
65
|
+
from tw_framework.module_boundaries import ImportInfo
|
|
66
|
+
imports = [ImportInfo(path="chart.js", context="client", line=1, file="test.tw")]
|
|
67
|
+
errors = interop.validate_server_isolation(imports)
|
|
68
|
+
assert len(errors) == 0
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Tests for the module boundary system."""
|
|
2
|
+
import pytest
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
7
|
+
|
|
8
|
+
from tw_framework.module_boundaries import (
|
|
9
|
+
ImportClassifier, ImportInfo, BoundaryViolation,
|
|
10
|
+
SERVER, CLIENT, SHARED,
|
|
11
|
+
ALL_TW_PACKAGES, TW_PACKAGE_BOUNDARIES,
|
|
12
|
+
is_tw_package, get_package_boundary,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestImportClassification:
|
|
17
|
+
def test_tw_state_is_client(self):
|
|
18
|
+
c = ImportClassifier()
|
|
19
|
+
assert c.classify_import("tw/state") == CLIENT
|
|
20
|
+
|
|
21
|
+
def test_tw_router_is_client(self):
|
|
22
|
+
c = ImportClassifier()
|
|
23
|
+
assert c.classify_import("tw/router") == CLIENT
|
|
24
|
+
|
|
25
|
+
def test_tw_form_is_client(self):
|
|
26
|
+
c = ImportClassifier()
|
|
27
|
+
assert c.classify_import("tw/form") == CLIENT
|
|
28
|
+
|
|
29
|
+
def test_tw_realtime_is_client(self):
|
|
30
|
+
c = ImportClassifier()
|
|
31
|
+
assert c.classify_import("tw/realtime") == CLIENT
|
|
32
|
+
|
|
33
|
+
def test_tw_auth_is_shared(self):
|
|
34
|
+
c = ImportClassifier()
|
|
35
|
+
assert c.classify_import("tw/auth") == SHARED
|
|
36
|
+
|
|
37
|
+
def test_tw_fetch_is_shared(self):
|
|
38
|
+
c = ImportClassifier()
|
|
39
|
+
assert c.classify_import("tw/fetch") == SHARED
|
|
40
|
+
|
|
41
|
+
def test_tw_image_is_shared(self):
|
|
42
|
+
c = ImportClassifier()
|
|
43
|
+
assert c.classify_import("tw/image") == SHARED
|
|
44
|
+
|
|
45
|
+
def test_tw_server_is_server(self):
|
|
46
|
+
c = ImportClassifier()
|
|
47
|
+
assert c.classify_import("tw/server") == SERVER
|
|
48
|
+
|
|
49
|
+
def test_npm_package_is_client(self):
|
|
50
|
+
c = ImportClassifier()
|
|
51
|
+
assert c.classify_import("chart.js") == CLIENT
|
|
52
|
+
|
|
53
|
+
def test_scoped_npm_is_client(self):
|
|
54
|
+
c = ImportClassifier()
|
|
55
|
+
assert c.classify_import("@tiptap/core") == CLIENT
|
|
56
|
+
|
|
57
|
+
def test_twm_file_is_server(self):
|
|
58
|
+
c = ImportClassifier()
|
|
59
|
+
assert c.classify_import("api.twm") == SERVER
|
|
60
|
+
|
|
61
|
+
def test_lib_import_is_server(self):
|
|
62
|
+
c = ImportClassifier()
|
|
63
|
+
assert c.classify_import("@lib/database") == SERVER
|
|
64
|
+
|
|
65
|
+
def test_tw_component_is_shared(self):
|
|
66
|
+
c = ImportClassifier()
|
|
67
|
+
assert c.classify_import("Button") == SHARED
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestBoundaryValidation:
|
|
71
|
+
def test_client_importing_server_is_error(self):
|
|
72
|
+
c = ImportClassifier()
|
|
73
|
+
imports = [ImportInfo(path="tw/server", context="client", line=1, file="test.tw")]
|
|
74
|
+
violations = c.validate_imports(imports)
|
|
75
|
+
assert len(violations) == 1
|
|
76
|
+
assert violations[0].code == "TW2000"
|
|
77
|
+
|
|
78
|
+
def test_server_importing_client_is_error(self):
|
|
79
|
+
c = ImportClassifier()
|
|
80
|
+
imports = [ImportInfo(path="tw/state", context="server", line=1, file="test.tw")]
|
|
81
|
+
violations = c.validate_imports(imports)
|
|
82
|
+
assert len(violations) == 1
|
|
83
|
+
assert violations[0].code == "TW2001"
|
|
84
|
+
|
|
85
|
+
def test_valid_imports_no_violations(self):
|
|
86
|
+
c = ImportClassifier()
|
|
87
|
+
imports = [
|
|
88
|
+
ImportInfo(path="tw/state", context="client", line=1, file="test.tw"),
|
|
89
|
+
ImportInfo(path="tw/server", context="server", line=2, file="test.tw"),
|
|
90
|
+
]
|
|
91
|
+
violations = c.validate_imports(imports)
|
|
92
|
+
assert len(violations) == 0
|
|
93
|
+
|
|
94
|
+
def test_get_client_imports_filters_correctly(self):
|
|
95
|
+
c = ImportClassifier()
|
|
96
|
+
imports = [
|
|
97
|
+
ImportInfo(path="tw/state", context="client", line=1),
|
|
98
|
+
ImportInfo(path="tw/server", context="server", line=2),
|
|
99
|
+
ImportInfo(path="tw/fetch", context="client", line=3),
|
|
100
|
+
]
|
|
101
|
+
client_imports = c.get_client_imports(imports)
|
|
102
|
+
paths = [imp.path for imp in client_imports]
|
|
103
|
+
assert "tw/state" in paths
|
|
104
|
+
assert "tw/fetch" in paths
|
|
105
|
+
assert "tw/server" not in paths
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestSourceScanning:
|
|
109
|
+
def test_scan_simple_import(self):
|
|
110
|
+
c = ImportClassifier()
|
|
111
|
+
source = 'import "Button"'
|
|
112
|
+
imports = c.scan_source_imports(source, "test.tw")
|
|
113
|
+
assert len(imports) == 1
|
|
114
|
+
assert imports[0].path == "Button"
|
|
115
|
+
|
|
116
|
+
def test_scan_named_import(self):
|
|
117
|
+
c = ImportClassifier()
|
|
118
|
+
source = 'import { store } from "tw/state"'
|
|
119
|
+
imports = c.scan_source_imports(source, "test.tw")
|
|
120
|
+
assert len(imports) == 1
|
|
121
|
+
assert imports[0].path == "tw/state"
|
|
122
|
+
|
|
123
|
+
def test_scan_default_import(self):
|
|
124
|
+
c = ImportClassifier()
|
|
125
|
+
source = 'import Chart from "chart.js"'
|
|
126
|
+
imports = c.scan_source_imports(source, "test.tw")
|
|
127
|
+
assert len(imports) == 1
|
|
128
|
+
assert imports[0].path == "chart.js"
|
|
129
|
+
|
|
130
|
+
def test_scan_multiple_imports(self):
|
|
131
|
+
c = ImportClassifier()
|
|
132
|
+
source = """import "Button"
|
|
133
|
+
import { store } from "tw/state"
|
|
134
|
+
import Chart from "chart.js"
|
|
135
|
+
"""
|
|
136
|
+
imports = c.scan_source_imports(source, "test.tw")
|
|
137
|
+
assert len(imports) == 3
|
|
138
|
+
|
|
139
|
+
def test_is_tw_package(self):
|
|
140
|
+
assert is_tw_package("tw/state") is True
|
|
141
|
+
assert is_tw_package("tw/router") is True
|
|
142
|
+
assert is_tw_package("chart.js") is False
|
|
143
|
+
|
|
144
|
+
def test_get_package_boundary(self):
|
|
145
|
+
assert get_package_boundary("tw/state") == CLIENT
|
|
146
|
+
assert get_package_boundary("tw/auth") == SHARED
|
|
147
|
+
assert get_package_boundary("tw/server") == SERVER
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestModuleSourceClassification:
|
|
151
|
+
def test_server_api_detected(self):
|
|
152
|
+
c = ImportClassifier()
|
|
153
|
+
source = "const fs = require('fs')"
|
|
154
|
+
assert c.classify_module_source(source) == SERVER
|
|
155
|
+
|
|
156
|
+
def test_client_api_detected(self):
|
|
157
|
+
c = ImportClassifier()
|
|
158
|
+
source = "document.getElementById('app')"
|
|
159
|
+
assert c.classify_module_source(source) == CLIENT
|
|
160
|
+
|
|
161
|
+
def test_pure_module_is_shared(self):
|
|
162
|
+
c = ImportClassifier()
|
|
163
|
+
source = "function add(a, b) { return a + b; }"
|
|
164
|
+
assert c.classify_module_source(source) == SHARED
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Tests for the runtime loader."""
|
|
2
|
+
import pytest
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
7
|
+
|
|
8
|
+
from tw_framework.runtime_loader import PageCapability, RuntimeLoader
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestPageCapability:
|
|
12
|
+
def test_default_is_zero_js(self):
|
|
13
|
+
cap = PageCapability()
|
|
14
|
+
assert cap.is_zero_js is True
|
|
15
|
+
|
|
16
|
+
def test_needs_state_disables_zero_js(self):
|
|
17
|
+
cap = PageCapability(needs_state=True)
|
|
18
|
+
assert cap.is_zero_js is False
|
|
19
|
+
|
|
20
|
+
def test_needs_router_disables_zero_js(self):
|
|
21
|
+
cap = PageCapability(needs_router=True)
|
|
22
|
+
assert cap.is_zero_js is False
|
|
23
|
+
|
|
24
|
+
def test_needs_npm_packages_disables_zero_js(self):
|
|
25
|
+
cap = PageCapability(needs_npm_packages=["chart.js"])
|
|
26
|
+
assert cap.is_zero_js is False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestRuntimeLoader:
|
|
30
|
+
def test_analyze_static_page(self):
|
|
31
|
+
loader = RuntimeLoader()
|
|
32
|
+
cap = loader.analyze_page_capabilities(
|
|
33
|
+
source="<h1>Hello</h1>",
|
|
34
|
+
page_ast=None,
|
|
35
|
+
)
|
|
36
|
+
assert cap.is_zero_js is True
|
|
37
|
+
|
|
38
|
+
def test_analyze_state_page(self):
|
|
39
|
+
loader = RuntimeLoader()
|
|
40
|
+
cap = loader.analyze_page_capabilities(
|
|
41
|
+
source="state { count 0 }",
|
|
42
|
+
page_ast=None,
|
|
43
|
+
)
|
|
44
|
+
assert cap.is_zero_js is False
|
|
45
|
+
assert cap.needs_state is True
|
|
46
|
+
|
|
47
|
+
def test_analyze_router_page(self):
|
|
48
|
+
loader = RuntimeLoader()
|
|
49
|
+
cap = loader.analyze_page_capabilities(
|
|
50
|
+
source='Link { href "/dashboard" } { "Dashboard" }',
|
|
51
|
+
page_ast=None,
|
|
52
|
+
)
|
|
53
|
+
assert cap.needs_router is True
|
|
54
|
+
|
|
55
|
+
def test_analyze_realtime_page(self):
|
|
56
|
+
loader = RuntimeLoader()
|
|
57
|
+
cap = loader.analyze_page_capabilities(
|
|
58
|
+
source="socket('/api/events')",
|
|
59
|
+
page_ast=None,
|
|
60
|
+
)
|
|
61
|
+
assert cap.needs_realtime is True
|
|
62
|
+
|
|
63
|
+
def test_analyze_form_page(self):
|
|
64
|
+
loader = RuntimeLoader()
|
|
65
|
+
cap = loader.analyze_page_capabilities(
|
|
66
|
+
source='Form { action "/api/contact" } { }',
|
|
67
|
+
page_ast=None,
|
|
68
|
+
)
|
|
69
|
+
assert cap.needs_forms is True
|
|
70
|
+
|
|
71
|
+
def test_generate_runtime_tags_zero_js(self):
|
|
72
|
+
loader = RuntimeLoader()
|
|
73
|
+
cap = PageCapability()
|
|
74
|
+
tags = loader.generate_runtime_tags(cap)
|
|
75
|
+
assert tags == ""
|
|
76
|
+
|
|
77
|
+
def test_generate_runtime_tags_with_state(self):
|
|
78
|
+
loader = RuntimeLoader()
|
|
79
|
+
cap = PageCapability(needs_state=True)
|
|
80
|
+
tags = loader.generate_runtime_tags(cap)
|
|
81
|
+
assert "state" in tags
|
|
82
|
+
assert "base" in tags
|
|
83
|
+
|
|
84
|
+
def test_get_required_runtimes_zero_js(self):
|
|
85
|
+
loader = RuntimeLoader()
|
|
86
|
+
cap = PageCapability()
|
|
87
|
+
runtimes = loader.get_required_runtimes(cap)
|
|
88
|
+
assert len(runtimes) == 0
|
|
89
|
+
|
|
90
|
+
def test_get_required_runtimes_interactive(self):
|
|
91
|
+
loader = RuntimeLoader()
|
|
92
|
+
cap = PageCapability(needs_state=True, needs_router=True)
|
|
93
|
+
runtimes = loader.get_required_runtimes(cap)
|
|
94
|
+
assert "base" in runtimes
|
|
95
|
+
assert "state" in runtimes
|
|
96
|
+
assert "router" in runtimes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Tests for server actions."""
|
|
2
|
+
import pytest
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
7
|
+
|
|
8
|
+
from tw_framework.server_actions import (
|
|
9
|
+
ActionRegistry, ServerAction, handle_action_request,
|
|
10
|
+
get_action_registry, register_action,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestActionRegistry:
|
|
15
|
+
def test_register_action(self):
|
|
16
|
+
registry = ActionRegistry()
|
|
17
|
+
registry.register("test_action", lambda args, session=None: {"ok": True})
|
|
18
|
+
assert "test_action" in registry.list_actions()
|
|
19
|
+
|
|
20
|
+
def test_get_action(self):
|
|
21
|
+
registry = ActionRegistry()
|
|
22
|
+
registry.register("test_action", lambda args, session=None: {"ok": True})
|
|
23
|
+
action = registry.get("test_action")
|
|
24
|
+
assert action is not None
|
|
25
|
+
assert action.name == "test_action"
|
|
26
|
+
|
|
27
|
+
def test_get_unknown_action(self):
|
|
28
|
+
registry = ActionRegistry()
|
|
29
|
+
assert registry.get("nonexistent") is None
|
|
30
|
+
|
|
31
|
+
def test_execute_action(self):
|
|
32
|
+
registry = ActionRegistry()
|
|
33
|
+
registry.register(
|
|
34
|
+
"create_post",
|
|
35
|
+
lambda args, session=None: {"id": 1, "title": args.get("title")},
|
|
36
|
+
require_auth=False,
|
|
37
|
+
require_csrf=False,
|
|
38
|
+
)
|
|
39
|
+
result = registry.execute("create_post", {"title": "Hello"}, session=None, csrf_token="")
|
|
40
|
+
assert result["ok"] is True
|
|
41
|
+
assert result["data"]["title"] == "Hello"
|
|
42
|
+
|
|
43
|
+
def test_execute_unknown_action(self):
|
|
44
|
+
registry = ActionRegistry()
|
|
45
|
+
result = registry.execute("unknown", {}, session=None, csrf_token="")
|
|
46
|
+
assert result["ok"] is False
|
|
47
|
+
assert result["status"] == 404
|
|
48
|
+
|
|
49
|
+
def test_execute_requires_auth(self):
|
|
50
|
+
registry = ActionRegistry()
|
|
51
|
+
registry.register("secret", lambda args, session=None: "data", require_auth=True, require_csrf=False)
|
|
52
|
+
result = registry.execute("secret", {}, session=None, csrf_token="")
|
|
53
|
+
assert result["ok"] is False
|
|
54
|
+
assert result["status"] == 401
|
|
55
|
+
|
|
56
|
+
def test_execute_requires_csrf(self):
|
|
57
|
+
registry = ActionRegistry()
|
|
58
|
+
registry.register("protected", lambda args, session=None: "data", require_auth=False, require_csrf=True)
|
|
59
|
+
result = registry.execute("protected", {}, session=None, csrf_token="")
|
|
60
|
+
assert result["ok"] is False
|
|
61
|
+
assert result["status"] == 403
|
|
62
|
+
|
|
63
|
+
def test_validate_args(self):
|
|
64
|
+
registry = ActionRegistry()
|
|
65
|
+
registry.register(
|
|
66
|
+
"create_user",
|
|
67
|
+
lambda args, session=None: {"created": True},
|
|
68
|
+
schema={"name": {"required": True, "type": "string"}},
|
|
69
|
+
require_auth=False,
|
|
70
|
+
require_csrf=False,
|
|
71
|
+
)
|
|
72
|
+
result = registry.execute("create_user", {}, session=None, csrf_token="")
|
|
73
|
+
assert result["ok"] is False
|
|
74
|
+
assert "Missing required field" in result["error"]
|
|
75
|
+
|
|
76
|
+
def test_validate_args_type(self):
|
|
77
|
+
registry = ActionRegistry()
|
|
78
|
+
registry.register(
|
|
79
|
+
"set_age",
|
|
80
|
+
lambda args, session=None: {"ok": True},
|
|
81
|
+
schema={"age": {"required": True, "type": "number"}},
|
|
82
|
+
require_auth=False,
|
|
83
|
+
require_csrf=False,
|
|
84
|
+
)
|
|
85
|
+
result = registry.execute("set_age", {"age": "not-a-number"}, session=None, csrf_token="")
|
|
86
|
+
assert result["ok"] is False
|
|
87
|
+
assert "must be a number" in result["error"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestHandleActionRequest:
|
|
91
|
+
def test_missing_action_name(self):
|
|
92
|
+
result = handle_action_request({"args": {}}, {})
|
|
93
|
+
assert result["status"] == 400
|
|
94
|
+
|
|
95
|
+
def test_unknown_action(self):
|
|
96
|
+
result = handle_action_request(
|
|
97
|
+
{"action": "unknown", "args": {}, "csrf": "token"},
|
|
98
|
+
{},
|
|
99
|
+
)
|
|
100
|
+
import json
|
|
101
|
+
data = json.loads(result["body"])
|
|
102
|
+
assert data["ok"] is False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestActionRegistration:
|
|
106
|
+
def teardown_method(self):
|
|
107
|
+
"""Clean up global registry."""
|
|
108
|
+
registry = get_action_registry()
|
|
109
|
+
registry._actions.clear()
|
|
110
|
+
|
|
111
|
+
def test_global_register(self):
|
|
112
|
+
register_action(
|
|
113
|
+
"global_test",
|
|
114
|
+
lambda args, session=None: {"ok": True},
|
|
115
|
+
require_auth=False,
|
|
116
|
+
require_csrf=False,
|
|
117
|
+
)
|
|
118
|
+
registry = get_action_registry()
|
|
119
|
+
assert "global_test" in registry.list_actions()
|