jac-client 0.2.6__py3-none-any.whl → 0.2.7__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 (61) hide show
  1. jac_client/examples/all-in-one/src/app.jac +473 -741
  2. jac_client/examples/all-in-one/src/components/CategoryFilter.jac +35 -0
  3. jac_client/examples/all-in-one/src/components/Header.jac +13 -0
  4. jac_client/examples/all-in-one/src/components/ProfitOverview.jac +50 -0
  5. jac_client/examples/all-in-one/src/components/Summary.jac +53 -0
  6. jac_client/examples/all-in-one/src/components/TransactionForm.jac +158 -0
  7. jac_client/examples/all-in-one/src/components/TransactionItem.jac +55 -0
  8. jac_client/examples/all-in-one/src/components/TransactionList.jac +37 -0
  9. jac_client/examples/all-in-one/src/components/navigation.jac +132 -0
  10. jac_client/examples/all-in-one/src/constants/categories.jac +37 -0
  11. jac_client/examples/all-in-one/src/constants/clients.jac +13 -0
  12. jac_client/examples/all-in-one/src/context/BudgetContext.jac +28 -0
  13. jac_client/examples/all-in-one/src/hooks/useBudget.jac +116 -0
  14. jac_client/examples/all-in-one/src/hooks/useLocalStorage.jac +36 -0
  15. jac_client/examples/all-in-one/src/pages/BudgetPlanner.cl.jac +70 -0
  16. jac_client/examples/all-in-one/src/pages/BudgetPlanner.jac +126 -0
  17. jac_client/examples/all-in-one/src/pages/FeaturesTest.cl.jac +552 -0
  18. jac_client/examples/all-in-one/src/pages/FeaturesTest.jac +126 -0
  19. jac_client/examples/all-in-one/src/pages/LandingPage.jac +101 -0
  20. jac_client/examples/all-in-one/src/pages/loginPage.jac +132 -0
  21. jac_client/examples/all-in-one/src/pages/nestedDemo.jac +61 -0
  22. jac_client/examples/all-in-one/src/pages/notFound.jac +24 -0
  23. jac_client/examples/all-in-one/src/pages/signupPage.jac +133 -0
  24. jac_client/examples/all-in-one/src/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 +155 -203
  42. jac_client/plugin/client_runtime.cl.jac +5 -1
  43. jac_client/plugin/impl/client.impl.jac +74 -12
  44. jac_client/plugin/plugin_config.jac +11 -11
  45. jac_client/plugin/src/compiler.jac +2 -1
  46. jac_client/plugin/src/impl/babel_processor.impl.jac +22 -17
  47. jac_client/plugin/src/impl/compiler.impl.jac +57 -18
  48. jac_client/plugin/src/impl/vite_bundler.impl.jac +66 -102
  49. jac_client/plugin/src/package_installer.jac +1 -1
  50. jac_client/plugin/src/vite_bundler.jac +1 -0
  51. jac_client/tests/conftest.py +10 -8
  52. jac_client/tests/fixtures/spawn_test/app.jac +15 -18
  53. jac_client/tests/fixtures/with-ts/app.jac +4 -4
  54. jac_client/tests/test_cli.py +99 -45
  55. jac_client/tests/test_it.py +290 -79
  56. {jac_client-0.2.6.dist-info → jac_client-0.2.7.dist-info}/METADATA +16 -7
  57. jac_client-0.2.7.dist-info/RECORD +97 -0
  58. jac_client-0.2.6.dist-info/RECORD +0 -74
  59. {jac_client-0.2.6.dist-info → jac_client-0.2.7.dist-info}/WHEEL +0 -0
  60. {jac_client-0.2.6.dist-info → jac_client-0.2.7.dist-info}/entry_points.txt +0 -0
  61. {jac_client-0.2.6.dist-info → jac_client-0.2.7.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."""
@@ -210,96 +243,81 @@ def test_all_in_one_app_endpoints() -> None:
210
243
  app_jac_path = os.path.join(project_path, "src", "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
 
@@ -475,7 +474,7 @@ def test_all_in_one_app_endpoints() -> None:
475
474
  f"Token: {register_data['token'][:20]}...\n"
476
475
  f"Root ID: {register_data['root_id']}"
477
476
  )
478
- except (URLError, HTTPError) as exc:
477
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
479
478
  print(f"[DEBUG] Error while requesting /user/register: {exc}")
480
479
  pytest.fail("Failed to POST /user/register")
481
480
 
@@ -507,7 +506,7 @@ def test_all_in_one_app_endpoints() -> None:
507
506
  f"[DEBUG] Successfully logged in user: {test_username}\n"
508
507
  f"Token: {login_data['token'][:20]}..."
509
508
  )
510
- except (URLError, HTTPError) as exc:
509
+ except (URLError, HTTPError, RemoteDisconnected) as exc:
511
510
  print(f"[DEBUG] Error while requesting /user/login: {exc}")
512
511
  pytest.fail("Failed to POST /user/login")
513
512
 
@@ -552,14 +551,14 @@ def test_all_in_one_app_endpoints() -> None:
552
551
  assert http_err.code in (400, 401, 403), (
553
552
  f"Expected 400/401/403 for invalid login, got {http_err.code}"
554
553
  )
555
- except URLError as exc:
554
+ except (URLError, RemoteDisconnected) as exc:
556
555
  print(
557
556
  f"[DEBUG] Unexpected error while testing invalid login: {exc}"
558
557
  )
559
558
  pytest.fail("Unexpected error testing invalid login")
560
559
 
561
560
  # 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
561
+ # The /cl/app endpoint should serve the app which includes the TypeScript Card component
563
562
  try:
564
563
  print("[DEBUG] Verifying TypeScript component integration")
565
564
  # The page should load successfully (already tested above)
@@ -570,12 +569,10 @@ def test_all_in_one_app_endpoints() -> None:
570
569
  print(f"[DEBUG] Error verifying TypeScript component: {exc}")
571
570
  pytest.fail("Failed to verify TypeScript component integration")
572
571
 
573
- # Verify nested folder imports are working - /page/app#/nested route
572
+ # Verify nested folder imports are working - /cl/app#/nested route
574
573
  # This route uses nested folder imports (components.button and button)
575
574
  try:
576
- print(
577
- "[DEBUG] Verifying nested folder imports via /page/app#/nested"
578
- )
575
+ print("[DEBUG] Verifying nested folder imports via /cl/app#/nested")
579
576
  # The nested route should load successfully (already tested above)
580
577
  # Nested imports are compiled and included in the bundle
581
578
  assert "<html" in nested_body.lower(), (
@@ -607,3 +604,217 @@ def test_all_in_one_app_endpoints() -> None:
607
604
  os.chdir(original_cwd)
608
605
  # Final garbage collection to ensure all resources are released
609
606
  gc.collect()
607
+
608
+
609
+ def test_default_client_app_renders() -> None:
610
+ """Test that a default `jac create --cl` app renders correctly when served.
611
+
612
+ This test validates the out-of-the-box experience:
613
+ 1. Creates a new client app using `jac create --cl`
614
+ 2. Installs packages
615
+ 3. Starts the server
616
+ 4. Validates that the default app renders with expected content
617
+ """
618
+ print("[DEBUG] Starting test_default_client_app_renders")
619
+
620
+ app_name = "e2e-default-app"
621
+
622
+ with tempfile.TemporaryDirectory() as temp_dir:
623
+ print(f"[DEBUG] Created temporary directory at {temp_dir}")
624
+ original_cwd = os.getcwd()
625
+ try:
626
+ os.chdir(temp_dir)
627
+ print(f"[DEBUG] Changed working directory to {temp_dir}")
628
+
629
+ # 1. Create a new default Jac client app
630
+ jac_cmd = _get_jac_command()
631
+ env = _get_env_with_npm()
632
+ print(f"[DEBUG] Running '{' '.join(jac_cmd)} create --cl {app_name}'")
633
+ process = Popen(
634
+ [*jac_cmd, "create", "--cl", app_name],
635
+ stdin=PIPE,
636
+ stdout=PIPE,
637
+ stderr=PIPE,
638
+ text=True,
639
+ env=env,
640
+ )
641
+ stdout, stderr = process.communicate()
642
+ returncode = process.returncode
643
+
644
+ print(
645
+ f"[DEBUG] 'jac create --cl' completed returncode={returncode}\n"
646
+ f"STDOUT:\n{stdout}\n"
647
+ f"STDERR:\n{stderr}\n"
648
+ )
649
+
650
+ if returncode != 0 and "unrecognized arguments: --cl" in stderr:
651
+ pytest.fail(
652
+ "Test failed: installed `jac` CLI does not support `create --cl`."
653
+ )
654
+
655
+ assert returncode == 0, (
656
+ f"jac create --cl failed\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}\n"
657
+ )
658
+
659
+ project_path = os.path.join(temp_dir, app_name)
660
+ print(f"[DEBUG] Created default Jac client app at {project_path}")
661
+ assert os.path.isdir(project_path)
662
+
663
+ # Verify expected files were created (new structure: main.jac at root)
664
+ main_jac_path = os.path.join(project_path, "main.jac")
665
+ assert os.path.isfile(main_jac_path), (
666
+ "main.jac should exist at project root"
667
+ )
668
+
669
+ # Components are now at root level (not src/components)
670
+ button_jac_path = os.path.join(project_path, "components", "Button.cl.jac")
671
+ assert os.path.isfile(button_jac_path), (
672
+ "components/Button.cl.jac should exist"
673
+ )
674
+
675
+ jac_toml_path = os.path.join(project_path, "jac.toml")
676
+ assert os.path.isfile(jac_toml_path), "jac.toml should exist"
677
+
678
+ # 2. Ensure packages are installed (jac create --cl should have done this)
679
+ # If node_modules doesn't exist, run jac add --cl
680
+ node_modules_path = os.path.join(
681
+ project_path, ".jac", "client", "node_modules"
682
+ )
683
+ if not os.path.isdir(node_modules_path):
684
+ print("[DEBUG] node_modules not found, running 'jac add --cl'")
685
+ jac_add_result = run(
686
+ [*jac_cmd, "add", "--cl"],
687
+ cwd=project_path,
688
+ capture_output=True,
689
+ text=True,
690
+ env=env,
691
+ )
692
+ print(
693
+ f"[DEBUG] 'jac add --cl' completed returncode={jac_add_result.returncode}\n"
694
+ f"STDOUT (truncated):\n{jac_add_result.stdout[:1000]}\n"
695
+ f"STDERR (truncated):\n{jac_add_result.stderr[:1000]}\n"
696
+ )
697
+ if jac_add_result.returncode != 0:
698
+ pytest.fail(
699
+ f"jac add --cl failed\n"
700
+ f"STDOUT:\n{jac_add_result.stdout}\n"
701
+ f"STDERR:\n{jac_add_result.stderr}\n"
702
+ )
703
+
704
+ # 3. Start the server (now uses main.jac at project root)
705
+ server: Popen[bytes] | None = None
706
+ try:
707
+ print("[DEBUG] Starting server with 'jac start main.jac'")
708
+ server = Popen(
709
+ [*jac_cmd, "start", "main.jac"],
710
+ cwd=project_path,
711
+ env=env,
712
+ )
713
+
714
+ # Wait for server to be ready
715
+ print("[DEBUG] Waiting for server on 127.0.0.1:8000")
716
+ _wait_for_port("127.0.0.1", 8000, timeout=90.0)
717
+ print("[DEBUG] Server is accepting connections")
718
+
719
+ # 4. Test root endpoint - for client-only apps, root serves the HTML app
720
+ # Note: The root endpoint may return 503 while the client bundle is building.
721
+ # We use _wait_for_endpoint to retry on 503 until it's ready.
722
+ try:
723
+ print("[DEBUG] Testing root endpoint / (with retry)")
724
+ root_bytes = _wait_for_endpoint(
725
+ "http://127.0.0.1:8000",
726
+ timeout=120.0,
727
+ poll_interval=2.0,
728
+ request_timeout=30.0,
729
+ )
730
+ root_body = root_bytes.decode("utf-8", errors="ignore")
731
+ print(
732
+ f"[DEBUG] Root response:\nBody (truncated):\n{root_body[:500]}"
733
+ )
734
+ # For client-only apps, root returns HTML with the React app
735
+ assert "<html" in root_body.lower(), (
736
+ "Root should return HTML for client-only app"
737
+ )
738
+ assert "<script" in root_body.lower(), (
739
+ "Root should include script tag for client bundle"
740
+ )
741
+ except (URLError, HTTPError, TimeoutError) as exc:
742
+ print(f"[DEBUG] Error at root endpoint: {exc}")
743
+ pytest.fail(f"Failed to GET root endpoint: {exc}")
744
+
745
+ # 5. Test client app endpoint - the rendered React app
746
+ try:
747
+ print("[DEBUG] Testing client app endpoint /cl/app")
748
+ page_bytes = _wait_for_endpoint(
749
+ "http://127.0.0.1:8000/cl/app",
750
+ timeout=120.0,
751
+ poll_interval=2.0,
752
+ request_timeout=30.0,
753
+ )
754
+ page_body = page_bytes.decode("utf-8", errors="ignore")
755
+ print(
756
+ f"[DEBUG] Client app response:\n"
757
+ f"Body (truncated):\n{page_body[:1000]}"
758
+ )
759
+
760
+ # Validate HTML structure
761
+ assert "<html" in page_body.lower(), "Response should contain HTML"
762
+ assert "<body" in page_body.lower(), "Response should contain body"
763
+
764
+ # The page should include the bundled JavaScript
765
+ # that will render "Hello, World!" client-side
766
+ assert (
767
+ "<script" in page_body.lower() or "src=" in page_body.lower()
768
+ ), "Response should include script tags for React app"
769
+
770
+ except (URLError, HTTPError, TimeoutError) as exc:
771
+ print(f"[DEBUG] Error at /cl/app endpoint: {exc}")
772
+ pytest.fail(f"Failed to GET /cl/app endpoint: {exc}")
773
+
774
+ # 6. Test that static JS bundle is being served
775
+ try:
776
+ print("[DEBUG] Testing that client.js bundle is served")
777
+ # Extract the client.js path from the HTML
778
+ import re
779
+
780
+ script_match = re.search(
781
+ r'src="(/static/client\.js[^"]*)"', root_body
782
+ )
783
+ if script_match:
784
+ js_path = script_match.group(1)
785
+ js_url = f"http://127.0.0.1:8000{js_path}"
786
+ print(f"[DEBUG] Fetching JS bundle from {js_url}")
787
+ with urlopen(js_url, timeout=30) as resp:
788
+ js_body = resp.read().decode("utf-8", errors="ignore")
789
+ assert resp.status == 200, "JS bundle should return 200"
790
+ assert len(js_body) > 0, "JS bundle should not be empty"
791
+ print(
792
+ f"[DEBUG] JS bundle fetched successfully "
793
+ f"({len(js_body)} bytes)"
794
+ )
795
+ else:
796
+ print("[DEBUG] Warning: Could not find client.js in HTML")
797
+ except (URLError, HTTPError) as exc:
798
+ print(f"[DEBUG] Warning: Could not verify static assets: {exc}")
799
+ # Not a hard failure - the main page test is sufficient
800
+
801
+ print("[DEBUG] All default app tests passed!")
802
+
803
+ finally:
804
+ if server is not None:
805
+ print("[DEBUG] Terminating server process")
806
+ server.terminate()
807
+ try:
808
+ server.wait(timeout=15)
809
+ print("[DEBUG] Server terminated cleanly")
810
+ except Exception:
811
+ print("[DEBUG] Server did not terminate cleanly, killing")
812
+ server.kill()
813
+ server.wait(timeout=5)
814
+ time.sleep(1)
815
+ gc.collect()
816
+
817
+ finally:
818
+ print(f"[DEBUG] Restoring working directory to {original_cwd}")
819
+ os.chdir(original_cwd)
820
+ 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.7
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.7
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 {