jac-client 0.2.3__py3-none-any.whl → 0.2.4__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 (176) hide show
  1. jac_client/examples/all-in-one/assets/workers/worker.py +5 -0
  2. jac_client/tests/conftest.py +281 -0
  3. jac_client/tests/test_cli.py +755 -0
  4. jac_client/tests/test_it.py +347 -67
  5. {jac_client-0.2.3.dist-info → jac_client-0.2.4.dist-info}/METADATA +28 -30
  6. jac_client-0.2.4.dist-info/RECORD +10 -0
  7. {jac_client-0.2.3.dist-info → jac_client-0.2.4.dist-info}/WHEEL +2 -1
  8. jac_client-0.2.4.dist-info/entry_points.txt +4 -0
  9. jac_client-0.2.4.dist-info/top_level.txt +1 -0
  10. jac_client/docs/README.md +0 -689
  11. jac_client/docs/advanced-state.md +0 -1265
  12. jac_client/docs/asset-serving/intro.md +0 -209
  13. jac_client/docs/assets/pipe_line-v2.svg +0 -32
  14. jac_client/docs/assets/pipe_line.png +0 -0
  15. jac_client/docs/file-system/app.jac.md +0 -121
  16. jac_client/docs/file-system/backend-frontend.md +0 -217
  17. jac_client/docs/file-system/intro.md +0 -72
  18. jac_client/docs/file-system/nested-imports.md +0 -348
  19. jac_client/docs/guide-example/intro.md +0 -115
  20. jac_client/docs/guide-example/step-01-setup.md +0 -270
  21. jac_client/docs/guide-example/step-02-components.md +0 -416
  22. jac_client/docs/guide-example/step-03-styling.md +0 -478
  23. jac_client/docs/guide-example/step-04-todo-ui.md +0 -477
  24. jac_client/docs/guide-example/step-05-local-state.md +0 -530
  25. jac_client/docs/guide-example/step-06-events.md +0 -749
  26. jac_client/docs/guide-example/step-07-effects.md +0 -468
  27. jac_client/docs/guide-example/step-08-walkers.md +0 -534
  28. jac_client/docs/guide-example/step-09-authentication.md +0 -586
  29. jac_client/docs/guide-example/step-10-routing.md +0 -539
  30. jac_client/docs/guide-example/step-11-final.md +0 -963
  31. jac_client/docs/imports.md +0 -1141
  32. jac_client/docs/lifecycle-hooks.md +0 -773
  33. jac_client/docs/routing.md +0 -659
  34. jac_client/docs/styling/intro.md +0 -249
  35. jac_client/docs/styling/js-styling.md +0 -367
  36. jac_client/docs/styling/material-ui.md +0 -341
  37. jac_client/docs/styling/pure-css.md +0 -299
  38. jac_client/docs/styling/sass.md +0 -403
  39. jac_client/docs/styling/styled-components.md +0 -395
  40. jac_client/docs/styling/tailwind.md +0 -298
  41. jac_client/examples/all-in-one/.babelrc +0 -9
  42. jac_client/examples/all-in-one/README.md +0 -16
  43. jac_client/examples/all-in-one/app.jac +0 -426
  44. jac_client/examples/all-in-one/assets/burger.png +0 -0
  45. jac_client/examples/all-in-one/button.jac +0 -7
  46. jac_client/examples/all-in-one/components/button.jac +0 -7
  47. jac_client/examples/all-in-one/package.json +0 -29
  48. jac_client/examples/all-in-one/styles.css +0 -26
  49. jac_client/examples/all-in-one/vite.config.js +0 -28
  50. jac_client/examples/asset-serving/css-with-image/.babelrc +0 -9
  51. jac_client/examples/asset-serving/css-with-image/README.md +0 -91
  52. jac_client/examples/asset-serving/css-with-image/app.jac +0 -88
  53. jac_client/examples/asset-serving/css-with-image/assets/burger.png +0 -0
  54. jac_client/examples/asset-serving/css-with-image/package.json +0 -28
  55. jac_client/examples/asset-serving/css-with-image/styles.css +0 -26
  56. jac_client/examples/asset-serving/css-with-image/vite.config.js +0 -28
  57. jac_client/examples/asset-serving/image-asset/.babelrc +0 -9
  58. jac_client/examples/asset-serving/image-asset/README.md +0 -119
  59. jac_client/examples/asset-serving/image-asset/app.jac +0 -55
  60. jac_client/examples/asset-serving/image-asset/assets/burger.png +0 -0
  61. jac_client/examples/asset-serving/image-asset/package.json +0 -28
  62. jac_client/examples/asset-serving/image-asset/styles.css +0 -26
  63. jac_client/examples/asset-serving/image-asset/vite.config.js +0 -28
  64. jac_client/examples/asset-serving/import-alias/.babelrc +0 -9
  65. jac_client/examples/asset-serving/import-alias/README.md +0 -83
  66. jac_client/examples/asset-serving/import-alias/app.jac +0 -111
  67. jac_client/examples/asset-serving/import-alias/assets/burger.png +0 -0
  68. jac_client/examples/asset-serving/import-alias/package.json +0 -28
  69. jac_client/examples/asset-serving/import-alias/vite.config.js +0 -28
  70. jac_client/examples/basic/.babelrc +0 -9
  71. jac_client/examples/basic/README.md +0 -16
  72. jac_client/examples/basic/app.jac +0 -21
  73. jac_client/examples/basic/package.json +0 -27
  74. jac_client/examples/basic/vite.config.js +0 -27
  75. jac_client/examples/basic-auth/.babelrc +0 -9
  76. jac_client/examples/basic-auth/README.md +0 -16
  77. jac_client/examples/basic-auth/app.jac +0 -308
  78. jac_client/examples/basic-auth/package.json +0 -27
  79. jac_client/examples/basic-auth/vite.config.js +0 -27
  80. jac_client/examples/basic-auth-with-router/.babelrc +0 -9
  81. jac_client/examples/basic-auth-with-router/README.md +0 -60
  82. jac_client/examples/basic-auth-with-router/app.jac +0 -464
  83. jac_client/examples/basic-auth-with-router/package.json +0 -28
  84. jac_client/examples/basic-auth-with-router/vite.config.js +0 -27
  85. jac_client/examples/basic-full-stack/.babelrc +0 -9
  86. jac_client/examples/basic-full-stack/README.md +0 -18
  87. jac_client/examples/basic-full-stack/app.jac +0 -320
  88. jac_client/examples/basic-full-stack/package.json +0 -28
  89. jac_client/examples/basic-full-stack/vite.config.js +0 -27
  90. jac_client/examples/css-styling/js-styling/.babelrc +0 -9
  91. jac_client/examples/css-styling/js-styling/README.md +0 -183
  92. jac_client/examples/css-styling/js-styling/app.jac +0 -84
  93. jac_client/examples/css-styling/js-styling/package.json +0 -28
  94. jac_client/examples/css-styling/js-styling/styles.js +0 -100
  95. jac_client/examples/css-styling/js-styling/vite.config.js +0 -27
  96. jac_client/examples/css-styling/material-ui/.babelrc +0 -9
  97. jac_client/examples/css-styling/material-ui/README.md +0 -16
  98. jac_client/examples/css-styling/material-ui/app.jac +0 -122
  99. jac_client/examples/css-styling/material-ui/package.json +0 -32
  100. jac_client/examples/css-styling/material-ui/vite.config.js +0 -27
  101. jac_client/examples/css-styling/pure-css/.babelrc +0 -9
  102. jac_client/examples/css-styling/pure-css/README.md +0 -16
  103. jac_client/examples/css-styling/pure-css/app.jac +0 -64
  104. jac_client/examples/css-styling/pure-css/package.json +0 -28
  105. jac_client/examples/css-styling/pure-css/styles.css +0 -111
  106. jac_client/examples/css-styling/pure-css/vite.config.js +0 -27
  107. jac_client/examples/css-styling/sass-example/.babelrc +0 -9
  108. jac_client/examples/css-styling/sass-example/README.md +0 -16
  109. jac_client/examples/css-styling/sass-example/app.jac +0 -64
  110. jac_client/examples/css-styling/sass-example/package.json +0 -29
  111. jac_client/examples/css-styling/sass-example/styles.scss +0 -153
  112. jac_client/examples/css-styling/sass-example/vite.config.js +0 -27
  113. jac_client/examples/css-styling/styled-components/.babelrc +0 -9
  114. jac_client/examples/css-styling/styled-components/README.md +0 -16
  115. jac_client/examples/css-styling/styled-components/app.jac +0 -71
  116. jac_client/examples/css-styling/styled-components/package.json +0 -29
  117. jac_client/examples/css-styling/styled-components/styled.js +0 -90
  118. jac_client/examples/css-styling/styled-components/vite.config.js +0 -27
  119. jac_client/examples/css-styling/tailwind-example/.babelrc +0 -9
  120. jac_client/examples/css-styling/tailwind-example/README.md +0 -16
  121. jac_client/examples/css-styling/tailwind-example/app.jac +0 -63
  122. jac_client/examples/css-styling/tailwind-example/global.css +0 -1
  123. jac_client/examples/css-styling/tailwind-example/package.json +0 -30
  124. jac_client/examples/css-styling/tailwind-example/vite.config.js +0 -29
  125. jac_client/examples/full-stack-with-auth/.babelrc +0 -9
  126. jac_client/examples/full-stack-with-auth/README.md +0 -16
  127. jac_client/examples/full-stack-with-auth/app.jac +0 -722
  128. jac_client/examples/full-stack-with-auth/package.json +0 -28
  129. jac_client/examples/full-stack-with-auth/vite.config.js +0 -29
  130. jac_client/examples/little-x/app.jac +0 -724
  131. jac_client/examples/little-x/package.json +0 -23
  132. jac_client/examples/little-x/submit-button.jac +0 -8
  133. jac_client/examples/nested-folders/nested-advance/.babelrc +0 -9
  134. jac_client/examples/nested-folders/nested-advance/ButtonRoot.jac +0 -11
  135. jac_client/examples/nested-folders/nested-advance/README.md +0 -77
  136. jac_client/examples/nested-folders/nested-advance/app.jac +0 -35
  137. jac_client/examples/nested-folders/nested-advance/level1/ButtonSecondL.jac +0 -19
  138. jac_client/examples/nested-folders/nested-advance/level1/Card.jac +0 -43
  139. jac_client/examples/nested-folders/nested-advance/level1/level2/ButtonThirdL.jac +0 -25
  140. jac_client/examples/nested-folders/nested-advance/package.json +0 -29
  141. jac_client/examples/nested-folders/nested-advance/vite.config.js +0 -28
  142. jac_client/examples/nested-folders/nested-basic/.babelrc +0 -9
  143. jac_client/examples/nested-folders/nested-basic/README.md +0 -183
  144. jac_client/examples/nested-folders/nested-basic/app.jac +0 -13
  145. jac_client/examples/nested-folders/nested-basic/app.js +0 -7
  146. jac_client/examples/nested-folders/nested-basic/button.jac +0 -7
  147. jac_client/examples/nested-folders/nested-basic/components/button.jac +0 -7
  148. jac_client/examples/nested-folders/nested-basic/package.json +0 -28
  149. jac_client/examples/nested-folders/nested-basic/vite.config.js +0 -27
  150. jac_client/examples/with-router/.babelrc +0 -9
  151. jac_client/examples/with-router/README.md +0 -17
  152. jac_client/examples/with-router/app.jac +0 -323
  153. jac_client/examples/with-router/package.json +0 -28
  154. jac_client/examples/with-router/vite.config.js +0 -27
  155. jac_client/plugin/cli.py +0 -244
  156. jac_client/plugin/client.py +0 -152
  157. jac_client/plugin/client_runtime.jac +0 -234
  158. jac_client/plugin/vite_client_bundle.py +0 -503
  159. jac_client/tests/fixtures/basic-app/app.jac +0 -23
  160. jac_client/tests/fixtures/cl_file/app.cl.jac +0 -48
  161. jac_client/tests/fixtures/cl_file/app.jac +0 -15
  162. jac_client/tests/fixtures/client_app_with_antd/app.jac +0 -34
  163. jac_client/tests/fixtures/js_import/app.jac +0 -34
  164. jac_client/tests/fixtures/js_import/utils.js +0 -21
  165. jac_client/tests/fixtures/package-lock.json +0 -329
  166. jac_client/tests/fixtures/package.json +0 -11
  167. jac_client/tests/fixtures/relative_import/app.jac +0 -11
  168. jac_client/tests/fixtures/relative_import/button.jac +0 -7
  169. jac_client/tests/fixtures/spawn_test/app.jac +0 -129
  170. jac_client/tests/fixtures/test_fragments_spread/app.jac +0 -67
  171. jac_client/tests/test_asset_examples.py +0 -322
  172. jac_client/tests/test_cl.py +0 -530
  173. jac_client/tests/test_create_jac_app.py +0 -131
  174. jac_client/tests/test_nested_file.py +0 -374
  175. jac_client-0.2.3.dist-info/RECORD +0 -171
  176. jac_client-0.2.3.dist-info/entry_points.txt +0 -4
@@ -2,18 +2,30 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import gc
5
6
  import json
6
7
  import os
7
8
  import shutil
8
9
  import socket
9
10
  import tempfile
10
11
  import time
11
- from subprocess import Popen, run
12
+ from http.client import RemoteDisconnected
13
+ from subprocess import PIPE, Popen, run
12
14
  from urllib.error import HTTPError, URLError
13
15
  from urllib.request import Request, urlopen
14
16
 
15
17
  import pytest
16
18
 
19
+ from jaclang.pycore.runtime import JacRuntime as Jac
20
+
21
+
22
+ @pytest.fixture(autouse=True)
23
+ def reset_jac_machine():
24
+ """Reset Jac machine before and after each test."""
25
+ Jac.reset_machine()
26
+ yield
27
+ Jac.reset_machine()
28
+
17
29
 
18
30
  def _wait_for_port(
19
31
  host: str,
@@ -44,10 +56,68 @@ def _wait_for_port(
44
56
  )
45
57
 
46
58
 
59
+ def _wait_for_endpoint(
60
+ url: str,
61
+ timeout: float = 120.0,
62
+ poll_interval: float = 2.0,
63
+ request_timeout: float = 30.0,
64
+ ) -> bytes:
65
+ """Block until an HTTP endpoint returns a successful response or timeout.
66
+
67
+ Retries on 503 Service Unavailable (temporary) and connection errors.
68
+ Fails immediately on 500 Internal Server Error (permanent errors like compilation failures).
69
+
70
+ Returns:
71
+ The response body as bytes.
72
+
73
+ Raises:
74
+ TimeoutError: if the endpoint does not return success within timeout.
75
+ HTTPError: if the endpoint returns a non-retryable error (e.g., 500).
76
+ """
77
+ deadline = time.time() + timeout
78
+ last_err: Exception | None = None
79
+
80
+ while time.time() < deadline:
81
+ try:
82
+ with urlopen(url, timeout=request_timeout) as resp:
83
+ return resp.read()
84
+ except HTTPError as exc:
85
+ if exc.code == 503:
86
+ # Service Unavailable - retry (temporary condition, e.g., compilation in progress)
87
+ # Close the underlying response to release the socket
88
+ exc.close()
89
+ last_err = exc
90
+ print(f"[DEBUG] Endpoint {url} returned 503, retrying...")
91
+ time.sleep(poll_interval)
92
+ elif exc.code == 500:
93
+ # Internal Server Error - do not retry (permanent error, e.g., compilation failure)
94
+ # Close the underlying response to release the socket
95
+ exc.close()
96
+ # Re-raise immediately - 500 indicates a permanent error that won't resolve by retrying
97
+ raise
98
+ else:
99
+ # Other HTTP errors should not be retried
100
+ raise
101
+ except URLError as exc:
102
+ # Connection errors - retry
103
+ last_err = exc
104
+ print(f"[DEBUG] Endpoint {url} connection error: {exc}, retrying...")
105
+ time.sleep(poll_interval)
106
+ except RemoteDisconnected as exc:
107
+ # Server closed connection - retry
108
+ last_err = exc
109
+ print(f"[DEBUG] Endpoint {url} remote disconnected: {exc}, retrying...")
110
+ time.sleep(poll_interval)
111
+
112
+ raise TimeoutError(
113
+ f"Timed out waiting for {url} to become available. Last error: {last_err}"
114
+ )
115
+
116
+
47
117
  def test_all_in_one_app_endpoints() -> None:
48
- """Create a Jac app, copy @all-in-one into it, run npm install, then verify endpoints."""
118
+ """Create a Jac app, copy @all-in-one into it, install packages from jac.toml, then verify endpoints."""
49
119
  print(
50
- "[DEBUG] Starting test_all_in_one_app_endpoints using jac create_jac_app + @all-in-one"
120
+ "[DEBUG] Starting test_all_in_one_app_endpoints using jac create --cl + @all-in-one"
51
121
  )
52
122
 
53
123
  # Resolve the path to jac_client/examples/all-in-one relative to this test file.
@@ -68,33 +138,33 @@ def test_all_in_one_app_endpoints() -> None:
68
138
  print(f"[DEBUG] Changed working directory to {temp_dir}")
69
139
 
70
140
  # 1. Create a new Jac app via CLI (requires jac + jac-client plugin installed)
71
- print(f"[DEBUG] Running 'jac create_jac_app {app_name}'")
72
- create_result = run(
73
- ["jac", "create_jac_app", app_name],
74
- capture_output=True,
141
+ print(f"[DEBUG] Running 'jac create --cl {app_name}'")
142
+ process = Popen(
143
+ ["jac", "create", "--cl", app_name],
144
+ stdin=PIPE,
145
+ stdout=PIPE,
146
+ stderr=PIPE,
75
147
  text=True,
76
148
  )
149
+ stdout, stderr = process.communicate()
150
+ returncode = process.returncode
151
+
77
152
  print(
78
- "[DEBUG] 'jac create_jac_app' completed "
79
- f"returncode={create_result.returncode}\n"
80
- f"STDOUT:\n{create_result.stdout}\n"
81
- f"STDERR:\n{create_result.stderr}\n"
153
+ "[DEBUG] 'jac create --cl' completed "
154
+ f"returncode={returncode}\n"
155
+ f"STDOUT:\n{stdout}\n"
156
+ f"STDERR:\n{stderr}\n"
82
157
  )
83
158
 
84
- # If the currently installed `jac` CLI does not support `create_jac_app`,
85
- # skip this integration test instead of failing the whole suite.
86
- if (
87
- create_result.returncode != 0
88
- and "invalid choice: 'create_jac_app'" in create_result.stderr
89
- ):
90
- pytest.skip(
91
- "Skipping: installed `jac` CLI does not support `create_jac_app`."
159
+ # If the currently installed `jac` CLI does not support `create --cl`,
160
+ # fail the test instead of skipping it.
161
+ if returncode != 0 and "unrecognized arguments: --cl" in stderr:
162
+ pytest.fail(
163
+ "Test failed: installed `jac` CLI does not support `create --cl`."
92
164
  )
93
165
 
94
- assert create_result.returncode == 0, (
95
- "jac create_jac_app failed\n"
96
- f"STDOUT:\n{create_result.stdout}\n"
97
- f"STDERR:\n{create_result.stderr}\n"
166
+ assert returncode == 0, (
167
+ f"jac create --cl failed\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}\n"
98
168
  )
99
169
 
100
170
  project_path = os.path.join(temp_dir, app_name)
@@ -114,37 +184,40 @@ def test_all_in_one_app_endpoints() -> None:
114
184
  else:
115
185
  shutil.copy2(src, dst)
116
186
 
117
- # 3. Run `npm install` inside the project directory so the frontend can build.
118
- print("[DEBUG] Running 'npm install' in created Jac app")
119
- npm_result = run(
120
- ["npm", "install"],
187
+ # 3. Install packages from jac.toml using `jac add --cl`
188
+ # This reads packages from jac.toml, generates package.json, and runs npm install
189
+ print("[DEBUG] Running 'jac add --cl' to install packages from jac.toml")
190
+ jac_add_result = run(
191
+ ["jac", "add", "--cl"],
121
192
  cwd=project_path,
122
193
  capture_output=True,
123
194
  text=True,
124
195
  )
125
196
  print(
126
- "[DEBUG] 'npm install' completed "
127
- f"returncode={npm_result.returncode}\n"
128
- f"STDOUT (truncated to 2000 chars):\n{npm_result.stdout[:2000]}\n"
129
- f"STDERR (truncated to 2000 chars):\n{npm_result.stderr[:2000]}\n"
197
+ "[DEBUG] 'jac add --cl' completed "
198
+ f"returncode={jac_add_result.returncode}\n"
199
+ f"STDOUT (truncated to 2000 chars):\n{jac_add_result.stdout[:2000]}\n"
200
+ f"STDERR (truncated to 2000 chars):\n{jac_add_result.stderr[:2000]}\n"
130
201
  )
131
202
 
132
- if npm_result.returncode != 0:
133
- pytest.skip(
134
- "Skipping: npm install failed or npm is not available in PATH."
203
+ if jac_add_result.returncode != 0:
204
+ pytest.fail(
205
+ f"Test failed: jac add --cl failed or npm is not available in PATH.\n"
206
+ f"STDOUT:\n{jac_add_result.stdout}\n"
207
+ f"STDERR:\n{jac_add_result.stderr}\n"
135
208
  )
136
209
 
137
- app_jac_path = os.path.join(project_path, "app.jac")
138
- assert os.path.isfile(app_jac_path), "all-in-one app.jac file missing"
210
+ app_jac_path = os.path.join(project_path, "src", "app.jac")
211
+ assert os.path.isfile(app_jac_path), "all-in-one src/app.jac file missing"
139
212
 
140
- # 4. Start the server: `jac serve app.jac`
213
+ # 4. Start the server: `jac serve src/app.jac`
141
214
  # NOTE: We don't use text mode here, so `Popen` defaults to bytes.
142
215
  # Use `Popen[bytes]` in the type annotation to keep mypy happy.
143
216
  server: Popen[bytes] | None = None
144
217
  try:
145
- print("[DEBUG] Starting server with 'jac serve app.jac'")
218
+ print("[DEBUG] Starting server with 'jac serve src/app.jac'")
146
219
  server = Popen(
147
- ["jac", "serve", "app.jac"],
220
+ ["jac", "serve", "src/app.jac"],
148
221
  cwd=project_path,
149
222
  )
150
223
 
@@ -169,28 +242,50 @@ def test_all_in_one_app_endpoints() -> None:
169
242
  assert resp_root.status == 200
170
243
  assert '"Jac API Server"' in root_body
171
244
  assert '"endpoints"' in root_body
245
+
246
+ # Verify custom headers from jac.toml are present
247
+ assert (
248
+ resp_root.headers.get("Cross-Origin-Opener-Policy")
249
+ == "same-origin"
250
+ ), (
251
+ "Expected Cross-Origin-Opener-Policy header to be 'same-origin'"
252
+ )
253
+ assert (
254
+ resp_root.headers.get("Cross-Origin-Embedder-Policy")
255
+ == "require-corp"
256
+ ), (
257
+ "Expected Cross-Origin-Embedder-Policy header to be 'require-corp'"
258
+ )
259
+ print(
260
+ "[DEBUG] Custom headers verified: COOP and COEP are present"
261
+ )
172
262
  except (URLError, HTTPError) as exc:
173
263
  print(f"[DEBUG] Error while requesting root endpoint: {exc}")
174
264
  pytest.fail(f"Failed to GET root endpoint: {exc}")
175
265
 
176
266
  # "/page/app" – main page is loading
267
+ # Note: This endpoint may return 503 (temporary) while the page is being compiled,
268
+ # or 500 (permanent) if there's a compilation error. We use _wait_for_endpoint
269
+ # to retry on 503 until it's ready, but it will fail immediately on 500.
177
270
  try:
178
- print("[DEBUG] Sending GET request to /page/app endpoint")
179
- with urlopen(
271
+ print(
272
+ "[DEBUG] Sending GET request to /page/app endpoint (with retry)"
273
+ )
274
+ page_bytes = _wait_for_endpoint(
180
275
  "http://127.0.0.1:8000/page/app",
181
- timeout=200,
182
- ) as resp_page:
183
- page_body = resp_page.read().decode("utf-8", errors="ignore")
184
- print(
185
- "[DEBUG] Received response from /page/app endpoint\n"
186
- f"Status: {resp_page.status}\n"
187
- f"Body (truncated to 500 chars):\n{page_body[:500]}"
188
- )
189
- assert resp_page.status == 200
190
- assert "<html" in page_body.lower()
191
- except (URLError, HTTPError) as exc:
276
+ timeout=120.0,
277
+ poll_interval=2.0,
278
+ request_timeout=30.0,
279
+ )
280
+ page_body = page_bytes.decode("utf-8", errors="ignore")
281
+ print(
282
+ "[DEBUG] Received response from /page/app endpoint\n"
283
+ f"Body (truncated to 500 chars):\n{page_body[:500]}"
284
+ )
285
+ assert "<html" in page_body.lower()
286
+ except (URLError, HTTPError, TimeoutError, RemoteDisconnected) as exc:
192
287
  print(f"[DEBUG] Error while requesting /page/app endpoint: {exc}")
193
- pytest.fail("Failed to GET /page/app endpoint")
288
+ pytest.fail(f"Failed to GET /page/app endpoint: {exc}")
194
289
 
195
290
  # "/page/app#/nested" – relative paths / nested route
196
291
  # (hash fragment is client-side only but server should still serve the app shell)
@@ -217,23 +312,26 @@ def test_all_in_one_app_endpoints() -> None:
217
312
  pytest.fail("Failed to GET /page/app#/nested endpoint")
218
313
 
219
314
  # "/static/main.css" – CSS compiled and serving
315
+ # Note: CSS may be compiled asynchronously, so we retry if it's not ready
220
316
  try:
221
- print("[DEBUG] Sending GET request to /static/main.css")
222
- with urlopen(
317
+ print(
318
+ "[DEBUG] Sending GET request to /static/main.css (with retry)"
319
+ )
320
+ css_bytes = _wait_for_endpoint(
223
321
  "http://127.0.0.1:8000/static/main.css",
224
- timeout=20,
225
- ) as resp_css:
226
- css_body = resp_css.read().decode("utf-8", errors="ignore")
227
- print(
228
- "[DEBUG] Received response from /static/main.css\n"
229
- f"Status: {resp_css.status}\n"
230
- f"Body (truncated to 500 chars):\n{css_body[:500]}"
231
- )
232
- assert resp_css.status == 200
233
- assert len(css_body.strip()) > 0
234
- except (URLError, HTTPError) as exc:
322
+ timeout=60.0,
323
+ poll_interval=2.0,
324
+ request_timeout=20.0,
325
+ )
326
+ css_body = css_bytes.decode("utf-8", errors="ignore")
327
+ print(
328
+ "[DEBUG] Received response from /static/main.css\n"
329
+ f"Body (truncated to 500 chars):\n{css_body[:500]}"
330
+ )
331
+ assert len(css_body.strip()) > 0, "CSS file should not be empty"
332
+ except (URLError, HTTPError, TimeoutError, RemoteDisconnected) as exc:
235
333
  print(f"[DEBUG] Error while requesting /static/main.css: {exc}")
236
- pytest.fail("Failed to GET /static/main.css")
334
+ pytest.fail(f"Failed to GET /static/main.css after retries: {exc}")
237
335
 
238
336
  # "/static/assets/burger.png" – static files are loading
239
337
  try:
@@ -259,6 +357,34 @@ def test_all_in_one_app_endpoints() -> None:
259
357
  )
260
358
  pytest.fail("Failed to GET /static/assets/burger.png")
261
359
 
360
+ # "/workers/worker.js" – worker script is served
361
+ try:
362
+ print(
363
+ "[DEBUG] Sending GET request to /workers/worker.js (with retry)"
364
+ )
365
+ worker_js_bytes = _wait_for_endpoint(
366
+ "http://127.0.0.1:8000/workers/worker.js",
367
+ timeout=60.0,
368
+ poll_interval=2.0,
369
+ request_timeout=20.0,
370
+ )
371
+ worker_js_body = worker_js_bytes.decode("utf-8", errors="ignore")
372
+ print(
373
+ "[DEBUG] Received response from /workers/worker.js\n"
374
+ f"Body (truncated to 500 chars):\n{worker_js_body[:500]}"
375
+ )
376
+ assert len(worker_js_body.strip()) > 0, (
377
+ "Worker JS should not be empty"
378
+ )
379
+ assert (
380
+ "postMessage" in worker_js_body or "onmessage" in worker_js_body
381
+ ), "Worker JS should contain a message handler"
382
+ except (URLError, HTTPError, TimeoutError, RemoteDisconnected) as exc:
383
+ print(f"[DEBUG] Error while requesting /workers/worker.js: {exc}")
384
+ pytest.fail(
385
+ f"Failed to GET /workers/worker.js after retries: {exc}"
386
+ )
387
+
262
388
  # "/walker/get_server_message" – walkers are integrated and up and running
263
389
  try:
264
390
  print("[DEBUG] Sending GET request to /walker/get_server_message")
@@ -312,6 +438,153 @@ def test_all_in_one_app_endpoints() -> None:
312
438
  print(f"[DEBUG] Error while requesting /walker/create_todo: {exc}")
313
439
  pytest.fail("Failed to POST /walker/create_todo")
314
440
 
441
+ # POST /user/register – register a new user
442
+ test_username = "test_user"
443
+ test_password = "test_password_123"
444
+ try:
445
+ print("[DEBUG] Sending POST request to /user/register endpoint")
446
+ register_payload = {
447
+ "username": test_username,
448
+ "password": test_password,
449
+ }
450
+ req_register = Request(
451
+ "http://127.0.0.1:8000/user/register",
452
+ data=json.dumps(register_payload).encode("utf-8"),
453
+ headers={"Content-Type": "application/json"},
454
+ method="POST",
455
+ )
456
+ with urlopen(req_register, timeout=20) as resp_register:
457
+ register_body = resp_register.read().decode(
458
+ "utf-8", errors="ignore"
459
+ )
460
+ print(
461
+ "[DEBUG] Received response from /user/register\n"
462
+ f"Status: {resp_register.status}\n"
463
+ f"Body (truncated to 500 chars):\n{register_body[:500]}"
464
+ )
465
+ assert resp_register.status == 201
466
+ register_data = json.loads(register_body)
467
+ assert "username" in register_data
468
+ assert "token" in register_data
469
+ assert "root_id" in register_data
470
+ assert register_data["username"] == test_username
471
+ assert len(register_data["token"]) > 0
472
+ assert len(register_data["root_id"]) > 0
473
+ print(
474
+ f"[DEBUG] Successfully registered user: {test_username}\n"
475
+ f"Token: {register_data['token'][:20]}...\n"
476
+ f"Root ID: {register_data['root_id']}"
477
+ )
478
+ except (URLError, HTTPError) as exc:
479
+ print(f"[DEBUG] Error while requesting /user/register: {exc}")
480
+ pytest.fail("Failed to POST /user/register")
481
+
482
+ # POST /user/login – login with registered credentials
483
+ try:
484
+ print("[DEBUG] Sending POST request to /user/login endpoint")
485
+ login_payload = {
486
+ "username": test_username,
487
+ "password": test_password,
488
+ }
489
+ req_login = Request(
490
+ "http://127.0.0.1:8000/user/login",
491
+ data=json.dumps(login_payload).encode("utf-8"),
492
+ headers={"Content-Type": "application/json"},
493
+ method="POST",
494
+ )
495
+ with urlopen(req_login, timeout=20) as resp_login:
496
+ login_body = resp_login.read().decode("utf-8", errors="ignore")
497
+ print(
498
+ "[DEBUG] Received response from /user/login\n"
499
+ f"Status: {resp_login.status}\n"
500
+ f"Body (truncated to 500 chars):\n{login_body[:500]}"
501
+ )
502
+ assert resp_login.status == 200
503
+ login_data = json.loads(login_body)
504
+ assert "token" in login_data
505
+ assert len(login_data["token"]) > 0
506
+ print(
507
+ f"[DEBUG] Successfully logged in user: {test_username}\n"
508
+ f"Token: {login_data['token'][:20]}..."
509
+ )
510
+ except (URLError, HTTPError) as exc:
511
+ print(f"[DEBUG] Error while requesting /user/login: {exc}")
512
+ pytest.fail("Failed to POST /user/login")
513
+
514
+ # POST /user/login – test login with invalid credentials
515
+ try:
516
+ print(
517
+ "[DEBUG] Sending POST request to /user/login with invalid credentials"
518
+ )
519
+ invalid_login_payload = {
520
+ "username": "nonexistent_user",
521
+ "password": "wrong_password",
522
+ }
523
+ req_invalid_login = Request(
524
+ "http://127.0.0.1:8000/user/login",
525
+ data=json.dumps(invalid_login_payload).encode("utf-8"),
526
+ headers={"Content-Type": "application/json"},
527
+ method="POST",
528
+ )
529
+ try:
530
+ with urlopen(req_invalid_login, timeout=20) as resp_invalid:
531
+ # If we get here, the request succeeded but should have failed
532
+ invalid_body = resp_invalid.read().decode(
533
+ "utf-8", errors="ignore"
534
+ )
535
+ print(
536
+ "[DEBUG] Received response from /user/login (invalid creds)\n"
537
+ f"Status: {resp_invalid.status}\n"
538
+ f"Body: {invalid_body}"
539
+ )
540
+ # Login should fail with invalid credentials
541
+ assert (
542
+ resp_invalid.status != 200
543
+ or "error" in invalid_body.lower()
544
+ )
545
+ except HTTPError as http_err:
546
+ # Expected: login should fail with 401 or similar
547
+ print(
548
+ f"[DEBUG] Expected error for invalid login: {http_err.code} {http_err.reason}"
549
+ )
550
+ # Close the underlying response to release the socket
551
+ http_err.close()
552
+ assert http_err.code in (400, 401, 403), (
553
+ f"Expected 400/401/403 for invalid login, got {http_err.code}"
554
+ )
555
+ except URLError as exc:
556
+ print(
557
+ f"[DEBUG] Unexpected error while testing invalid login: {exc}"
558
+ )
559
+ pytest.fail("Unexpected error testing invalid login")
560
+
561
+ # Verify TypeScript component is working - check that page loads with TS component
562
+ # The /page/app endpoint should serve the app which includes the TypeScript Card component
563
+ try:
564
+ print("[DEBUG] Verifying TypeScript component integration")
565
+ # The page should load successfully (already tested above)
566
+ # TypeScript components are compiled and included in the bundle
567
+ # We verify this by checking the page loads without errors
568
+ assert "<html" in page_body.lower(), "Page should contain HTML"
569
+ except Exception as exc:
570
+ print(f"[DEBUG] Error verifying TypeScript component: {exc}")
571
+ pytest.fail("Failed to verify TypeScript component integration")
572
+
573
+ # Verify nested folder imports are working - /page/app#/nested route
574
+ # This route uses nested folder imports (components.button and button)
575
+ try:
576
+ print(
577
+ "[DEBUG] Verifying nested folder imports via /page/app#/nested"
578
+ )
579
+ # The nested route should load successfully (already tested above)
580
+ # Nested imports are compiled and included in the bundle
581
+ assert "<html" in nested_body.lower(), (
582
+ "Nested route should contain HTML"
583
+ )
584
+ except Exception as exc:
585
+ print(f"[DEBUG] Error verifying nested folder imports: {exc}")
586
+ pytest.fail("Failed to verify nested folder imports")
587
+
315
588
  finally:
316
589
  if server is not None:
317
590
  print("[DEBUG] Terminating server process")
@@ -324,6 +597,13 @@ def test_all_in_one_app_endpoints() -> None:
324
597
  "[DEBUG] Server did not terminate cleanly, killing process"
325
598
  )
326
599
  server.kill()
600
+ server.wait(timeout=5)
601
+ # Allow time for sockets to fully close and run garbage collection
602
+ # to clean up any lingering socket objects before temp dir cleanup
603
+ time.sleep(1)
604
+ gc.collect()
327
605
  finally:
328
606
  print(f"[DEBUG] Restoring original working directory to {original_cwd}")
329
607
  os.chdir(original_cwd)
608
+ # Final garbage collection to ensure all resources are released
609
+ gc.collect()
@@ -1,24 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jac-client
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Build full-stack web applications with Jac - one language for frontend and backend.
5
- License: MIT
6
- Keywords: jac,jaclang,jaseci,frontend,full-stack,web-development
7
- Author: Jason Mars
8
- Author-email: jason@mars.ninja
9
- Maintainer: Jason Mars
10
- Maintainer-email: jason@mars.ninja
11
- Requires-Python: >=3.12.0,<4.0.0
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Programming Language :: Python :: 3.14
17
- Requires-Dist: jaclang (==0.9.3)
18
- Project-URL: Documentation, https://jac-lang.org
19
- Project-URL: Homepage, https://jaseci.org
5
+ Author-email: Jason Mars <jason@mars.ninja>
6
+ Maintainer-email: Jason Mars <jason@mars.ninja>
7
+ License-Expression: MIT
20
8
  Project-URL: Repository, https://github.com/Jaseci-Labs/jaseci
9
+ Project-URL: Homepage, https://jaseci.org
10
+ Project-URL: Documentation, https://jac-lang.org
11
+ Keywords: jac,jaclang,jaseci,frontend,full-stack,web-development
12
+ Requires-Python: >=3.12
21
13
  Description-Content-Type: text/markdown
14
+ Requires-Dist: jaclang==0.9.4
15
+ Provides-Extra: dev
16
+ Requires-Dist: python-dotenv==1.0.1; extra == "dev"
17
+ Requires-Dist: pytest==8.3.5; extra == "dev"
22
18
 
23
19
  # Jac Client
24
20
 
@@ -28,7 +24,7 @@ Jac Client enables you to write React-like components, manage state, and build i
28
24
 
29
25
  ---
30
26
 
31
- ## Features
27
+ ## Features
32
28
 
33
29
  - **Single Language**: Write frontend and backend in Jac
34
30
  - **No HTTP Client**: Use `jacSpawn()` instead of fetch/axios
@@ -40,7 +36,7 @@ Jac Client enables you to write React-like components, manage state, and build i
40
36
 
41
37
  ---
42
38
 
43
- ## 🚀 Quick Start
39
+ ## Quick Start
44
40
 
45
41
  ### Installation
46
42
 
@@ -51,16 +47,18 @@ pip install jac-client
51
47
  ### Create a New App
52
48
 
53
49
  ```bash
54
- jac create_jac_app my-app
50
+ jac create --cl my-app
55
51
  cd my-app
56
- jac serve app.jac
52
+ jac serve src/app.jac
57
53
  ```
58
54
 
59
55
  Visit `http://localhost:8000/page/app` to see your app!
60
56
 
57
+ > **Note**: The `--cl` flag creates a client-side project with an organized folder structure. Without `--cl`, `jac create` creates a standard Jac project.
58
+
61
59
  ---
62
60
 
63
- ## 📚 Documentation
61
+ ## Documentation
64
62
 
65
63
  For detailed guides and tutorials, see the **[docs folder](jac_client/docs/)**:
66
64
 
@@ -72,7 +70,7 @@ For detailed guides and tutorials, see the **[docs folder](jac_client/docs/)**:
72
70
 
73
71
  ---
74
72
 
75
- ## 💡 Example
73
+ ## Example
76
74
 
77
75
  ### Simple Counter with React Hooks
78
76
 
@@ -81,7 +79,7 @@ cl import from react { useState, useEffect }
81
79
 
82
80
  cl {
83
81
  def Counter() -> any {
84
- let [count, setCount] = useState(0);
82
+ [count, setCount] = useState(0);
85
83
 
86
84
  useEffect(lambda -> None {
87
85
  console.log("Count changed:", count);
@@ -132,7 +130,7 @@ walker read_todos {
132
130
  # Frontend: React component
133
131
  cl {
134
132
  def app() -> any {
135
- let [todos, setTodos] = useState([]);
133
+ [todos, setTodos] = useState([]);
136
134
 
137
135
  useEffect(lambda -> None {
138
136
  async def loadTodos() -> None {
@@ -154,7 +152,7 @@ cl {
154
152
 
155
153
  ---
156
154
 
157
- ## 🔧 Requirements
155
+ ## Requirements
158
156
 
159
157
  - Python: 3.12+
160
158
  - Node.js: For npm and Vite
@@ -162,9 +160,10 @@ cl {
162
160
 
163
161
  ---
164
162
 
165
- ## 🛠️ How It Works
163
+ ## How It Works
166
164
 
167
165
  Jac Client is a plugin that:
166
+
168
167
  1. Compiles your `.jac` client code to JavaScript
169
168
  2. Bundles dependencies with Vite for optimal performance
170
169
  3. Provides a runtime for reactive state and components
@@ -172,7 +171,7 @@ Jac Client is a plugin that:
172
171
 
173
172
  ---
174
173
 
175
- ## 📖 Learn More
174
+ ## Learn More
176
175
 
177
176
  - **Full Documentation**: See [docs/](jac_client/docs/) for comprehensive guides
178
177
  - **Examples**: Check `jac_client/examples/` for working examples
@@ -180,11 +179,10 @@ Jac Client is a plugin that:
180
179
 
181
180
  ---
182
181
 
183
- ## 📄 License
182
+ ## License
184
183
 
185
184
  MIT License - see [LICENSE](../LICENSE) file.
186
185
 
187
186
  ---
188
187
 
189
- **Happy coding with Jac!** 🎉
190
-
188
+ **Happy coding with Jac!**