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.
- jac_client/examples/all-in-one/src/app.jac +473 -741
- jac_client/examples/all-in-one/src/components/CategoryFilter.jac +35 -0
- jac_client/examples/all-in-one/src/components/Header.jac +13 -0
- jac_client/examples/all-in-one/src/components/ProfitOverview.jac +50 -0
- jac_client/examples/all-in-one/src/components/Summary.jac +53 -0
- jac_client/examples/all-in-one/src/components/TransactionForm.jac +158 -0
- jac_client/examples/all-in-one/src/components/TransactionItem.jac +55 -0
- jac_client/examples/all-in-one/src/components/TransactionList.jac +37 -0
- jac_client/examples/all-in-one/src/components/navigation.jac +132 -0
- jac_client/examples/all-in-one/src/constants/categories.jac +37 -0
- jac_client/examples/all-in-one/src/constants/clients.jac +13 -0
- jac_client/examples/all-in-one/src/context/BudgetContext.jac +28 -0
- jac_client/examples/all-in-one/src/hooks/useBudget.jac +116 -0
- jac_client/examples/all-in-one/src/hooks/useLocalStorage.jac +36 -0
- jac_client/examples/all-in-one/src/pages/BudgetPlanner.cl.jac +70 -0
- jac_client/examples/all-in-one/src/pages/BudgetPlanner.jac +126 -0
- jac_client/examples/all-in-one/src/pages/FeaturesTest.cl.jac +552 -0
- jac_client/examples/all-in-one/src/pages/FeaturesTest.jac +126 -0
- jac_client/examples/all-in-one/src/pages/LandingPage.jac +101 -0
- jac_client/examples/all-in-one/src/pages/loginPage.jac +132 -0
- jac_client/examples/all-in-one/src/pages/nestedDemo.jac +61 -0
- jac_client/examples/all-in-one/src/pages/notFound.jac +24 -0
- jac_client/examples/all-in-one/src/pages/signupPage.jac +133 -0
- jac_client/examples/all-in-one/src/utils/formatters.jac +52 -0
- jac_client/examples/asset-serving/css-with-image/src/app.jac +3 -3
- jac_client/examples/asset-serving/image-asset/src/app.jac +3 -3
- jac_client/examples/asset-serving/import-alias/src/app.jac +3 -3
- jac_client/examples/basic/src/app.jac +3 -3
- jac_client/examples/basic-auth/src/app.jac +31 -37
- jac_client/examples/basic-auth-with-router/src/app.jac +16 -16
- jac_client/examples/basic-full-stack/src/app.jac +24 -30
- jac_client/examples/css-styling/js-styling/src/app.jac +5 -5
- jac_client/examples/css-styling/material-ui/src/app.jac +5 -5
- jac_client/examples/css-styling/pure-css/src/app.jac +5 -5
- jac_client/examples/css-styling/sass-example/src/app.jac +5 -5
- jac_client/examples/css-styling/styled-components/src/app.jac +5 -5
- jac_client/examples/css-styling/tailwind-example/src/app.jac +5 -5
- jac_client/examples/full-stack-with-auth/src/app.jac +16 -16
- jac_client/examples/ts-support/src/app.jac +4 -4
- jac_client/examples/with-router/src/app.jac +4 -4
- jac_client/plugin/cli.jac +155 -203
- jac_client/plugin/client_runtime.cl.jac +5 -1
- jac_client/plugin/impl/client.impl.jac +74 -12
- jac_client/plugin/plugin_config.jac +11 -11
- jac_client/plugin/src/compiler.jac +2 -1
- jac_client/plugin/src/impl/babel_processor.impl.jac +22 -17
- jac_client/plugin/src/impl/compiler.impl.jac +57 -18
- jac_client/plugin/src/impl/vite_bundler.impl.jac +66 -102
- jac_client/plugin/src/package_installer.jac +1 -1
- jac_client/plugin/src/vite_bundler.jac +1 -0
- jac_client/tests/conftest.py +10 -8
- jac_client/tests/fixtures/spawn_test/app.jac +15 -18
- jac_client/tests/fixtures/with-ts/app.jac +4 -4
- jac_client/tests/test_cli.py +99 -45
- jac_client/tests/test_it.py +290 -79
- {jac_client-0.2.6.dist-info → jac_client-0.2.7.dist-info}/METADATA +16 -7
- jac_client-0.2.7.dist-info/RECORD +97 -0
- jac_client-0.2.6.dist-info/RECORD +0 -74
- {jac_client-0.2.6.dist-info → jac_client-0.2.7.dist-info}/WHEEL +0 -0
- {jac_client-0.2.6.dist-info → jac_client-0.2.7.dist-info}/entry_points.txt +0 -0
- {jac_client-0.2.6.dist-info → jac_client-0.2.7.dist-info}/top_level.txt +0 -0
jac_client/tests/test_it.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""End-to-end tests for `jac
|
|
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
|
|
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
|
|
251
|
+
print("[DEBUG] Starting server with 'jac start src/app.jac'")
|
|
219
252
|
server = Popen(
|
|
220
|
-
["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
|
-
|
|
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=
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
# "/
|
|
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 /
|
|
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/
|
|
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 /
|
|
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 /
|
|
288
|
-
pytest.fail(f"Failed to GET /
|
|
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
|
-
# "/
|
|
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 /
|
|
311
|
+
print("[DEBUG] Sending GET request to /cl/app#/nested endpoint")
|
|
294
312
|
with urlopen(
|
|
295
|
-
"http://127.0.0.1:8000/
|
|
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 /
|
|
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 /
|
|
328
|
+
f"[DEBUG] Error while requesting /cl/app#/nested endpoint: {exc}"
|
|
311
329
|
)
|
|
312
|
-
pytest.fail("Failed to GET /
|
|
330
|
+
pytest.fail("Failed to GET /cl/app#/nested endpoint")
|
|
313
331
|
|
|
314
|
-
#
|
|
315
|
-
#
|
|
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 /
|
|
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 - /
|
|
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.
|
|
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.
|
|
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
|
|
52
|
+
jac start src/app.jac
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Visit `http://localhost:8000
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|