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.
Files changed (124) hide show
  1. {tw_framework-0.5.2 → tw_framework-0.6.2}/PKG-INFO +1 -1
  2. {tw_framework-0.5.2 → tw_framework-0.6.2}/pyproject.toml +3 -3
  3. tw_framework-0.6.2/tests/test_component_classifier.py +90 -0
  4. tw_framework-0.6.2/tests/test_error_boundaries.py +75 -0
  5. tw_framework-0.6.2/tests/test_js_interop.py +68 -0
  6. tw_framework-0.6.2/tests/test_module_boundaries.py +164 -0
  7. tw_framework-0.6.2/tests/test_runtime_loader.py +96 -0
  8. tw_framework-0.6.2/tests/test_server_actions.py +119 -0
  9. tw_framework-0.6.2/tests/test_tw_auth.py +180 -0
  10. tw_framework-0.6.2/tests/test_tw_fetch.py +76 -0
  11. tw_framework-0.6.2/tests/test_tw_form.py +147 -0
  12. tw_framework-0.6.2/tests/test_tw_realtime.py +104 -0
  13. tw_framework-0.6.2/tests/test_tw_router.py +101 -0
  14. tw_framework-0.6.2/tests/test_tw_state.py +118 -0
  15. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/__init__.py +1 -1
  16. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/compiler.py +23 -0
  17. tw_framework-0.6.2/tw_framework/component_classifier.py +216 -0
  18. tw_framework-0.6.2/tw_framework/error_boundaries.py +254 -0
  19. tw_framework-0.6.2/tw_framework/js_interop.py +293 -0
  20. tw_framework-0.6.2/tw_framework/module_boundaries.py +276 -0
  21. tw_framework-0.6.2/tw_framework/runtime_loader.py +663 -0
  22. tw_framework-0.6.2/tw_framework/server_actions.py +226 -0
  23. tw_framework-0.6.2/tw_framework/tw_auth/__init__.py +11 -0
  24. tw_framework-0.6.2/tw_framework/tw_auth/client.py +46 -0
  25. tw_framework-0.6.2/tw_framework/tw_auth/middleware.py +169 -0
  26. tw_framework-0.6.2/tw_framework/tw_auth/runtime.py +79 -0
  27. tw_framework-0.6.2/tw_framework/tw_auth/session.py +148 -0
  28. tw_framework-0.6.2/tw_framework/tw_fetch/__init__.py +5 -0
  29. tw_framework-0.6.2/tw_framework/tw_fetch/fetch.py +142 -0
  30. tw_framework-0.6.2/tw_framework/tw_fetch/runtime.py +58 -0
  31. tw_framework-0.6.2/tw_framework/tw_font/__init__.py +4 -0
  32. tw_framework-0.6.2/tw_framework/tw_font/loader.py +70 -0
  33. tw_framework-0.6.2/tw_framework/tw_form/__init__.py +6 -0
  34. tw_framework-0.6.2/tw_framework/tw_form/form.py +118 -0
  35. tw_framework-0.6.2/tw_framework/tw_form/runtime.py +136 -0
  36. tw_framework-0.6.2/tw_framework/tw_form/validation.py +139 -0
  37. tw_framework-0.6.2/tw_framework/tw_image/__init__.py +10 -0
  38. tw_framework-0.6.2/tw_framework/tw_image/cache.py +42 -0
  39. tw_framework-0.6.2/tw_framework/tw_image/component.py +144 -0
  40. tw_framework-0.6.2/tw_framework/tw_image/formats.py +21 -0
  41. tw_framework-0.6.2/tw_framework/tw_image/optimizer.py +67 -0
  42. tw_framework-0.6.2/tw_framework/tw_metadata/__init__.py +4 -0
  43. tw_framework-0.6.2/tw_framework/tw_metadata/meta.py +137 -0
  44. tw_framework-0.6.2/tw_framework/tw_realtime/__init__.py +6 -0
  45. tw_framework-0.6.2/tw_framework/tw_realtime/client.py +73 -0
  46. tw_framework-0.6.2/tw_framework/tw_realtime/runtime.py +85 -0
  47. tw_framework-0.6.2/tw_framework/tw_realtime/server.py +167 -0
  48. tw_framework-0.6.2/tw_framework/tw_router/__init__.py +5 -0
  49. tw_framework-0.6.2/tw_framework/tw_router/router.py +114 -0
  50. tw_framework-0.6.2/tw_framework/tw_router/runtime.py +99 -0
  51. tw_framework-0.6.2/tw_framework/tw_state/__init__.py +21 -0
  52. tw_framework-0.6.2/tw_framework/tw_state/runtime.py +105 -0
  53. tw_framework-0.6.2/tw_framework/tw_state/store.py +163 -0
  54. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/PKG-INFO +1 -1
  55. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/SOURCES.txt +50 -1
  56. {tw_framework-0.5.2 → tw_framework-0.6.2}/LICENSE +0 -0
  57. {tw_framework-0.5.2 → tw_framework-0.6.2}/README.md +0 -0
  58. {tw_framework-0.5.2 → tw_framework-0.6.2}/setup.cfg +0 -0
  59. {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_cache_invalidation.py +0 -0
  60. {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_deployment.py +0 -0
  61. {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_inline_json_and_tailwind.py +0 -0
  62. {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_lib_system.py +0 -0
  63. {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_parallel_compilation.py +0 -0
  64. {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_tw_image.py +0 -0
  65. {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_type_safety.py +0 -0
  66. {tw_framework-0.5.2 → tw_framework-0.6.2}/tests/test_zero_js.py +0 -0
  67. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/adapters/__init__.py +0 -0
  68. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/adapters/cloudflare.py +0 -0
  69. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/adapters/netlify.py +0 -0
  70. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/adapters/vercel.py +0 -0
  71. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/adapters/vercel_functions.py +0 -0
  72. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/advanced_diagnostics.py +0 -0
  73. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/asset_optimizer.py +0 -0
  74. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/ast_nodes.py +0 -0
  75. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/build.py +0 -0
  76. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/build_performance.py +0 -0
  77. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/build_report.py +0 -0
  78. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/cli.py +0 -0
  79. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/code_splitting.py +0 -0
  80. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/common.py +0 -0
  81. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/compiler_stats.py +0 -0
  82. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/dead_code.py +0 -0
  83. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/dependency_graph.py +0 -0
  84. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/deploy.py +0 -0
  85. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/dev.py +0 -0
  86. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/diagnostics.py +0 -0
  87. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/dynamic_imports.py +0 -0
  88. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/error_formatter.py +0 -0
  89. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/framework.py +0 -0
  90. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/hydration.py +0 -0
  91. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/incremental_cache.py +0 -0
  92. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/interpreter.py +0 -0
  93. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/ir.py +0 -0
  94. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/lexer.py +0 -0
  95. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/lib_executor.py +0 -0
  96. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/lowering.py +0 -0
  97. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/lsp_server.py +0 -0
  98. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/parser.py +0 -0
  99. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/partial_rebuild.py +0 -0
  100. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/performance_analyzer.py +0 -0
  101. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/plugin_runtime.py +0 -0
  102. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/production_optimizer.py +0 -0
  103. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/reactivity.py +0 -0
  104. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/render_css.py +0 -0
  105. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/render_html.py +0 -0
  106. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/route_optimizer.py +0 -0
  107. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/router.py +0 -0
  108. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/runtime_model.py +0 -0
  109. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/runtime_values.py +0 -0
  110. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/semantic.py +0 -0
  111. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/server.py +0 -0
  112. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/signature.py +0 -0
  113. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/static_dynamic_auto.py +0 -0
  114. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/streaming.py +0 -0
  115. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/tailwind_map.py +0 -0
  116. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/test_build_cli_pytest.py +0 -0
  117. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/tree_shaking.py +0 -0
  118. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/twm_api_runner.js +0 -0
  119. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/twm_parser.py +0 -0
  120. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework/websocket.py +0 -0
  121. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/dependency_links.txt +0 -0
  122. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/entry_points.txt +0 -0
  123. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/requires.txt +0 -0
  124. {tw_framework-0.5.2 → tw_framework-0.6.2}/tw_framework.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tw-framework
3
- Version: 0.5.2
3
+ Version: 0.6.2
4
4
  Summary: TW framework CLI with create, dev, build, and deploy commands
5
5
  Author-email: KANISHK KUMAR <mlkraj290@gmail.com>
6
6
  Maintainer-email: Kanishk Kumar <mlkraj290@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tw-framework"
7
- version = "0.5.2"
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
- packages = ["tw_framework", "tw_framework.adapters"]
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()