jac-client 0.2.3__py3-none-any.whl → 0.2.5__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/assets/workers/worker.py +5 -0
- jac_client/examples/all-in-one/src/app.jac +841 -0
- jac_client/examples/all-in-one/{button.jac → src/button.jac} +1 -1
- jac_client/examples/all-in-one/{components → src/components}/button.jac +1 -1
- jac_client/examples/asset-serving/css-with-image/{app.jac → src/app.jac} +2 -2
- jac_client/examples/asset-serving/image-asset/{app.jac → src/app.jac} +2 -2
- jac_client/examples/asset-serving/import-alias/{app.jac → src/app.jac} +3 -3
- jac_client/examples/basic/{app.jac → src/app.jac} +2 -2
- jac_client/examples/basic-auth/src/app.jac +377 -0
- jac_client/examples/basic-auth-with-router/{app.jac → src/app.jac} +18 -18
- jac_client/examples/basic-full-stack/{app.jac → src/app.jac} +175 -130
- jac_client/examples/css-styling/js-styling/{app.jac → src/app.jac} +6 -6
- jac_client/examples/css-styling/material-ui/{app.jac → src/app.jac} +5 -5
- jac_client/examples/css-styling/pure-css/{app.jac → src/app.jac} +6 -6
- jac_client/examples/css-styling/sass-example/{app.jac → src/app.jac} +6 -6
- jac_client/examples/css-styling/styled-components/{app.jac → src/app.jac} +5 -5
- jac_client/examples/css-styling/tailwind-example/{app.jac → src/app.jac} +6 -6
- jac_client/examples/full-stack-with-auth/{app.jac → src/app.jac} +37 -37
- jac_client/examples/little-x/{app.jac → src/app.jac} +27 -32
- jac_client/examples/little-x/src/submit-button.jac +16 -0
- jac_client/examples/nested-folders/nested-advance/{ButtonRoot.jac → src/ButtonRoot.jac} +1 -1
- jac_client/examples/nested-folders/nested-advance/{app.jac → src/app.jac} +1 -1
- jac_client/examples/nested-folders/nested-advance/{level1 → src/level1}/ButtonSecondL.jac +1 -1
- jac_client/examples/nested-folders/nested-advance/{level1 → src/level1}/Card.jac +1 -1
- jac_client/examples/nested-folders/nested-advance/{level1 → src/level1}/level2/ButtonThirdL.jac +1 -1
- jac_client/examples/nested-folders/nested-basic/{app.jac → src/app.jac} +2 -2
- jac_client/examples/nested-folders/nested-basic/{button.jac → src/button.jac} +1 -1
- jac_client/examples/nested-folders/nested-basic/{components → src/components}/button.jac +1 -1
- jac_client/examples/ts-support/src/app.jac +35 -0
- jac_client/examples/with-router/{app.jac → src/app.jac} +11 -11
- jac_client/plugin/cli.jac +547 -0
- jac_client/plugin/client.jac +52 -0
- jac_client/plugin/client_runtime.cl.jac +38 -0
- jac_client/plugin/impl/client.impl.jac +134 -0
- jac_client/plugin/impl/client_runtime.impl.jac +177 -0
- jac_client/plugin/impl/vite_client_bundle.impl.jac +72 -0
- jac_client/plugin/plugin_config.jac +195 -0
- jac_client/plugin/src/__init__.jac +20 -0
- jac_client/plugin/src/asset_processor.jac +33 -0
- jac_client/plugin/src/babel_processor.jac +18 -0
- jac_client/plugin/src/compiler.jac +66 -0
- jac_client/plugin/src/config_loader.jac +32 -0
- jac_client/plugin/src/impl/asset_processor.impl.jac +127 -0
- jac_client/plugin/src/impl/babel_processor.impl.jac +84 -0
- jac_client/plugin/src/impl/compiler.impl.jac +251 -0
- jac_client/plugin/src/impl/config_loader.impl.jac +119 -0
- jac_client/plugin/src/impl/import_processor.impl.jac +33 -0
- jac_client/plugin/src/impl/jac_to_js.impl.jac +41 -0
- jac_client/plugin/src/impl/package_installer.impl.jac +105 -0
- jac_client/plugin/src/impl/vite_bundler.impl.jac +513 -0
- jac_client/plugin/src/import_processor.jac +19 -0
- jac_client/plugin/src/jac_to_js.jac +35 -0
- jac_client/plugin/src/package_installer.jac +26 -0
- jac_client/plugin/src/vite_bundler.jac +36 -0
- jac_client/plugin/vite_client_bundle.jac +31 -0
- jac_client/tests/conftest.py +281 -0
- jac_client/tests/fixtures/basic-app/app.jac +2 -2
- jac_client/tests/fixtures/cl_file/app.cl.jac +2 -2
- jac_client/tests/fixtures/client_app_with_antd/app.jac +1 -1
- jac_client/tests/fixtures/js_import/app.jac +5 -5
- jac_client/tests/fixtures/spawn_test/app.jac +7 -7
- jac_client/tests/fixtures/with-ts/app.jac +35 -0
- jac_client/tests/test_cli.py +755 -0
- jac_client/tests/test_it.py +347 -67
- {jac_client-0.2.3.dist-info → jac_client-0.2.5.dist-info}/METADATA +28 -30
- jac_client-0.2.5.dist-info/RECORD +74 -0
- {jac_client-0.2.3.dist-info → jac_client-0.2.5.dist-info}/WHEEL +2 -1
- jac_client-0.2.5.dist-info/entry_points.txt +4 -0
- jac_client-0.2.5.dist-info/top_level.txt +1 -0
- jac_client/docs/README.md +0 -689
- jac_client/docs/advanced-state.md +0 -1265
- jac_client/docs/asset-serving/intro.md +0 -209
- jac_client/docs/assets/pipe_line-v2.svg +0 -32
- jac_client/docs/assets/pipe_line.png +0 -0
- jac_client/docs/file-system/app.jac.md +0 -121
- jac_client/docs/file-system/backend-frontend.md +0 -217
- jac_client/docs/file-system/intro.md +0 -72
- jac_client/docs/file-system/nested-imports.md +0 -348
- jac_client/docs/guide-example/intro.md +0 -115
- jac_client/docs/guide-example/step-01-setup.md +0 -270
- jac_client/docs/guide-example/step-02-components.md +0 -416
- jac_client/docs/guide-example/step-03-styling.md +0 -478
- jac_client/docs/guide-example/step-04-todo-ui.md +0 -477
- jac_client/docs/guide-example/step-05-local-state.md +0 -530
- jac_client/docs/guide-example/step-06-events.md +0 -749
- jac_client/docs/guide-example/step-07-effects.md +0 -468
- jac_client/docs/guide-example/step-08-walkers.md +0 -534
- jac_client/docs/guide-example/step-09-authentication.md +0 -586
- jac_client/docs/guide-example/step-10-routing.md +0 -539
- jac_client/docs/guide-example/step-11-final.md +0 -963
- jac_client/docs/imports.md +0 -1141
- jac_client/docs/lifecycle-hooks.md +0 -773
- jac_client/docs/routing.md +0 -659
- jac_client/docs/styling/intro.md +0 -249
- jac_client/docs/styling/js-styling.md +0 -367
- jac_client/docs/styling/material-ui.md +0 -341
- jac_client/docs/styling/pure-css.md +0 -299
- jac_client/docs/styling/sass.md +0 -403
- jac_client/docs/styling/styled-components.md +0 -395
- jac_client/docs/styling/tailwind.md +0 -298
- jac_client/examples/all-in-one/.babelrc +0 -9
- jac_client/examples/all-in-one/README.md +0 -16
- jac_client/examples/all-in-one/app.jac +0 -426
- jac_client/examples/all-in-one/assets/burger.png +0 -0
- jac_client/examples/all-in-one/package.json +0 -29
- jac_client/examples/all-in-one/styles.css +0 -26
- jac_client/examples/all-in-one/vite.config.js +0 -28
- jac_client/examples/asset-serving/css-with-image/.babelrc +0 -9
- jac_client/examples/asset-serving/css-with-image/README.md +0 -91
- jac_client/examples/asset-serving/css-with-image/assets/burger.png +0 -0
- jac_client/examples/asset-serving/css-with-image/package.json +0 -28
- jac_client/examples/asset-serving/css-with-image/styles.css +0 -26
- jac_client/examples/asset-serving/css-with-image/vite.config.js +0 -28
- jac_client/examples/asset-serving/image-asset/.babelrc +0 -9
- jac_client/examples/asset-serving/image-asset/README.md +0 -119
- jac_client/examples/asset-serving/image-asset/assets/burger.png +0 -0
- jac_client/examples/asset-serving/image-asset/package.json +0 -28
- jac_client/examples/asset-serving/image-asset/styles.css +0 -26
- jac_client/examples/asset-serving/image-asset/vite.config.js +0 -28
- jac_client/examples/asset-serving/import-alias/.babelrc +0 -9
- jac_client/examples/asset-serving/import-alias/README.md +0 -83
- jac_client/examples/asset-serving/import-alias/assets/burger.png +0 -0
- jac_client/examples/asset-serving/import-alias/package.json +0 -28
- jac_client/examples/asset-serving/import-alias/vite.config.js +0 -28
- jac_client/examples/basic/.babelrc +0 -9
- jac_client/examples/basic/README.md +0 -16
- jac_client/examples/basic/package.json +0 -27
- jac_client/examples/basic/vite.config.js +0 -27
- jac_client/examples/basic-auth/.babelrc +0 -9
- jac_client/examples/basic-auth/README.md +0 -16
- jac_client/examples/basic-auth/app.jac +0 -308
- jac_client/examples/basic-auth/package.json +0 -27
- jac_client/examples/basic-auth/vite.config.js +0 -27
- jac_client/examples/basic-auth-with-router/.babelrc +0 -9
- jac_client/examples/basic-auth-with-router/README.md +0 -60
- jac_client/examples/basic-auth-with-router/package.json +0 -28
- jac_client/examples/basic-auth-with-router/vite.config.js +0 -27
- jac_client/examples/basic-full-stack/.babelrc +0 -9
- jac_client/examples/basic-full-stack/README.md +0 -18
- jac_client/examples/basic-full-stack/package.json +0 -28
- jac_client/examples/basic-full-stack/vite.config.js +0 -27
- jac_client/examples/css-styling/js-styling/.babelrc +0 -9
- jac_client/examples/css-styling/js-styling/README.md +0 -183
- jac_client/examples/css-styling/js-styling/package.json +0 -28
- jac_client/examples/css-styling/js-styling/styles.js +0 -100
- jac_client/examples/css-styling/js-styling/vite.config.js +0 -27
- jac_client/examples/css-styling/material-ui/.babelrc +0 -9
- jac_client/examples/css-styling/material-ui/README.md +0 -16
- jac_client/examples/css-styling/material-ui/package.json +0 -32
- jac_client/examples/css-styling/material-ui/vite.config.js +0 -27
- jac_client/examples/css-styling/pure-css/.babelrc +0 -9
- jac_client/examples/css-styling/pure-css/README.md +0 -16
- jac_client/examples/css-styling/pure-css/package.json +0 -28
- jac_client/examples/css-styling/pure-css/styles.css +0 -111
- jac_client/examples/css-styling/pure-css/vite.config.js +0 -27
- jac_client/examples/css-styling/sass-example/.babelrc +0 -9
- jac_client/examples/css-styling/sass-example/README.md +0 -16
- jac_client/examples/css-styling/sass-example/package.json +0 -29
- jac_client/examples/css-styling/sass-example/styles.scss +0 -153
- jac_client/examples/css-styling/sass-example/vite.config.js +0 -27
- jac_client/examples/css-styling/styled-components/.babelrc +0 -9
- jac_client/examples/css-styling/styled-components/README.md +0 -16
- jac_client/examples/css-styling/styled-components/package.json +0 -29
- jac_client/examples/css-styling/styled-components/styled.js +0 -90
- jac_client/examples/css-styling/styled-components/vite.config.js +0 -27
- jac_client/examples/css-styling/tailwind-example/.babelrc +0 -9
- jac_client/examples/css-styling/tailwind-example/README.md +0 -16
- jac_client/examples/css-styling/tailwind-example/global.css +0 -1
- jac_client/examples/css-styling/tailwind-example/package.json +0 -30
- jac_client/examples/css-styling/tailwind-example/vite.config.js +0 -29
- jac_client/examples/full-stack-with-auth/.babelrc +0 -9
- jac_client/examples/full-stack-with-auth/README.md +0 -16
- jac_client/examples/full-stack-with-auth/package.json +0 -28
- jac_client/examples/full-stack-with-auth/vite.config.js +0 -29
- jac_client/examples/little-x/package.json +0 -23
- jac_client/examples/little-x/submit-button.jac +0 -8
- jac_client/examples/nested-folders/nested-advance/.babelrc +0 -9
- jac_client/examples/nested-folders/nested-advance/README.md +0 -77
- jac_client/examples/nested-folders/nested-advance/package.json +0 -29
- jac_client/examples/nested-folders/nested-advance/vite.config.js +0 -28
- jac_client/examples/nested-folders/nested-basic/.babelrc +0 -9
- jac_client/examples/nested-folders/nested-basic/README.md +0 -183
- jac_client/examples/nested-folders/nested-basic/app.js +0 -7
- jac_client/examples/nested-folders/nested-basic/package.json +0 -28
- jac_client/examples/nested-folders/nested-basic/vite.config.js +0 -27
- jac_client/examples/with-router/.babelrc +0 -9
- jac_client/examples/with-router/README.md +0 -17
- jac_client/examples/with-router/package.json +0 -28
- jac_client/examples/with-router/vite.config.js +0 -27
- jac_client/plugin/cli.py +0 -244
- jac_client/plugin/client.py +0 -152
- jac_client/plugin/client_runtime.jac +0 -234
- jac_client/plugin/vite_client_bundle.py +0 -503
- jac_client/tests/fixtures/js_import/utils.js +0 -21
- jac_client/tests/fixtures/package-lock.json +0 -329
- jac_client/tests/fixtures/package.json +0 -11
- jac_client/tests/test_asset_examples.py +0 -322
- jac_client/tests/test_cl.py +0 -530
- jac_client/tests/test_create_jac_app.py +0 -131
- jac_client/tests/test_nested_file.py +0 -374
- jac_client-0.2.3.dist-info/RECORD +0 -171
- jac_client-0.2.3.dist-info/entry_points.txt +0 -4
jac_client/tests/test_it.py
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
72
|
-
|
|
73
|
-
["jac", "
|
|
74
|
-
|
|
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
|
|
79
|
-
f"returncode={
|
|
80
|
-
f"STDOUT:\n{
|
|
81
|
-
f"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 `
|
|
85
|
-
#
|
|
86
|
-
if
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
95
|
-
"jac
|
|
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.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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] '
|
|
127
|
-
f"returncode={
|
|
128
|
-
f"STDOUT (truncated to 2000 chars):\n{
|
|
129
|
-
f"STDERR (truncated to 2000 chars):\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
|
|
133
|
-
pytest.
|
|
134
|
-
"
|
|
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(
|
|
179
|
-
|
|
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=
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
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(
|
|
222
|
-
|
|
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=
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Build full-stack web applications with Jac - one language for frontend and backend.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.5
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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!**
|