jac-client 0.2.0__py3-none-any.whl → 0.2.2__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 (154) hide show
  1. jac_client/docs/README.md +50 -20
  2. jac_client/docs/advanced-state.md +13 -14
  3. jac_client/docs/asset-serving/intro.md +209 -0
  4. jac_client/docs/assets/pipe_line-v2.svg +32 -0
  5. jac_client/docs/file-system/app.jac.md +121 -0
  6. jac_client/docs/file-system/backend-frontend.md +217 -0
  7. jac_client/docs/file-system/intro.md +72 -0
  8. jac_client/docs/file-system/nested-imports.md +348 -0
  9. jac_client/docs/guide-example/intro.md +11 -13
  10. jac_client/docs/guide-example/step-01-setup.md +30 -20
  11. jac_client/docs/guide-example/step-02-components.md +24 -24
  12. jac_client/docs/guide-example/step-03-styling.md +24 -24
  13. jac_client/docs/guide-example/step-04-todo-ui.md +17 -17
  14. jac_client/docs/guide-example/step-05-local-state.md +23 -23
  15. jac_client/docs/guide-example/step-06-events.md +23 -24
  16. jac_client/docs/guide-example/step-07-effects.md +27 -28
  17. jac_client/docs/guide-example/step-08-walkers.md +23 -23
  18. jac_client/docs/guide-example/step-09-authentication.md +18 -18
  19. jac_client/docs/guide-example/step-10-routing.md +20 -21
  20. jac_client/docs/guide-example/step-11-final.md +34 -35
  21. jac_client/docs/imports.md +4 -5
  22. jac_client/docs/lifecycle-hooks.md +12 -13
  23. jac_client/docs/routing.md +21 -22
  24. jac_client/docs/styling/intro.md +249 -0
  25. jac_client/docs/styling/js-styling.md +367 -0
  26. jac_client/docs/styling/material-ui.md +341 -0
  27. jac_client/docs/styling/pure-css.md +299 -0
  28. jac_client/docs/styling/sass.md +403 -0
  29. jac_client/docs/styling/styled-components.md +395 -0
  30. jac_client/docs/styling/tailwind.md +298 -0
  31. jac_client/examples/all-in-one/.babelrc +9 -0
  32. jac_client/examples/all-in-one/README.md +16 -0
  33. jac_client/examples/all-in-one/app.jac +426 -0
  34. jac_client/examples/all-in-one/assets/burger.png +0 -0
  35. jac_client/examples/all-in-one/button.jac +7 -0
  36. jac_client/examples/all-in-one/components/button.jac +7 -0
  37. jac_client/examples/all-in-one/package.json +29 -0
  38. jac_client/examples/all-in-one/styles.css +26 -0
  39. jac_client/examples/all-in-one/vite.config.js +28 -0
  40. jac_client/examples/asset-serving/css-with-image/.babelrc +9 -0
  41. jac_client/examples/asset-serving/css-with-image/README.md +91 -0
  42. jac_client/examples/asset-serving/css-with-image/app.jac +88 -0
  43. jac_client/examples/asset-serving/css-with-image/assets/burger.png +0 -0
  44. jac_client/examples/asset-serving/css-with-image/package.json +28 -0
  45. jac_client/examples/asset-serving/css-with-image/styles.css +26 -0
  46. jac_client/examples/asset-serving/css-with-image/vite.config.js +28 -0
  47. jac_client/examples/asset-serving/image-asset/.babelrc +9 -0
  48. jac_client/examples/asset-serving/image-asset/README.md +119 -0
  49. jac_client/examples/asset-serving/image-asset/app.jac +55 -0
  50. jac_client/examples/asset-serving/image-asset/assets/burger.png +0 -0
  51. jac_client/examples/asset-serving/image-asset/package.json +28 -0
  52. jac_client/examples/asset-serving/image-asset/styles.css +26 -0
  53. jac_client/examples/asset-serving/image-asset/vite.config.js +28 -0
  54. jac_client/examples/asset-serving/import-alias/.babelrc +9 -0
  55. jac_client/examples/asset-serving/import-alias/README.md +83 -0
  56. jac_client/examples/asset-serving/import-alias/app.jac +111 -0
  57. jac_client/examples/asset-serving/import-alias/assets/burger.png +0 -0
  58. jac_client/examples/asset-serving/import-alias/package.json +28 -0
  59. jac_client/examples/asset-serving/import-alias/vite.config.js +28 -0
  60. jac_client/examples/basic/app.jac +14 -9
  61. jac_client/examples/basic/package.json +1 -1
  62. jac_client/examples/basic/vite.config.js +0 -1
  63. jac_client/examples/basic-auth/package.json +1 -1
  64. jac_client/examples/basic-auth/vite.config.js +0 -1
  65. jac_client/examples/basic-auth-with-router/package.json +1 -1
  66. jac_client/examples/basic-auth-with-router/vite.config.js +0 -1
  67. jac_client/examples/basic-full-stack/package.json +1 -1
  68. jac_client/examples/basic-full-stack/vite.config.js +0 -1
  69. jac_client/examples/css-styling/js-styling/.babelrc +9 -0
  70. jac_client/examples/css-styling/js-styling/README.md +183 -0
  71. jac_client/examples/css-styling/js-styling/app.jac +84 -0
  72. jac_client/examples/css-styling/js-styling/package.json +28 -0
  73. jac_client/examples/css-styling/js-styling/styles.js +100 -0
  74. jac_client/examples/css-styling/js-styling/vite.config.js +27 -0
  75. jac_client/examples/css-styling/material-ui/.babelrc +9 -0
  76. jac_client/examples/css-styling/material-ui/README.md +16 -0
  77. jac_client/examples/css-styling/material-ui/app.jac +122 -0
  78. jac_client/examples/css-styling/material-ui/package.json +32 -0
  79. jac_client/examples/css-styling/material-ui/vite.config.js +27 -0
  80. jac_client/examples/css-styling/pure-css/.babelrc +9 -0
  81. jac_client/examples/css-styling/pure-css/README.md +16 -0
  82. jac_client/examples/css-styling/pure-css/app.jac +64 -0
  83. jac_client/examples/css-styling/pure-css/package.json +28 -0
  84. jac_client/examples/css-styling/pure-css/styles.css +111 -0
  85. jac_client/examples/css-styling/pure-css/vite.config.js +27 -0
  86. jac_client/examples/css-styling/sass-example/.babelrc +9 -0
  87. jac_client/examples/css-styling/sass-example/README.md +16 -0
  88. jac_client/examples/css-styling/sass-example/app.jac +64 -0
  89. jac_client/examples/css-styling/sass-example/package.json +29 -0
  90. jac_client/examples/css-styling/sass-example/styles.scss +153 -0
  91. jac_client/examples/css-styling/sass-example/vite.config.js +27 -0
  92. jac_client/examples/css-styling/styled-components/.babelrc +9 -0
  93. jac_client/examples/css-styling/styled-components/README.md +16 -0
  94. jac_client/examples/css-styling/styled-components/app.jac +71 -0
  95. jac_client/examples/css-styling/styled-components/package.json +29 -0
  96. jac_client/examples/css-styling/styled-components/styled.js +90 -0
  97. jac_client/examples/css-styling/styled-components/vite.config.js +27 -0
  98. jac_client/examples/css-styling/tailwind-example/.babelrc +9 -0
  99. jac_client/examples/css-styling/tailwind-example/README.md +16 -0
  100. jac_client/examples/css-styling/tailwind-example/app.jac +63 -0
  101. jac_client/examples/css-styling/tailwind-example/global.css +1 -0
  102. jac_client/examples/css-styling/tailwind-example/package.json +30 -0
  103. jac_client/examples/css-styling/tailwind-example/vite.config.js +29 -0
  104. jac_client/examples/full-stack-with-auth/app.jac +20 -33
  105. jac_client/examples/full-stack-with-auth/package.json +1 -1
  106. jac_client/examples/full-stack-with-auth/vite.config.js +0 -1
  107. jac_client/examples/little-x/app.jac +327 -218
  108. jac_client/examples/little-x/submit-button.jac +1 -1
  109. jac_client/examples/nested-folders/nested-advance/.babelrc +9 -0
  110. jac_client/examples/nested-folders/nested-advance/ButtonRoot.jac +11 -0
  111. jac_client/examples/nested-folders/nested-advance/README.md +77 -0
  112. jac_client/examples/nested-folders/nested-advance/app.jac +35 -0
  113. jac_client/examples/nested-folders/nested-advance/level1/ButtonSecondL.jac +19 -0
  114. jac_client/examples/nested-folders/nested-advance/level1/Card.jac +43 -0
  115. jac_client/examples/nested-folders/nested-advance/level1/level2/ButtonThirdL.jac +25 -0
  116. jac_client/examples/nested-folders/nested-advance/package.json +29 -0
  117. jac_client/examples/nested-folders/nested-advance/vite.config.js +28 -0
  118. jac_client/examples/nested-folders/nested-basic/.babelrc +9 -0
  119. jac_client/examples/nested-folders/nested-basic/README.md +183 -0
  120. jac_client/examples/nested-folders/nested-basic/app.jac +13 -0
  121. jac_client/examples/nested-folders/nested-basic/app.js +7 -0
  122. jac_client/examples/nested-folders/nested-basic/button.jac +7 -0
  123. jac_client/examples/nested-folders/nested-basic/components/button.jac +7 -0
  124. jac_client/examples/nested-folders/nested-basic/package.json +28 -0
  125. jac_client/examples/nested-folders/nested-basic/vite.config.js +27 -0
  126. jac_client/examples/with-router/app.jac +1 -1
  127. jac_client/examples/with-router/package.json +1 -1
  128. jac_client/examples/with-router/vite.config.js +0 -1
  129. jac_client/plugin/cli.py +7 -2
  130. jac_client/plugin/client.py +68 -5
  131. jac_client/plugin/client_runtime.jac +1 -1
  132. jac_client/plugin/vite_client_bundle.py +162 -14
  133. jac_client/tests/__init__.py +0 -1
  134. jac_client/tests/fixtures/basic-app/app.jac +7 -2
  135. jac_client/tests/fixtures/cl_file/app.cl.jac +48 -0
  136. jac_client/tests/fixtures/cl_file/app.jac +15 -0
  137. jac_client/tests/fixtures/client_app_with_antd/app.jac +14 -8
  138. jac_client/tests/fixtures/js_import/app.jac +19 -15
  139. jac_client/tests/fixtures/js_import/utils.js +0 -1
  140. jac_client/tests/fixtures/package.json +1 -1
  141. jac_client/tests/fixtures/relative_import/app.jac +4 -6
  142. jac_client/tests/fixtures/relative_import/button.jac +7 -6
  143. jac_client/tests/fixtures/spawn_test/app.jac +1 -5
  144. jac_client/tests/fixtures/test_fragments_spread/app.jac +24 -10
  145. jac_client/tests/test_asset_examples.py +322 -0
  146. jac_client/tests/test_cl.py +480 -426
  147. jac_client/tests/test_create_jac_app.py +125 -133
  148. jac_client/tests/test_it.py +329 -0
  149. jac_client/tests/test_nested_file.py +374 -0
  150. {jac_client-0.2.0.dist-info → jac_client-0.2.2.dist-info}/METADATA +2 -2
  151. jac_client-0.2.2.dist-info/RECORD +171 -0
  152. jac_client-0.2.0.dist-info/RECORD +0 -72
  153. {jac_client-0.2.0.dist-info → jac_client-0.2.2.dist-info}/WHEEL +0 -0
  154. {jac_client-0.2.0.dist-info → jac_client-0.2.2.dist-info}/entry_points.txt +0 -0
@@ -1,17 +1,27 @@
1
+ import hashlib
1
2
  import html
3
+ import mimetypes
2
4
  import types
5
+ from http.server import BaseHTTPRequestHandler
3
6
  from pathlib import Path
4
- from typing import Any
7
+ from typing import Any, Literal, TypeAlias
5
8
 
6
9
  from jaclang.runtimelib.client_bundle import ClientBundle
7
- from jaclang.runtimelib.machine import (
8
- JacMachine as Jac,
10
+ from jaclang.runtimelib.runtime import (
11
+ JacRuntime as Jac,
12
+ )
13
+ from jaclang.runtimelib.runtime import (
9
14
  hookimpl,
10
15
  )
11
16
  from jaclang.runtimelib.server import ModuleIntrospector
12
17
 
13
18
  from .vite_client_bundle import ViteClientBundleBuilder
14
19
 
20
+ JsonValue: TypeAlias = (
21
+ None | str | int | float | bool | list["JsonValue"] | dict[str, "JsonValue"]
22
+ )
23
+ StatusCode: TypeAlias = Literal[200, 201, 400, 401, 404, 503]
24
+
15
25
 
16
26
  class JacClientModuleIntrospector(ModuleIntrospector):
17
27
  """Jac Client Module Introspector."""
@@ -30,12 +40,28 @@ class JacClientModuleIntrospector(ModuleIntrospector):
30
40
 
31
41
  bundle_hash = self.ensure_bundle()
32
42
 
43
+ # Find CSS file in dist directory
44
+ base_path = Path(Jac.base_path_dir)
45
+ dist_dir = base_path / "dist"
46
+ css_link = ""
47
+
48
+ # Try to find CSS file (main.css is the default Vite output)
49
+ css_file = dist_dir / "main.css"
50
+ if css_file.exists():
51
+ css_hash = hashlib.sha256(css_file.read_bytes()).hexdigest()[:8]
52
+ css_link = (
53
+ f'<link rel="stylesheet" href="/static/main.css?hash={css_hash}"/>'
54
+ )
55
+
56
+ head_content = f'<meta charset="utf-8"/>\n <title>{html.escape(function_name)}</title>'
57
+ if css_link:
58
+ head_content += f"\n {css_link}"
59
+
33
60
  page = (
34
61
  "<!DOCTYPE html>"
35
62
  '<html lang="en">'
36
63
  "<head>"
37
- '<meta charset="utf-8"/>'
38
- f"<title>{html.escape(function_name)}</title>"
64
+ f"{head_content}"
39
65
  "</head>"
40
66
  "<body>"
41
67
  '<div id="root"></div>'
@@ -87,3 +113,40 @@ class JacClient:
87
113
  ) -> ModuleIntrospector:
88
114
  """Get a module introspector for the supplied module."""
89
115
  return JacClientModuleIntrospector(module_name, base_path)
116
+
117
+ @staticmethod
118
+ @hookimpl
119
+ def send_static_file(
120
+ handler: BaseHTTPRequestHandler,
121
+ file_path: Path,
122
+ content_type: str | None = None,
123
+ ) -> None:
124
+ """Send static file response (images, fonts, etc.).
125
+
126
+ Args:
127
+ handler: HTTP request handler
128
+ file_path: Path to the file to serve
129
+ content_type: MIME type (auto-detected if None)
130
+ """
131
+ from jaclang.runtimelib.server import ResponseBuilder
132
+
133
+ if not file_path.exists() or not file_path.is_file():
134
+ ResponseBuilder.send_json(handler, 404, {"error": "File not found"})
135
+ return
136
+
137
+ try:
138
+ file_content = file_path.read_bytes()
139
+ if content_type is None:
140
+ content_type, _ = mimetypes.guess_type(str(file_path))
141
+ if content_type is None:
142
+ content_type = "application/octet-stream"
143
+
144
+ handler.send_response(200)
145
+ handler.send_header("Content-Type", content_type)
146
+ handler.send_header("Content-Length", str(len(file_content)))
147
+ handler.send_header("Cache-Control", "public, max-age=3600")
148
+ ResponseBuilder._add_cors_headers(handler)
149
+ handler.end_headers()
150
+ handler.wfile.write(file_content)
151
+ except Exception as exc:
152
+ ResponseBuilder.send_json(handler, 500, {"error": str(exc)})
@@ -106,7 +106,7 @@ cl {
106
106
  "Content-Type": "application/json",
107
107
  "Authorization": f"Bearer {token}" if token else ""
108
108
  },
109
- "body": JSON.stringify({"fields": fields})
109
+ "body": JSON.stringify(fields)
110
110
  }
111
111
  );
112
112
 
@@ -8,7 +8,7 @@ import shutil
8
8
  import subprocess
9
9
  from pathlib import Path
10
10
  from types import ModuleType
11
- from typing import Any, TYPE_CHECKING
11
+ from typing import TYPE_CHECKING, Any
12
12
 
13
13
  from jaclang.runtimelib.client_bundle import (
14
14
  ClientBundle,
@@ -43,9 +43,9 @@ class ViteClientBundleBuilder(ClientBundleBuilder):
43
43
  self.vite_package_json = vite_package_json
44
44
  self.vite_minify = vite_minify
45
45
 
46
- def _process_imports(
46
+ def _process_vite_imports(
47
47
  self, manifest: ClientManifest | None, module_path: Path
48
- ) -> list[Path | None]: # type: ignore[override]
48
+ ) -> list[Path | None]:
49
49
  """Process client imports for Vite bundling.
50
50
 
51
51
  Only mark modules as bundled when we actually inline their code (.jac files we compile
@@ -53,7 +53,6 @@ class ViteClientBundleBuilder(ClientBundleBuilder):
53
53
  ES imports so Vite can resolve and bundle them.
54
54
  """
55
55
  imported_js_modules: list[Path | None] = []
56
-
57
56
  if manifest and manifest.imports:
58
57
  for _, import_path in manifest.imports.items():
59
58
  import_path_obj = Path(import_path)
@@ -61,7 +60,6 @@ class ViteClientBundleBuilder(ClientBundleBuilder):
61
60
  if import_path_obj.suffix == ".js":
62
61
  # Inline local JS files and mark as bundled
63
62
  try:
64
-
65
63
  imported_js_modules.append(import_path_obj)
66
64
  except FileNotFoundError:
67
65
  imported_js_modules.append(None)
@@ -86,12 +84,20 @@ class ViteClientBundleBuilder(ClientBundleBuilder):
86
84
  visited: set[Path] | None = None,
87
85
  collected_exports: set[str] | None = None,
88
86
  collected_globals: dict[str, Any] | None = None,
87
+ source_root: Path | None = None,
89
88
  ) -> None:
90
89
  """Recursively compile/copy .jac/.js imports to temp, skipping bundling.
91
90
 
92
91
  Only prepares dependency JS artifacts for Vite by writing compiled JS (.jac)
93
92
  or copying local JS (.js) into the temp directory. Bare specifiers are left
94
93
  untouched for Vite to resolve.
94
+
95
+ Args:
96
+ module_path: Path to the module being compiled
97
+ visited: Set of already visited paths to avoid cycles
98
+ collected_exports: Set to accumulate exported symbols
99
+ collected_globals: Dict to accumulate global values
100
+ source_root: Root directory of the source files (for preserving folder structure)
95
101
  """
96
102
  if visited is None:
97
103
  visited = set()
@@ -104,6 +110,11 @@ class ViteClientBundleBuilder(ClientBundleBuilder):
104
110
  if module_path in visited:
105
111
  return
106
112
  visited.add(module_path)
113
+
114
+ # Set source_root on first call (root module's parent directory)
115
+ if source_root is None:
116
+ source_root = module_path.parent.resolve()
117
+
107
118
  manifest = None
108
119
 
109
120
  # Compile current module to JS and append registration
@@ -129,9 +140,24 @@ class ViteClientBundleBuilder(ClientBundleBuilder):
129
140
 
130
141
  combined_js = f"{jac_jsx_path}\n{module_js}\n{export_block}"
131
142
  if self.vite_package_json is not None:
132
- (
133
- self.vite_package_json.parent / "src" / f"{module_path.stem}.js"
134
- ).write_text(combined_js, encoding="utf-8")
143
+ # Preserve folder structure: calculate relative path from source_root
144
+ try:
145
+ relative_path = module_path.relative_to(source_root)
146
+ # Change extension from .jac to .js
147
+ output_path = (
148
+ self.vite_package_json.parent
149
+ / "src"
150
+ / relative_path.with_suffix(".js")
151
+ )
152
+ except ValueError:
153
+ # If file is outside source_root, fall back to just filename
154
+ output_path = (
155
+ self.vite_package_json.parent / "src" / f"{module_path.stem}.js"
156
+ )
157
+
158
+ # Ensure parent directories exist
159
+ output_path.parent.mkdir(parents=True, exist_ok=True)
160
+ output_path.write_text(combined_js, encoding="utf-8")
135
161
 
136
162
  if not manifest or not manifest.imports:
137
163
  return
@@ -148,18 +174,49 @@ class ViteClientBundleBuilder(ClientBundleBuilder):
148
174
  visited,
149
175
  collected_exports=collected_exports,
150
176
  collected_globals=collected_globals,
177
+ source_root=source_root,
151
178
  )
152
179
  elif path_obj.suffix == ".js":
153
180
  try:
154
181
  js_code = path_obj.read_text(encoding="utf-8")
155
182
  if self.vite_package_json is not None:
156
- (
157
- self.vite_package_json.parent / "src" / path_obj.name
158
- ).write_text(js_code, encoding="utf-8")
183
+ # Preserve folder structure for .js files too
184
+ try:
185
+ relative_path = path_obj.relative_to(source_root)
186
+ output_path = (
187
+ self.vite_package_json.parent / "src" / relative_path
188
+ )
189
+ except ValueError:
190
+ # If file is outside source_root, fall back to just filename
191
+ output_path = (
192
+ self.vite_package_json.parent / "src" / path_obj.name
193
+ )
194
+
195
+ # Ensure parent directories exist
196
+ output_path.parent.mkdir(parents=True, exist_ok=True)
197
+ output_path.write_text(js_code, encoding="utf-8")
159
198
  except FileNotFoundError:
160
199
  pass
161
200
  else:
162
201
  # Bare specifiers or other assets handled by Vite
202
+ if self.vite_package_json is not None and path_obj.is_file():
203
+ # Preserve folder structure for other assets too
204
+ try:
205
+ relative_path = path_obj.relative_to(source_root)
206
+ output_path = (
207
+ self.vite_package_json.parent / "src" / relative_path
208
+ )
209
+ except ValueError:
210
+ # If file is outside source_root, fall back to just filename
211
+ output_path = (
212
+ self.vite_package_json.parent / "src" / path_obj.name
213
+ )
214
+
215
+ # Ensure parent directories exist
216
+ output_path.parent.mkdir(parents=True, exist_ok=True)
217
+ output_path.write_text(
218
+ path_obj.read_text(encoding="utf-8"), encoding="utf-8"
219
+ )
163
220
  continue
164
221
 
165
222
  def _compile_bundle(
@@ -211,9 +268,9 @@ class ViteClientBundleBuilder(ClientBundleBuilder):
211
268
  module_js, mod = self._compile_to_js(module_path)
212
269
  module_manifest = mod.gen.client_manifest if mod else None
213
270
  collected_exports: set[str] = set(self._extract_client_exports(module_manifest))
271
+
214
272
  client_globals_map = self._extract_client_globals(module_manifest, module)
215
273
  collected_globals: dict[str, Any] = dict(client_globals_map)
216
-
217
274
  # Recursively prepare dependencies and accumulate symbols
218
275
  self._compile_dependencies_recursively(
219
276
  module_path,
@@ -221,6 +278,13 @@ class ViteClientBundleBuilder(ClientBundleBuilder):
221
278
  collected_globals=collected_globals,
222
279
  )
223
280
 
281
+ # Copy assets from root assets/ folder to src/assets/ for @jac-client/assets alias
282
+ project_dir = self.vite_package_json.parent
283
+ root_assets_dir = project_dir / "assets"
284
+ src_assets_dir = project_dir / "src" / "assets"
285
+ if root_assets_dir.exists() and root_assets_dir.is_dir():
286
+ self._copy_asset_files(root_assets_dir, src_assets_dir)
287
+
224
288
  client_exports = sorted(collected_exports)
225
289
  client_globals_map = collected_globals
226
290
 
@@ -283,6 +347,9 @@ root.render(<App />);
283
347
  subprocess.run(
284
348
  command, cwd=project_dir, check=True, capture_output=True, text=True
285
349
  )
350
+ # Copy CSS and other asset files from src/ to build/ after Babel compilation
351
+ # Babel only transpiles JS, so we need to manually copy assets
352
+ self._copy_asset_files(project_dir / "src", project_dir / "build")
286
353
  # then build the code
287
354
  command = ["npm", "run", "build"]
288
355
  subprocess.run(
@@ -293,7 +360,7 @@ root.render(<App />);
293
360
  except FileNotFoundError:
294
361
  raise ClientBundleError(
295
362
  "npx or vite command not found. Ensure Node.js and npm are installed."
296
- )
363
+ ) from None
297
364
  # Find the generated bundle file
298
365
  bundle_file = self._find_vite_bundle(output_dir)
299
366
  if not bundle_file:
@@ -336,12 +403,93 @@ root.render(<App />);
336
403
  }});
337
404
  """
338
405
 
406
+ def _copy_asset_files(self, src_dir: Path, build_dir: Path) -> None:
407
+ """Copy CSS and other asset files from src/ to build/ directory recursively.
408
+
409
+ Babel only transpiles JavaScript files, so CSS and other assets need to be
410
+ manually copied to the build directory for Vite to resolve them.
411
+ This method recursively copies assets from subdirectories (e.g., src/assets/)
412
+ while preserving the directory structure.
413
+ """
414
+ if not src_dir.exists():
415
+ return
416
+
417
+ # Ensure build directory exists
418
+ build_dir.mkdir(parents=True, exist_ok=True)
419
+
420
+ # Asset file extensions to copy
421
+ asset_extensions = {
422
+ ".css",
423
+ ".scss",
424
+ ".sass",
425
+ ".less",
426
+ ".svg",
427
+ ".png",
428
+ ".jpg",
429
+ ".jpeg",
430
+ ".gif",
431
+ ".webp",
432
+ ".ico",
433
+ ".woff",
434
+ ".woff2",
435
+ ".ttf",
436
+ ".eot",
437
+ ".otf",
438
+ ".mp4",
439
+ ".webm",
440
+ ".mp3",
441
+ ".wav",
442
+ }
443
+
444
+ def copy_recursive(
445
+ source: Path, destination: Path, base: Path | None = None
446
+ ) -> None:
447
+ """Recursively copy asset files from source to destination.
448
+
449
+ Args:
450
+ source: Source directory to copy from
451
+ destination: Destination directory to copy to
452
+ base: Base directory for calculating relative paths (defaults to source)
453
+ """
454
+ if not source.exists():
455
+ return
456
+
457
+ if base is None:
458
+ base = source
459
+
460
+ for item in source.iterdir():
461
+ if item.is_file() and item.suffix.lower() in asset_extensions:
462
+ # Preserve relative path structure from base
463
+ relative_path = item.relative_to(base)
464
+ dest_file = destination / relative_path
465
+ dest_file.parent.mkdir(parents=True, exist_ok=True)
466
+ with contextlib.suppress(OSError, shutil.Error):
467
+ shutil.copy2(item, dest_file)
468
+ elif item.is_dir():
469
+ # Recursively process subdirectories
470
+ copy_recursive(item, destination, base)
471
+
472
+ # Copy files from src_dir root and recursively from subdirectories
473
+ copy_recursive(src_dir, build_dir)
474
+
339
475
  def _find_vite_bundle(self, output_dir: Path) -> Path | None:
340
476
  """Find the generated Vite bundle file."""
341
477
  for file in output_dir.glob("client.*.js"):
342
478
  return file
343
479
  return None
344
480
 
481
+ def _find_vite_css(self, output_dir: Path) -> Path | None:
482
+ """Find the generated Vite CSS file."""
483
+ # Vite typically outputs CSS as main.css or with a hash
484
+ # Try main.css first (most common), then any .css file
485
+ css_file = output_dir / "main.css"
486
+ if css_file.exists():
487
+ return css_file
488
+ # Fallback: find any CSS file
489
+ for file in output_dir.glob("*.css"):
490
+ return file
491
+ return None
492
+
345
493
  def cleanup_temp_dir(self) -> None:
346
494
  """Clean up the src directory and its contents."""
347
495
  if not self.vite_package_json or not self.vite_package_json.exists():
@@ -351,5 +499,5 @@ root.render(<App />);
351
499
  temp_dir = project_dir / "src"
352
500
 
353
501
  if temp_dir.exists():
354
- with contextlib.suppress(OSError):
502
+ with contextlib.suppress(OSError, shutil.Error):
355
503
  shutil.rmtree(temp_dir)
@@ -1,2 +1 @@
1
1
  """Tests for jac-client package."""
2
-
@@ -10,8 +10,13 @@ cl obj ButtonProps {
10
10
  cl def app() {
11
11
  let props = ButtonProps(label="Tap Me", color="primary");
12
12
  return <div class="app">
13
- <h1>{API_LABEL}</h1>
14
- <button class={props.color} data-id="button">
13
+ <h1>
14
+ {API_LABEL}
15
+ </h1>
16
+ <button
17
+ class={props.color}
18
+ data-id="button"
19
+ >
15
20
  {props.label}
16
21
  </button>
17
22
  </div>;
@@ -0,0 +1,48 @@
1
+ import from react { useState }
2
+
3
+ def app() -> any {
4
+ let [todos, setTodos] = useState([]);
5
+ let [input, setInput] = useState("");
6
+
7
+ # Event Handler
8
+ async def addTodo() -> None {
9
+ if not input.trim() {
10
+ return;
11
+ }
12
+ response = root spawn create_todo(text=input.trim());
13
+ new_todo = response.reports[0][0];
14
+ setTodos(todos.concat([new_todo]));
15
+ setInput("");
16
+
17
+ def foo { }
18
+ }
19
+
20
+ return <div>
21
+ <h2>
22
+ My Todos
23
+ </h2>
24
+ <input
25
+ value={input}
26
+ onChange={lambda e: any -> None{ setInput(e.target.value);} }
27
+ onKeyPress={lambda e: any -> None{ if e.key == "Enter" {
28
+ addTodo();
29
+ }} }
30
+ />
31
+ <button
32
+ onClick={addTodo}
33
+ >
34
+ Add Todo
35
+ </button>
36
+ <div>
37
+ {todos.map(
38
+ lambda todo: any -> any{ return <div
39
+ key={todo._jac_id}
40
+ >
41
+ <span>
42
+ {todo.text}
43
+ </span>
44
+ </div>; }
45
+ )}
46
+ </div>
47
+ </div>;
48
+ }
@@ -0,0 +1,15 @@
1
+ '''Test file for .cl file serves the client module.'''
2
+
3
+ node Todo {
4
+ has text: str;
5
+ has done: bool = False;
6
+ }
7
+
8
+ walker create_todo {
9
+ has text: str;
10
+
11
+ can create with `root entry {
12
+ new_todo = here ++> Todo(text=self.text);
13
+ report new_todo ;
14
+ }
15
+ }
@@ -1,21 +1,28 @@
1
1
  """Sample Jac module using Ant Design components."""
2
2
 
3
- cl import from antd {
4
- Button
5
- }
3
+ cl import from antd { Button }
4
+
6
5
  cl let APP_NAME: str = "Ant Design Test";
7
6
 
8
7
  cl def ButtonTest() {
9
8
  return <div>
10
- <h1>{APP_NAME}</h1>
11
- <p>Testing Ant Design integration</p>
12
- <Button>Click Me</Button>
9
+ <h1>
10
+ {APP_NAME}
11
+ </h1>
12
+ <p>
13
+ Testing Ant Design integration
14
+ </p>
15
+ <Button>
16
+ Click Me
17
+ </Button>
13
18
  </div>;
14
19
  }
15
20
 
16
21
  cl def CardTest() {
17
22
  return <div class="card-wrapper">
18
- <h2>Card Component</h2>
23
+ <h2>
24
+ Card Component
25
+ </h2>
19
26
  </div>;
20
27
  }
21
28
 
@@ -25,4 +32,3 @@ cl def app() {
25
32
  <CardTest />
26
33
  </div>;
27
34
  }
28
-
@@ -1,30 +1,34 @@
1
1
  """Test module that imports JavaScript functions."""
2
2
 
3
- cl import from .utils {
4
- formatMessage,
5
- calculateSum,
6
- JS_CONSTANT,
7
- MessageFormatter
8
- }
3
+ cl import from .utils { formatMessage, calculateSum, JS_CONSTANT, MessageFormatter }
9
4
 
10
5
  cl let JS_IMPORT_LABEL: str = "JavaScript Import Test";
11
6
 
12
- cl def JsImportTest() -> any {
7
+ cl def JsImportTest() -> any {
13
8
  let greeting = formatMessage("Jac");
14
9
  let sum = calculateSum(5, 3);
15
10
  let formatter = MessageFormatter("JS");
16
11
  let formatted = formatter.format("Hello from JS class");
17
-
12
+
18
13
  return <div class="js-import-test">
19
- <h1>{JS_IMPORT_LABEL}</h1>
20
- <p>Greeting: {greeting}</p>
21
- <p>Sum (5 + 3): {sum}</p>
22
- <p>Constant: {JS_CONSTANT}</p>
23
- <p>Formatted: {formatted}</p>
14
+ <h1>
15
+ {JS_IMPORT_LABEL}
16
+ </h1>
17
+ <p>
18
+ Greeting: {greeting}
19
+ </p>
20
+ <p>
21
+ Sum (5 + 3): {sum}
22
+ </p>
23
+ <p>
24
+ Constant: {JS_CONSTANT}
25
+ </p>
26
+ <p>
27
+ Formatted: {formatted}
28
+ </p>
24
29
  </div>;
25
30
  }
26
31
 
27
- cl def app() -> any {
32
+ cl def app() -> any {
28
33
  return <JsImportTest />;
29
34
  }
30
-
@@ -19,4 +19,3 @@ export class MessageFormatter {
19
19
  return `${this.prefix}: ${text}`;
20
20
  }
21
21
  }
22
-
@@ -8,4 +8,4 @@
8
8
  "devDependencies": {
9
9
  "vite": "^5.0.0"
10
10
  }
11
- }
11
+ }
@@ -1,13 +1,11 @@
1
- cl import from .button {
2
- CustomButton
3
- }
1
+ cl import from .button { CustomButton }
4
2
 
5
- cl def RelativeImport() -> any {
3
+ cl def RelativeImport() -> any {
6
4
  return <div>
7
5
  <CustomButton />
8
6
  </div>;
9
7
  }
10
8
 
11
- cl def app() -> any {
9
+ cl def app() -> any {
12
10
  return <RelativeImport />;
13
- }
11
+ }
@@ -1,6 +1,7 @@
1
- cl import from antd {
2
- Button
3
- }
4
- cl def CustomButton() -> any {
5
- return <Button>Click Me</Button>;
6
- }
1
+ cl import from antd { Button }
2
+
3
+ cl def CustomButton() -> any {
4
+ return <Button>
5
+ Click Me
6
+ </Button>;
7
+ }
@@ -27,11 +27,7 @@ walker positional_walker {
27
27
  has metadata: dict = {};
28
28
 
29
29
  can execute with `root entry {
30
- report {
31
- "label": self.label,
32
- "count": self.count,
33
- "meta": self.metadata
34
- } ;
30
+ report {"label": self.label, "count": self.count, "meta": self.metadata} ;
35
31
  }
36
32
  }
37
33