jac-client 0.2.6__py3-none-any.whl → 0.2.8__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 (66) hide show
  1. jac_client/examples/all-in-one/app.jac +573 -0
  2. jac_client/examples/all-in-one/components/CategoryFilter.jac +35 -0
  3. jac_client/examples/all-in-one/components/Header.jac +13 -0
  4. jac_client/examples/all-in-one/components/ProfitOverview.jac +50 -0
  5. jac_client/examples/all-in-one/components/Summary.jac +53 -0
  6. jac_client/examples/all-in-one/components/TransactionForm.jac +158 -0
  7. jac_client/examples/all-in-one/components/TransactionItem.jac +55 -0
  8. jac_client/examples/all-in-one/components/TransactionList.jac +37 -0
  9. jac_client/examples/all-in-one/components/navigation.jac +132 -0
  10. jac_client/examples/all-in-one/constants/categories.jac +37 -0
  11. jac_client/examples/all-in-one/constants/clients.jac +13 -0
  12. jac_client/examples/all-in-one/context/BudgetContext.jac +28 -0
  13. jac_client/examples/all-in-one/hooks/useBudget.jac +116 -0
  14. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +36 -0
  15. jac_client/examples/all-in-one/pages/BudgetPlanner.cl.jac +70 -0
  16. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +126 -0
  17. jac_client/examples/all-in-one/pages/FeaturesTest.cl.jac +552 -0
  18. jac_client/examples/all-in-one/pages/FeaturesTest.jac +126 -0
  19. jac_client/examples/all-in-one/pages/LandingPage.jac +101 -0
  20. jac_client/examples/all-in-one/pages/loginPage.jac +132 -0
  21. jac_client/examples/all-in-one/pages/nestedDemo.jac +61 -0
  22. jac_client/examples/all-in-one/pages/notFound.jac +24 -0
  23. jac_client/examples/all-in-one/pages/signupPage.jac +133 -0
  24. jac_client/examples/all-in-one/utils/formatters.jac +52 -0
  25. jac_client/examples/asset-serving/css-with-image/src/app.jac +3 -3
  26. jac_client/examples/asset-serving/image-asset/src/app.jac +3 -3
  27. jac_client/examples/asset-serving/import-alias/src/app.jac +3 -3
  28. jac_client/examples/basic/src/app.jac +3 -3
  29. jac_client/examples/basic-auth/src/app.jac +31 -37
  30. jac_client/examples/basic-auth-with-router/src/app.jac +16 -16
  31. jac_client/examples/basic-full-stack/src/app.jac +24 -30
  32. jac_client/examples/css-styling/js-styling/src/app.jac +5 -5
  33. jac_client/examples/css-styling/material-ui/src/app.jac +5 -5
  34. jac_client/examples/css-styling/pure-css/src/app.jac +5 -5
  35. jac_client/examples/css-styling/sass-example/src/app.jac +5 -5
  36. jac_client/examples/css-styling/styled-components/src/app.jac +5 -5
  37. jac_client/examples/css-styling/tailwind-example/src/app.jac +5 -5
  38. jac_client/examples/full-stack-with-auth/src/app.jac +16 -16
  39. jac_client/examples/ts-support/src/app.jac +4 -4
  40. jac_client/examples/with-router/src/app.jac +4 -4
  41. jac_client/plugin/cli.jac +160 -203
  42. jac_client/plugin/client.jac +8 -15
  43. jac_client/plugin/client_runtime.cl.jac +18 -14
  44. jac_client/plugin/impl/client.impl.jac +85 -26
  45. jac_client/plugin/impl/client_runtime.impl.jac +27 -9
  46. jac_client/plugin/plugin_config.jac +11 -11
  47. jac_client/plugin/src/compiler.jac +2 -1
  48. jac_client/plugin/src/impl/babel_processor.impl.jac +22 -17
  49. jac_client/plugin/src/impl/compiler.impl.jac +55 -18
  50. jac_client/plugin/src/impl/vite_bundler.impl.jac +215 -102
  51. jac_client/plugin/src/package_installer.jac +1 -1
  52. jac_client/plugin/src/vite_bundler.jac +9 -1
  53. jac_client/tests/conftest.py +10 -8
  54. jac_client/tests/fixtures/spawn_test/app.jac +15 -18
  55. jac_client/tests/fixtures/with-ts/app.jac +4 -4
  56. jac_client/tests/test_cli.py +105 -49
  57. jac_client/tests/test_it.py +297 -82
  58. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/METADATA +16 -7
  59. jac_client-0.2.8.dist-info/RECORD +97 -0
  60. jac_client/examples/all-in-one/src/app.jac +0 -841
  61. jac_client-0.2.6.dist-info/RECORD +0 -74
  62. /jac_client/examples/all-in-one/{src/button.jac → button.jac} +0 -0
  63. /jac_client/examples/all-in-one/{src/components → components}/button.jac +0 -0
  64. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/WHEEL +0 -0
  65. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/entry_points.txt +0 -0
  66. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- """End-to-end tests for `jac serve` HTTP endpoints."""
1
+ """End-to-end tests for `jac start` HTTP endpoints."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -7,9 +7,11 @@ import json
7
7
  import os
8
8
  import shutil
9
9
  import socket
10
+ import sys
10
11
  import tempfile
11
12
  import time
12
13
  from http.client import RemoteDisconnected
14
+ from pathlib import Path
13
15
  from subprocess import PIPE, Popen, run
14
16
  from urllib.error import HTTPError, URLError
15
17
  from urllib.request import Request, urlopen
@@ -19,6 +21,37 @@ import pytest
19
21
  from jaclang.pycore.runtime import JacRuntime as Jac
20
22
 
21
23
 
24
+ def _get_jac_command() -> list[str]:
25
+ """Get the jac command with proper path handling."""
26
+ jac_path = shutil.which("jac")
27
+ if jac_path:
28
+ return [jac_path]
29
+ return [sys.executable, "-m", "jaclang"]
30
+
31
+
32
+ def _get_env_with_npm() -> dict[str, str]:
33
+ """Get environment dict with npm in PATH."""
34
+ env = os.environ.copy()
35
+ npm_path = shutil.which("npm")
36
+ if npm_path:
37
+ npm_dir = str(Path(npm_path).parent)
38
+ current_path = env.get("PATH", "")
39
+ if npm_dir not in current_path:
40
+ env["PATH"] = f"{npm_dir}:{current_path}"
41
+ # Also check common nvm locations
42
+ nvm_dir = os.environ.get("NVM_DIR", os.path.expanduser("~/.nvm"))
43
+ nvm_node_bin = Path(nvm_dir) / "versions" / "node"
44
+ if nvm_node_bin.exists():
45
+ for version_dir in nvm_node_bin.iterdir():
46
+ bin_dir = version_dir / "bin"
47
+ if bin_dir.exists() and (bin_dir / "npm").exists():
48
+ current_path = env.get("PATH", "")
49
+ if str(bin_dir) not in current_path:
50
+ env["PATH"] = f"{bin_dir}:{current_path}"
51
+ break
52
+ return env
53
+
54
+
22
55
  @pytest.fixture(autouse=True)
23
56
  def reset_jac_machine():
24
57
  """Reset Jac machine before and after each test."""
@@ -207,99 +240,84 @@ def test_all_in_one_app_endpoints() -> None:
207
240
  f"STDERR:\n{jac_add_result.stderr}\n"
208
241
  )
209
242
 
210
- app_jac_path = os.path.join(project_path, "src", "app.jac")
243
+ app_jac_path = os.path.join(project_path, "app.jac")
211
244
  assert os.path.isfile(app_jac_path), "all-in-one src/app.jac file missing"
212
245
 
213
- # 4. Start the server: `jac serve src/app.jac`
246
+ # 4. Start the server: `jac start src/app.jac`
214
247
  # NOTE: We don't use text mode here, so `Popen` defaults to bytes.
215
248
  # Use `Popen[bytes]` in the type annotation to keep mypy happy.
216
249
  server: Popen[bytes] | None = None
217
250
  try:
218
- print("[DEBUG] Starting server with 'jac serve src/app.jac'")
251
+ print("[DEBUG] Starting server with 'jac start src/app.jac'")
219
252
  server = Popen(
220
- ["jac", "serve", "src/app.jac"],
253
+ ["jac", "start", "src/app.jac"],
221
254
  cwd=project_path,
222
255
  )
223
-
224
256
  # Wait for localhost:8000 to become available
225
257
  print("[DEBUG] Waiting for server to be available on 127.0.0.1:8000")
226
258
  _wait_for_port("127.0.0.1", 8000, timeout=90.0)
227
259
  print("[DEBUG] Server is now accepting connections on 127.0.0.1:8000")
228
260
 
229
- # "/" – server up
261
+ # "/" – server up (serves client app HTML due to base_route_app="app")
262
+ # Note: The root endpoint may return 503 while the client bundle is building.
263
+ # We use _wait_for_endpoint to retry on 503 until it's ready.
230
264
  try:
231
- print("[DEBUG] Sending GET request to root endpoint /")
232
- with urlopen(
265
+ print("[DEBUG] Sending GET request to root endpoint / (with retry)")
266
+ root_bytes = _wait_for_endpoint(
233
267
  "http://127.0.0.1:8000",
234
- timeout=10,
235
- ) as resp_root:
236
- root_body = resp_root.read().decode("utf-8", errors="ignore")
237
- print(
238
- "[DEBUG] Received response from root endpoint /\n"
239
- f"Status: {resp_root.status}\n"
240
- f"Body (truncated to 500 chars):\n{root_body[:500]}"
241
- )
242
- assert resp_root.status == 200
243
- assert '"Jac API Server"' in root_body
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
- )
262
- except (URLError, HTTPError) as exc:
268
+ timeout=120.0,
269
+ poll_interval=2.0,
270
+ request_timeout=30.0,
271
+ )
272
+ root_body = root_bytes.decode("utf-8", errors="ignore")
273
+ print(
274
+ "[DEBUG] Received response from root endpoint /\n"
275
+ f"Body (truncated to 500 chars):\n{root_body[:500]}"
276
+ )
277
+ # With base_route_app="app", root serves client HTML
278
+ assert "<!DOCTYPE html>" in root_body or "<html" in root_body
279
+ assert '<div id="root">' in root_body
280
+ except (URLError, HTTPError, TimeoutError) as exc:
263
281
  print(f"[DEBUG] Error while requesting root endpoint: {exc}")
264
282
  pytest.fail(f"Failed to GET root endpoint: {exc}")
265
283
 
266
- # "/page/app" – main page is loading
284
+ # "/cl/app" – main page is loading
267
285
  # Note: This endpoint may return 503 (temporary) while the page is being compiled,
268
286
  # or 500 (permanent) if there's a compilation error. We use _wait_for_endpoint
269
287
  # to retry on 503 until it's ready, but it will fail immediately on 500.
270
288
  try:
271
289
  print(
272
- "[DEBUG] Sending GET request to /page/app endpoint (with retry)"
290
+ "[DEBUG] Sending GET request to /cl/app endpoint (with retry)"
273
291
  )
274
292
  page_bytes = _wait_for_endpoint(
275
- "http://127.0.0.1:8000/page/app",
293
+ "http://127.0.0.1:8000/cl/app",
276
294
  timeout=120.0,
277
295
  poll_interval=2.0,
278
296
  request_timeout=30.0,
279
297
  )
280
298
  page_body = page_bytes.decode("utf-8", errors="ignore")
281
299
  print(
282
- "[DEBUG] Received response from /page/app endpoint\n"
300
+ "[DEBUG] Received response from /cl/app endpoint\n"
283
301
  f"Body (truncated to 500 chars):\n{page_body[:500]}"
284
302
  )
285
303
  assert "<html" in page_body.lower()
286
304
  except (URLError, HTTPError, TimeoutError, RemoteDisconnected) as exc:
287
- print(f"[DEBUG] Error while requesting /page/app endpoint: {exc}")
288
- pytest.fail(f"Failed to GET /page/app endpoint: {exc}")
305
+ print(f"[DEBUG] Error while requesting /cl/app endpoint: {exc}")
306
+ pytest.fail(f"Failed to GET /cl/app endpoint: {exc}")
289
307
 
290
- # "/page/app#/nested" – relative paths / nested route
308
+ # "/cl/app#/nested" – relative paths / nested route
291
309
  # (hash fragment is client-side only but server should still serve the app shell)
292
310
  try:
293
- print("[DEBUG] Sending GET request to /page/app#/nested endpoint")
311
+ print("[DEBUG] Sending GET request to /cl/app#/nested endpoint")
294
312
  with urlopen(
295
- "http://127.0.0.1:8000/page/app#/nested",
313
+ "http://127.0.0.1:8000/cl/app#/nested",
296
314
  timeout=200,
297
315
  ) as resp_nested:
298
316
  nested_body = resp_nested.read().decode(
299
317
  "utf-8", errors="ignore"
300
318
  )
301
319
  print(
302
- "[DEBUG] Received response from /page/app#/nested endpoint\n"
320
+ "[DEBUG] Received response from /cl/app#/nested endpoint\n"
303
321
  f"Status: {resp_nested.status}\n"
304
322
  f"Body (truncated to 500 chars):\n{nested_body[:500]}"
305
323
  )
@@ -307,31 +325,12 @@ def test_all_in_one_app_endpoints() -> None:
307
325
  assert "<html" in nested_body.lower()
308
326
  except (URLError, HTTPError) as exc:
309
327
  print(
310
- f"[DEBUG] Error while requesting /page/app#/nested endpoint: {exc}"
328
+ f"[DEBUG] Error while requesting /cl/app#/nested endpoint: {exc}"
311
329
  )
312
- pytest.fail("Failed to GET /page/app#/nested endpoint")
330
+ pytest.fail("Failed to GET /cl/app#/nested endpoint")
313
331
 
314
- # "/static/main.css" CSS compiled and serving
315
- # Note: CSS may be compiled asynchronously, so we retry if it's not ready
316
- try:
317
- print(
318
- "[DEBUG] Sending GET request to /static/main.css (with retry)"
319
- )
320
- css_bytes = _wait_for_endpoint(
321
- "http://127.0.0.1:8000/static/main.css",
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:
333
- print(f"[DEBUG] Error while requesting /static/main.css: {exc}")
334
- pytest.fail(f"Failed to GET /static/main.css after retries: {exc}")
332
+ # Note: CSS serving is tested separately in test_css_with_image
333
+ # The CSS is bundled into client.js so no separate /static/styles.css endpoint
335
334
 
336
335
  # "/static/assets/burger.png" – static files are loading
337
336
  try:
@@ -434,7 +433,7 @@ def test_all_in_one_app_endpoints() -> None:
434
433
  assert resp_create.status == 200
435
434
  # Basic sanity check: created Todo text should appear in the response payload.
436
435
  assert "Sample todo from all-in-one app" in create_body
437
- except (URLError, HTTPError) as exc:
436
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
438
437
  print(f"[DEBUG] Error while requesting /walker/create_todo: {exc}")
439
438
  pytest.fail("Failed to POST /walker/create_todo")
440
439
 
@@ -463,7 +462,9 @@ def test_all_in_one_app_endpoints() -> None:
463
462
  f"Body (truncated to 500 chars):\n{register_body[:500]}"
464
463
  )
465
464
  assert resp_register.status == 201
466
- register_data = json.loads(register_body)
465
+ register_response = json.loads(register_body)
466
+ # Handle new TransportResponse envelope format
467
+ register_data = register_response.get("data", register_response)
467
468
  assert "username" in register_data
468
469
  assert "token" in register_data
469
470
  assert "root_id" in register_data
@@ -475,7 +476,7 @@ def test_all_in_one_app_endpoints() -> None:
475
476
  f"Token: {register_data['token'][:20]}...\n"
476
477
  f"Root ID: {register_data['root_id']}"
477
478
  )
478
- except (URLError, HTTPError) as exc:
479
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
479
480
  print(f"[DEBUG] Error while requesting /user/register: {exc}")
480
481
  pytest.fail("Failed to POST /user/register")
481
482
 
@@ -500,14 +501,16 @@ def test_all_in_one_app_endpoints() -> None:
500
501
  f"Body (truncated to 500 chars):\n{login_body[:500]}"
501
502
  )
502
503
  assert resp_login.status == 200
503
- login_data = json.loads(login_body)
504
+ login_response = json.loads(login_body)
505
+ # Handle new TransportResponse envelope format
506
+ login_data = login_response.get("data", login_response)
504
507
  assert "token" in login_data
505
508
  assert len(login_data["token"]) > 0
506
509
  print(
507
510
  f"[DEBUG] Successfully logged in user: {test_username}\n"
508
511
  f"Token: {login_data['token'][:20]}..."
509
512
  )
510
- except (URLError, HTTPError) as exc:
513
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
511
514
  print(f"[DEBUG] Error while requesting /user/login: {exc}")
512
515
  pytest.fail("Failed to POST /user/login")
513
516
 
@@ -552,14 +555,14 @@ def test_all_in_one_app_endpoints() -> None:
552
555
  assert http_err.code in (400, 401, 403), (
553
556
  f"Expected 400/401/403 for invalid login, got {http_err.code}"
554
557
  )
555
- except URLError as exc:
558
+ except (URLError, RemoteDisconnected) as exc:
556
559
  print(
557
560
  f"[DEBUG] Unexpected error while testing invalid login: {exc}"
558
561
  )
559
562
  pytest.fail("Unexpected error testing invalid login")
560
563
 
561
564
  # 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
565
+ # The /cl/app endpoint should serve the app which includes the TypeScript Card component
563
566
  try:
564
567
  print("[DEBUG] Verifying TypeScript component integration")
565
568
  # The page should load successfully (already tested above)
@@ -570,12 +573,10 @@ def test_all_in_one_app_endpoints() -> None:
570
573
  print(f"[DEBUG] Error verifying TypeScript component: {exc}")
571
574
  pytest.fail("Failed to verify TypeScript component integration")
572
575
 
573
- # Verify nested folder imports are working - /page/app#/nested route
576
+ # Verify nested folder imports are working - /cl/app#/nested route
574
577
  # This route uses nested folder imports (components.button and button)
575
578
  try:
576
- print(
577
- "[DEBUG] Verifying nested folder imports via /page/app#/nested"
578
- )
579
+ print("[DEBUG] Verifying nested folder imports via /cl/app#/nested")
579
580
  # The nested route should load successfully (already tested above)
580
581
  # Nested imports are compiled and included in the bundle
581
582
  assert "<html" in nested_body.lower(), (
@@ -607,3 +608,217 @@ def test_all_in_one_app_endpoints() -> None:
607
608
  os.chdir(original_cwd)
608
609
  # Final garbage collection to ensure all resources are released
609
610
  gc.collect()
611
+
612
+
613
+ def test_default_client_app_renders() -> None:
614
+ """Test that a default `jac create --cl` app renders correctly when served.
615
+
616
+ This test validates the out-of-the-box experience:
617
+ 1. Creates a new client app using `jac create --cl`
618
+ 2. Installs packages
619
+ 3. Starts the server
620
+ 4. Validates that the default app renders with expected content
621
+ """
622
+ print("[DEBUG] Starting test_default_client_app_renders")
623
+
624
+ app_name = "e2e-default-app"
625
+
626
+ with tempfile.TemporaryDirectory() as temp_dir:
627
+ print(f"[DEBUG] Created temporary directory at {temp_dir}")
628
+ original_cwd = os.getcwd()
629
+ try:
630
+ os.chdir(temp_dir)
631
+ print(f"[DEBUG] Changed working directory to {temp_dir}")
632
+
633
+ # 1. Create a new default Jac client app
634
+ jac_cmd = _get_jac_command()
635
+ env = _get_env_with_npm()
636
+ print(f"[DEBUG] Running '{' '.join(jac_cmd)} create --cl {app_name}'")
637
+ process = Popen(
638
+ [*jac_cmd, "create", "--cl", app_name],
639
+ stdin=PIPE,
640
+ stdout=PIPE,
641
+ stderr=PIPE,
642
+ text=True,
643
+ env=env,
644
+ )
645
+ stdout, stderr = process.communicate()
646
+ returncode = process.returncode
647
+
648
+ print(
649
+ f"[DEBUG] 'jac create --cl' completed returncode={returncode}\n"
650
+ f"STDOUT:\n{stdout}\n"
651
+ f"STDERR:\n{stderr}\n"
652
+ )
653
+
654
+ if returncode != 0 and "unrecognized arguments: --cl" in stderr:
655
+ pytest.fail(
656
+ "Test failed: installed `jac` CLI does not support `create --cl`."
657
+ )
658
+
659
+ assert returncode == 0, (
660
+ f"jac create --cl failed\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}\n"
661
+ )
662
+
663
+ project_path = os.path.join(temp_dir, app_name)
664
+ print(f"[DEBUG] Created default Jac client app at {project_path}")
665
+ assert os.path.isdir(project_path)
666
+
667
+ # Verify expected files were created (new structure: main.jac at root)
668
+ main_jac_path = os.path.join(project_path, "main.jac")
669
+ assert os.path.isfile(main_jac_path), (
670
+ "main.jac should exist at project root"
671
+ )
672
+
673
+ # Components are now at root level (not src/components)
674
+ button_jac_path = os.path.join(project_path, "components", "Button.cl.jac")
675
+ assert os.path.isfile(button_jac_path), (
676
+ "components/Button.cl.jac should exist"
677
+ )
678
+
679
+ jac_toml_path = os.path.join(project_path, "jac.toml")
680
+ assert os.path.isfile(jac_toml_path), "jac.toml should exist"
681
+
682
+ # 2. Ensure packages are installed (jac create --cl should have done this)
683
+ # If node_modules doesn't exist, run jac add --cl
684
+ node_modules_path = os.path.join(
685
+ project_path, ".jac", "client", "node_modules"
686
+ )
687
+ if not os.path.isdir(node_modules_path):
688
+ print("[DEBUG] node_modules not found, running 'jac add --cl'")
689
+ jac_add_result = run(
690
+ [*jac_cmd, "add", "--cl"],
691
+ cwd=project_path,
692
+ capture_output=True,
693
+ text=True,
694
+ env=env,
695
+ )
696
+ print(
697
+ f"[DEBUG] 'jac add --cl' completed returncode={jac_add_result.returncode}\n"
698
+ f"STDOUT (truncated):\n{jac_add_result.stdout[:1000]}\n"
699
+ f"STDERR (truncated):\n{jac_add_result.stderr[:1000]}\n"
700
+ )
701
+ if jac_add_result.returncode != 0:
702
+ pytest.fail(
703
+ f"jac add --cl failed\n"
704
+ f"STDOUT:\n{jac_add_result.stdout}\n"
705
+ f"STDERR:\n{jac_add_result.stderr}\n"
706
+ )
707
+
708
+ # 3. Start the server (now uses main.jac at project root)
709
+ server: Popen[bytes] | None = None
710
+ try:
711
+ print("[DEBUG] Starting server with 'jac start main.jac'")
712
+ server = Popen(
713
+ [*jac_cmd, "start", "main.jac"],
714
+ cwd=project_path,
715
+ env=env,
716
+ )
717
+
718
+ # Wait for server to be ready
719
+ print("[DEBUG] Waiting for server on 127.0.0.1:8000")
720
+ _wait_for_port("127.0.0.1", 8000, timeout=90.0)
721
+ print("[DEBUG] Server is accepting connections")
722
+
723
+ # 4. Test root endpoint - for client-only apps, root serves the HTML app
724
+ # Note: The root endpoint may return 503 while the client bundle is building.
725
+ # We use _wait_for_endpoint to retry on 503 until it's ready.
726
+ try:
727
+ print("[DEBUG] Testing root endpoint / (with retry)")
728
+ root_bytes = _wait_for_endpoint(
729
+ "http://127.0.0.1:8000",
730
+ timeout=120.0,
731
+ poll_interval=2.0,
732
+ request_timeout=30.0,
733
+ )
734
+ root_body = root_bytes.decode("utf-8", errors="ignore")
735
+ print(
736
+ f"[DEBUG] Root response:\nBody (truncated):\n{root_body[:500]}"
737
+ )
738
+ # For client-only apps, root returns HTML with the React app
739
+ assert "<html" in root_body.lower(), (
740
+ "Root should return HTML for client-only app"
741
+ )
742
+ assert "<script" in root_body.lower(), (
743
+ "Root should include script tag for client bundle"
744
+ )
745
+ except (URLError, HTTPError, TimeoutError) as exc:
746
+ print(f"[DEBUG] Error at root endpoint: {exc}")
747
+ pytest.fail(f"Failed to GET root endpoint: {exc}")
748
+
749
+ # 5. Test client app endpoint - the rendered React app
750
+ try:
751
+ print("[DEBUG] Testing client app endpoint /cl/app")
752
+ page_bytes = _wait_for_endpoint(
753
+ "http://127.0.0.1:8000/cl/app",
754
+ timeout=120.0,
755
+ poll_interval=2.0,
756
+ request_timeout=30.0,
757
+ )
758
+ page_body = page_bytes.decode("utf-8", errors="ignore")
759
+ print(
760
+ f"[DEBUG] Client app response:\n"
761
+ f"Body (truncated):\n{page_body[:1000]}"
762
+ )
763
+
764
+ # Validate HTML structure
765
+ assert "<html" in page_body.lower(), "Response should contain HTML"
766
+ assert "<body" in page_body.lower(), "Response should contain body"
767
+
768
+ # The page should include the bundled JavaScript
769
+ # that will render "Hello, World!" client-side
770
+ assert (
771
+ "<script" in page_body.lower() or "src=" in page_body.lower()
772
+ ), "Response should include script tags for React app"
773
+
774
+ except (URLError, HTTPError, TimeoutError) as exc:
775
+ print(f"[DEBUG] Error at /cl/app endpoint: {exc}")
776
+ pytest.fail(f"Failed to GET /cl/app endpoint: {exc}")
777
+
778
+ # 6. Test that static JS bundle is being served
779
+ try:
780
+ print("[DEBUG] Testing that client.js bundle is served")
781
+ # Extract the client.js path from the HTML
782
+ import re
783
+
784
+ script_match = re.search(
785
+ r'src="(/static/client\.js[^"]*)"', root_body
786
+ )
787
+ if script_match:
788
+ js_path = script_match.group(1)
789
+ js_url = f"http://127.0.0.1:8000{js_path}"
790
+ print(f"[DEBUG] Fetching JS bundle from {js_url}")
791
+ with urlopen(js_url, timeout=30) as resp:
792
+ js_body = resp.read().decode("utf-8", errors="ignore")
793
+ assert resp.status == 200, "JS bundle should return 200"
794
+ assert len(js_body) > 0, "JS bundle should not be empty"
795
+ print(
796
+ f"[DEBUG] JS bundle fetched successfully "
797
+ f"({len(js_body)} bytes)"
798
+ )
799
+ else:
800
+ print("[DEBUG] Warning: Could not find client.js in HTML")
801
+ except (URLError, HTTPError) as exc:
802
+ print(f"[DEBUG] Warning: Could not verify static assets: {exc}")
803
+ # Not a hard failure - the main page test is sufficient
804
+
805
+ print("[DEBUG] All default app tests passed!")
806
+
807
+ finally:
808
+ if server is not None:
809
+ print("[DEBUG] Terminating server process")
810
+ server.terminate()
811
+ try:
812
+ server.wait(timeout=15)
813
+ print("[DEBUG] Server terminated cleanly")
814
+ except Exception:
815
+ print("[DEBUG] Server did not terminate cleanly, killing")
816
+ server.kill()
817
+ server.wait(timeout=5)
818
+ time.sleep(1)
819
+ gc.collect()
820
+
821
+ finally:
822
+ print(f"[DEBUG] Restoring working directory to {original_cwd}")
823
+ os.chdir(original_cwd)
824
+ gc.collect()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jac-client
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Build full-stack web applications with Jac - one language for frontend and backend.
5
5
  Author-email: Jason Mars <jason@mars.ninja>
6
6
  Maintainer-email: Jason Mars <jason@mars.ninja>
@@ -11,7 +11,7 @@ Project-URL: Documentation, https://jac-lang.org
11
11
  Keywords: jac,jaclang,jaseci,frontend,full-stack,web-development
12
12
  Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
- Requires-Dist: jaclang==0.9.6
14
+ Requires-Dist: jaclang==0.9.8
15
15
  Provides-Extra: dev
16
16
  Requires-Dist: python-dotenv==1.0.1; extra == "dev"
17
17
  Requires-Dist: pytest==8.3.5; extra == "dev"
@@ -28,7 +28,7 @@ Jac Client enables you to write React-like components, manage state, and build i
28
28
 
29
29
  - **Single Language**: Write frontend and backend in Jac
30
30
  - **No HTTP Client**: Use `jacSpawn()` instead of fetch/axios
31
- - **React Hooks**: Use standard React `useState` and `useEffect` hooks
31
+ - **React Hooks**: Use standard React `useState` and `useEffect` hooks (useState is auto-injected when using `has` variables)
32
32
  - **Component-Based**: Build reusable UI components with JSX
33
33
  - **Graph Database**: Built-in graph data model eliminates need for SQL/NoSQL
34
34
  - **Type Safety**: Type checking across frontend and backend
@@ -49,10 +49,12 @@ pip install jac-client
49
49
  ```bash
50
50
  jac create --cl my-app
51
51
  cd my-app
52
- jac serve src/app.jac
52
+ jac start src/app.jac
53
53
  ```
54
54
 
55
- Visit `http://localhost:8000/page/app` to see your app!
55
+ Visit `http://localhost:8000` to see your app! (The `app` component is served at the root by default.)
56
+
57
+ You can also access the app at `http://localhost:8000/cl/app`.
56
58
 
57
59
  > **Note**: The `--cl` flag creates a client-side project with an organized folder structure. Without `--cl`, `jac create` creates a standard Jac project.
58
60
 
@@ -75,10 +77,13 @@ For detailed guides and tutorials, see the **[docs folder](jac_client/docs/)**:
75
77
  ### Simple Counter with React Hooks
76
78
 
77
79
  ```jac
78
- cl import from react { useState, useEffect }
80
+ # Note: useState is auto-injected when using has variables in cl blocks
81
+ # Only useEffect needs explicit import
82
+ cl import from react { useEffect }
79
83
 
80
84
  cl {
81
85
  def Counter() -> any {
86
+ # useState is automatically available - no import needed!
82
87
  [count, setCount] = useState(0);
83
88
 
84
89
  useEffect(lambda -> None {
@@ -101,10 +106,13 @@ cl {
101
106
  }
102
107
  ```
103
108
 
109
+ > **Note:** When using `has` variables in `cl {}` blocks or `.cl.jac` files, the `useState` import is automatically injected. You only need to explicitly import other hooks like `useEffect`.
110
+
104
111
  ### Full-Stack Todo App
105
112
 
106
113
  ```jac
107
- cl import from react { useState, useEffect }
114
+ # useState is auto-injected, only import useEffect
115
+ cl import from react { useEffect }
108
116
  cl import from '@jac-client/utils' { jacSpawn }
109
117
 
110
118
  # Backend: Jac nodes and walkers
@@ -130,6 +138,7 @@ walker read_todos {
130
138
  # Frontend: React component
131
139
  cl {
132
140
  def app() -> any {
141
+ # useState is automatically available - no import needed!
133
142
  [todos, setTodos] = useState([]);
134
143
 
135
144
  useEffect(lambda -> None {