jac-client 0.2.0__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 (72) hide show
  1. jac_client/docs/README.md +659 -0
  2. jac_client/docs/advanced-state.md +1266 -0
  3. jac_client/docs/assets/pipe_line.png +0 -0
  4. jac_client/docs/guide-example/intro.md +117 -0
  5. jac_client/docs/guide-example/step-01-setup.md +260 -0
  6. jac_client/docs/guide-example/step-02-components.md +416 -0
  7. jac_client/docs/guide-example/step-03-styling.md +478 -0
  8. jac_client/docs/guide-example/step-04-todo-ui.md +477 -0
  9. jac_client/docs/guide-example/step-05-local-state.md +530 -0
  10. jac_client/docs/guide-example/step-06-events.md +750 -0
  11. jac_client/docs/guide-example/step-07-effects.md +469 -0
  12. jac_client/docs/guide-example/step-08-walkers.md +534 -0
  13. jac_client/docs/guide-example/step-09-authentication.md +586 -0
  14. jac_client/docs/guide-example/step-10-routing.md +540 -0
  15. jac_client/docs/guide-example/step-11-final.md +964 -0
  16. jac_client/docs/imports.md +1142 -0
  17. jac_client/docs/lifecycle-hooks.md +774 -0
  18. jac_client/docs/routing.md +660 -0
  19. jac_client/examples/basic/.babelrc +9 -0
  20. jac_client/examples/basic/README.md +16 -0
  21. jac_client/examples/basic/app.jac +16 -0
  22. jac_client/examples/basic/package.json +27 -0
  23. jac_client/examples/basic/vite.config.js +28 -0
  24. jac_client/examples/basic-auth/.babelrc +9 -0
  25. jac_client/examples/basic-auth/README.md +16 -0
  26. jac_client/examples/basic-auth/app.jac +308 -0
  27. jac_client/examples/basic-auth/package.json +27 -0
  28. jac_client/examples/basic-auth/vite.config.js +28 -0
  29. jac_client/examples/basic-auth-with-router/.babelrc +9 -0
  30. jac_client/examples/basic-auth-with-router/README.md +60 -0
  31. jac_client/examples/basic-auth-with-router/app.jac +464 -0
  32. jac_client/examples/basic-auth-with-router/package.json +28 -0
  33. jac_client/examples/basic-auth-with-router/vite.config.js +28 -0
  34. jac_client/examples/basic-full-stack/.babelrc +9 -0
  35. jac_client/examples/basic-full-stack/README.md +18 -0
  36. jac_client/examples/basic-full-stack/app.jac +320 -0
  37. jac_client/examples/basic-full-stack/package.json +28 -0
  38. jac_client/examples/basic-full-stack/vite.config.js +28 -0
  39. jac_client/examples/full-stack-with-auth/.babelrc +9 -0
  40. jac_client/examples/full-stack-with-auth/README.md +16 -0
  41. jac_client/examples/full-stack-with-auth/app.jac +735 -0
  42. jac_client/examples/full-stack-with-auth/package.json +28 -0
  43. jac_client/examples/full-stack-with-auth/vite.config.js +30 -0
  44. jac_client/examples/little-x/app.jac +615 -0
  45. jac_client/examples/little-x/package.json +23 -0
  46. jac_client/examples/little-x/submit-button.jac +8 -0
  47. jac_client/examples/with-router/.babelrc +9 -0
  48. jac_client/examples/with-router/README.md +17 -0
  49. jac_client/examples/with-router/app.jac +323 -0
  50. jac_client/examples/with-router/package.json +28 -0
  51. jac_client/examples/with-router/vite.config.js +28 -0
  52. jac_client/plugin/cli.py +239 -0
  53. jac_client/plugin/client.py +89 -0
  54. jac_client/plugin/client_runtime.jac +234 -0
  55. jac_client/plugin/vite_client_bundle.py +355 -0
  56. jac_client/tests/__init__.py +2 -0
  57. jac_client/tests/fixtures/basic-app/app.jac +18 -0
  58. jac_client/tests/fixtures/client_app_with_antd/app.jac +28 -0
  59. jac_client/tests/fixtures/js_import/app.jac +30 -0
  60. jac_client/tests/fixtures/js_import/utils.js +22 -0
  61. jac_client/tests/fixtures/package-lock.json +329 -0
  62. jac_client/tests/fixtures/package.json +11 -0
  63. jac_client/tests/fixtures/relative_import/app.jac +13 -0
  64. jac_client/tests/fixtures/relative_import/button.jac +6 -0
  65. jac_client/tests/fixtures/spawn_test/app.jac +133 -0
  66. jac_client/tests/fixtures/test_fragments_spread/app.jac +53 -0
  67. jac_client/tests/test_cl.py +476 -0
  68. jac_client/tests/test_create_jac_app.py +139 -0
  69. jac_client-0.2.0.dist-info/METADATA +182 -0
  70. jac_client-0.2.0.dist-info/RECORD +72 -0
  71. jac_client-0.2.0.dist-info/WHEEL +4 -0
  72. jac_client-0.2.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,476 @@
1
+ """Tests for Vite client bundle generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import json
7
+ import tempfile
8
+ import subprocess
9
+
10
+ from jaclang.runtimelib.machine import JacMachine as Jac
11
+ from jaclang.utils.test import TestCase
12
+ from jac_client.plugin.vite_client_bundle import ViteClientBundleBuilder
13
+
14
+
15
+ class ViteClientBundleBuilderTests(TestCase):
16
+ """Validate Vite-powered client bundle compilation."""
17
+
18
+ def setUp(self) -> None:
19
+ Jac.reset_machine()
20
+ return super().setUp()
21
+
22
+ def tearDown(self) -> None:
23
+ Jac.reset_machine()
24
+ return super().tearDown()
25
+
26
+ def _create_test_project_with_vite(self, temp_path: Path, include_antd: bool = False) -> tuple[Path, Path]:
27
+ """Create a minimal test project with Vite installed.
28
+
29
+ Args:
30
+ temp_path: Path to the temporary directory
31
+ include_antd: If True, includes antd in dependencies
32
+ """
33
+ # Create package.json with base dependencies
34
+ dependencies = {
35
+ "react": "^19.2.0",
36
+ "react-dom": "^19.2.0",
37
+ "react-router-dom": "^7.3.0"
38
+ }
39
+
40
+ # Add antd if requested
41
+ if include_antd:
42
+ dependencies["antd"] = "^5.0.0"
43
+
44
+ # Create package.json structure
45
+ package_data = {
46
+ "name": "test-client",
47
+ "version": "0.0.1",
48
+ "type": "module",
49
+ "scripts": {
50
+ "build": "npm run compile && vite build",
51
+ "dev": "vite dev",
52
+ "preview": "vite preview",
53
+ "compile": 'babel src --out-dir build --extensions ".jsx,.js" --out-file-extension .js'
54
+ },
55
+ "dependencies": dependencies,
56
+ "devDependencies": {
57
+ "vite": "^6.4.1",
58
+ "@babel/cli": "^7.28.3",
59
+ "@babel/core": "^7.28.5",
60
+ "@babel/preset-env": "^7.28.5",
61
+ "@babel/preset-react": "^7.28.5"
62
+ }
63
+ }
64
+
65
+ package_json = temp_path / "package.json"
66
+ with package_json.open("w", encoding="utf-8") as f:
67
+ json.dump(package_data, f, indent=2)
68
+
69
+ # Create .babelrc file
70
+ babelrc = temp_path / ".babelrc"
71
+ babelrc.write_text("""{
72
+ "presets": [[
73
+ "@babel/preset-env",
74
+ {
75
+ "modules": false
76
+ }
77
+ ], "@babel/preset-react"]
78
+ }
79
+ """, encoding="utf-8")
80
+
81
+ # Create vite.config.js file
82
+ vite_config = temp_path / "vite.config.js"
83
+ vite_config.write_text("""import { defineConfig } from "vite";
84
+ import path from "path";
85
+ import { fileURLToPath } from "url";
86
+
87
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
88
+
89
+ export default defineConfig({
90
+ root: ".",
91
+ build: {
92
+ rollupOptions: {
93
+ input: "build/main.js",
94
+ output: {
95
+ entryFileNames: "client.[hash].js",
96
+ assetFileNames: "[name].[ext]",
97
+ },
98
+ },
99
+ outDir: "dist",
100
+ emptyOutDir: true,
101
+ minify: false,
102
+ },
103
+ publicDir: false,
104
+ resolve: {
105
+ alias: {
106
+ "@jac-client/utils": path.resolve(__dirname, "src/client_runtime.js"),
107
+ },
108
+ },
109
+ });
110
+ """, encoding="utf-8")
111
+
112
+ # Install dependencies
113
+ result = subprocess.run(
114
+ ["npm", "install"],
115
+ cwd=temp_path,
116
+ check=False,
117
+ capture_output=True,
118
+ text=True,
119
+ )
120
+ if result.returncode != 0:
121
+ error_msg = f"npm install failed with exit code {result.returncode}\n"
122
+ error_msg += f"stdout: {result.stdout}\n"
123
+ error_msg += f"stderr: {result.stderr}\n"
124
+ raise RuntimeError(error_msg)
125
+
126
+ # Create output directory
127
+ output_dir = temp_path / "dist"
128
+ output_dir.mkdir(parents=True, exist_ok=True)
129
+
130
+ src_dir = temp_path / "src"
131
+ src_dir.mkdir(parents=True, exist_ok=True)
132
+
133
+ build_dir = temp_path / "build"
134
+ build_dir.mkdir(parents=True, exist_ok=True)
135
+
136
+ return package_json, output_dir
137
+
138
+ def test_build_bundle_with_vite(self) -> None:
139
+ """Test that Vite bundling produces optimized output with proper structure."""
140
+ # Create a temporary directory for our test project
141
+ with tempfile.TemporaryDirectory() as temp_dir:
142
+ temp_path = Path(temp_dir)
143
+
144
+ package_json, output_dir = self._create_test_project_with_vite(temp_path)
145
+ runtime_path = Path(__file__).parent.parent / "plugin" / "client_runtime.jac"
146
+ # Initialize the Vite builder
147
+ builder = ViteClientBundleBuilder(
148
+ runtime_path=runtime_path,
149
+ vite_package_json=package_json,
150
+ vite_output_dir=output_dir,
151
+ vite_minify=False, # Disable minification for easier inspection
152
+ )
153
+ # Import the test module
154
+ fixtures_dir = Path(__file__).parent / "fixtures" / "basic-app"
155
+ (module,) = Jac.jac_import("app", str(fixtures_dir))
156
+ # Build the bundle
157
+ bundle = builder.build(module, force=True)
158
+
159
+ self.assertIsNotNone(bundle)
160
+ self.assertEqual(bundle.module_name, "app")
161
+ self.assertIn("app", bundle.client_functions)
162
+ self.assertIn("ButtonProps", bundle.client_functions)
163
+ self.assertIn("API_LABEL", bundle.client_globals)
164
+ self.assertGreater(len(bundle.hash), 10)
165
+
166
+ # Verify bundle code contains expected content
167
+ self.assertIn("function app()", bundle.code)
168
+ self.assertIn('API_LABEL = "Runtime Test";', bundle.code)
169
+
170
+
171
+ # Verify bundle was written to output directory
172
+ bundle_files = list(output_dir.glob("client.*.js"))
173
+ self.assertGreater(len(bundle_files), 0, "Expected at least one bundle file")
174
+
175
+ # Verify cached bundle is identical
176
+ cached = builder.build(module, force=False)
177
+ self.assertEqual(bundle.hash, cached.hash)
178
+ self.assertEqual(bundle.code, cached.code)
179
+
180
+ def test_vite_bundle_without_package_json(self) -> None:
181
+ """Test that missing package.json raises appropriate error."""
182
+ fixtures_dir = Path(__file__).parent / "fixtures" / "basic-app"
183
+ (module,) = Jac.jac_import("app", str(fixtures_dir))
184
+
185
+ runtime_path = Path(__file__).parent.parent / "plugin" / "client_runtime.jac"
186
+
187
+ # Create builder without package.json
188
+ builder = ViteClientBundleBuilder(
189
+ runtime_path=runtime_path,
190
+ vite_package_json=Path("/nonexistent/package.json"),
191
+ vite_output_dir=Path("/tmp/output"),
192
+ )
193
+
194
+ # Building should raise an error
195
+ from jaclang.runtimelib.client_bundle import ClientBundleError
196
+ with self.assertRaises(ClientBundleError) as cm:
197
+ builder.build(module, force=True)
198
+
199
+ self.assertIn("Vite package.json not found", str(cm.exception))
200
+
201
+ def test_build_bundle_with_antd(self) -> None:
202
+ """Test that Vite bundling works with Ant Design components."""
203
+ with tempfile.TemporaryDirectory() as temp_dir:
204
+
205
+ temp_path = Path(temp_dir)
206
+
207
+ # Create project with Vite and Ant Design installed
208
+ package_json, output_dir = self._create_test_project_with_vite(temp_path, include_antd=True)
209
+ runtime_path = Path(__file__).parent.parent / "plugin" / "client_runtime.jac"
210
+
211
+ # Initialize the Vite builder
212
+ builder = ViteClientBundleBuilder(
213
+ runtime_path=runtime_path,
214
+ vite_package_json=package_json,
215
+ vite_output_dir=output_dir,
216
+ vite_minify=False,
217
+ )
218
+
219
+ # Import the test module with Ant Design
220
+ fixtures_dir = Path(__file__).parent / "fixtures" / "client_app_with_antd"
221
+ (module,) = Jac.jac_import("app", str(fixtures_dir))
222
+
223
+ # Build the bundle
224
+ bundle = builder.build(module, force=True)
225
+
226
+ # Verify bundle structure
227
+ self.assertIsNotNone(bundle)
228
+ self.assertEqual(bundle.module_name, "app")
229
+ self.assertIn("ButtonTest", bundle.client_functions)
230
+ self.assertIn("CardTest", bundle.client_functions)
231
+ self.assertIn("APP_NAME", bundle.client_globals)
232
+
233
+ # Verify bundle code contains expected content
234
+ self.assertIn("function ButtonTest()", bundle.code)
235
+ self.assertIn("function CardTest()", bundle.code)
236
+ self.assertIn('APP_NAME = "Ant Design Test";', bundle.code)
237
+
238
+ # verify antd components are present
239
+ self.assertIn("ButtonGroup", bundle.code)
240
+
241
+ # Verify the Ant Design fixture content is present
242
+ self.assertIn("Testing Ant Design integration", bundle.code)
243
+
244
+ # Verify bundle was written to output directory
245
+ bundle_files = list(output_dir.glob("client.*.js"))
246
+ self.assertGreater(len(bundle_files), 0, "Expected at least one bundle file")
247
+
248
+ # Cleanup
249
+ builder.cleanup_temp_dir()
250
+
251
+ def test_relative_import(self) -> None:
252
+ """Test that relative imports work correctly in Vite bundling."""
253
+ with tempfile.TemporaryDirectory() as temp_dir:
254
+ temp_path = Path(temp_dir)
255
+
256
+ # Create project with Vite installed
257
+ package_json, output_dir = self._create_test_project_with_vite(temp_path, include_antd=True)
258
+ runtime_path = Path(__file__).parent.parent / "plugin" / "client_runtime.jac"
259
+
260
+ # Initialize the Vite builder
261
+ builder = ViteClientBundleBuilder(
262
+ runtime_path=runtime_path,
263
+ vite_package_json=package_json,
264
+ vite_output_dir=output_dir,
265
+ vite_minify=False,
266
+ )
267
+
268
+ # Import the test module with relative import
269
+ fixtures_dir = Path(__file__).parent / "fixtures" / "relative_import"
270
+ (module,) = Jac.jac_import("app", str(fixtures_dir))
271
+
272
+ # Build the bundle
273
+ bundle = builder.build(module, force=True)
274
+
275
+ # Verify bundle structure
276
+ self.assertIsNotNone(bundle)
277
+ self.assertEqual(bundle.module_name, "app")
278
+ self.assertIn("RelativeImport", bundle.client_functions)
279
+ self.assertIn("app", bundle.client_functions)
280
+ self.assertIn("CustomButton", bundle.code)
281
+
282
+ # Verify bundle code contains expected content
283
+ self.assertIn("function RelativeImport()", bundle.code)
284
+ self.assertIn("function app()", bundle.code)
285
+
286
+ # Verify that the relative import (Button from .button) is properly resolved
287
+ self.assertIn("ButtonGroup", bundle.code)
288
+
289
+ # Verify bundle was written to output directory
290
+ bundle_files = list(output_dir.glob("client.*.js"))
291
+ self.assertGreater(len(bundle_files), 0, "Expected at least one bundle file")
292
+
293
+ # Cleanup
294
+ builder.cleanup_temp_dir()
295
+
296
+ def test_js_import(self) -> None:
297
+ """Test that JavaScript file imports work correctly in Vite bundling."""
298
+ with tempfile.TemporaryDirectory() as temp_dir:
299
+ temp_path = Path(temp_dir)
300
+ # Create project with Vite installed
301
+ package_json, output_dir = self._create_test_project_with_vite(temp_path)
302
+ runtime_path = Path(__file__).parent.parent / "plugin" / "client_runtime.jac"
303
+ # Initialize the Vite builder
304
+ builder = ViteClientBundleBuilder(
305
+ runtime_path=runtime_path,
306
+ vite_package_json=package_json,
307
+ vite_output_dir=output_dir,
308
+ vite_minify=False,
309
+ )
310
+
311
+ # Import the test module with JavaScript import
312
+ fixtures_dir = Path(__file__).parent / "fixtures" / "js_import"
313
+ (module,) = Jac.jac_import("app", str(fixtures_dir))
314
+
315
+ # Build the bundle
316
+ bundle = builder.build(module, force=True)
317
+
318
+ # Verify bundle structure
319
+ self.assertIsNotNone(bundle)
320
+ self.assertEqual(bundle.module_name, "app")
321
+ self.assertIn("JsImportTest", bundle.client_functions)
322
+ self.assertIn("app", bundle.client_functions)
323
+ self.assertIn("JS_IMPORT_LABEL", bundle.client_globals)
324
+
325
+ # Verify bundle code contains expected content
326
+ self.assertIn("function JsImportTest()", bundle.code)
327
+ self.assertIn("function app()", bundle.code)
328
+ self.assertIn('JS_IMPORT_LABEL = "JavaScript Import Test";', bundle.code)
329
+
330
+ # Verify JavaScript imports are present in the bundle
331
+ # The JavaScript functions should be available in the bundle
332
+ self.assertIn("formatMessage", bundle.code)
333
+ self.assertIn("calculateSum", bundle.code)
334
+ self.assertIn("JS_CONSTANT", bundle.code)
335
+ self.assertIn("MessageFormatter", bundle.code)
336
+
337
+ # Verify the JavaScript utility code is included
338
+ self.assertIn("Hello,", bundle.code) # From formatMessage function
339
+ self.assertIn("Imported from JavaScript", bundle.code) # From JS_CONSTANT
340
+
341
+ # Verify bundle was written to output directory
342
+ bundle_files = list(output_dir.glob("client.*.js"))
343
+ self.assertGreater(len(bundle_files), 0, "Expected at least one bundle file")
344
+
345
+ # Cleanup
346
+ builder.cleanup_temp_dir()
347
+
348
+ def test_jsx_fragments_and_spread_props(self) -> None:
349
+ """Test that JSX fragments and spread props work correctly."""
350
+ with tempfile.TemporaryDirectory() as temp_dir:
351
+ temp_path = Path(temp_dir)
352
+
353
+ # Create project with Vite installed
354
+ package_json, output_dir = self._create_test_project_with_vite(temp_path)
355
+ runtime_path = Path(__file__).parent.parent / "plugin" / "client_runtime.jac"
356
+
357
+ # Initialize the Vite builder
358
+ builder = ViteClientBundleBuilder(
359
+ runtime_path=runtime_path,
360
+ vite_package_json=package_json,
361
+ vite_output_dir=output_dir,
362
+ vite_minify=False,
363
+ )
364
+
365
+ # Import the test module with fragments and spread props
366
+ fixtures_dir = Path(__file__).parent / "fixtures" / "test_fragments_spread"
367
+ (module,) = Jac.jac_import("app", str(fixtures_dir))
368
+
369
+ # Build the bundle
370
+ bundle = builder.build(module, force=True)
371
+
372
+ # Verify bundle structure
373
+ self.assertIsNotNone(bundle)
374
+ self.assertEqual(bundle.module_name, "app")
375
+ self.assertIn("FragmentTest", bundle.client_functions)
376
+ self.assertIn("SpreadPropsTest", bundle.client_functions)
377
+ self.assertIn("MixedTest", bundle.client_functions)
378
+ self.assertIn("NestedFragments", bundle.client_functions)
379
+
380
+ # Verify spread props handling (Object.assign is used by compiler)
381
+ self.assertIn("Object.assign", bundle.code)
382
+
383
+ # Verify fragment test function exists
384
+ self.assertIn("function FragmentTest()", bundle.code)
385
+
386
+ # Verify spread props test function exists
387
+ self.assertIn("function SpreadPropsTest()", bundle.code)
388
+
389
+ # Verify bundle was written to output directory
390
+ bundle_files = list(output_dir.glob("client.*.js"))
391
+ self.assertGreater(len(bundle_files), 0, "Expected at least one bundle file")
392
+
393
+ # Cleanup
394
+ builder.cleanup_temp_dir()
395
+
396
+ def test_spawn_operator(self) -> None:
397
+ """Test that spawn operator generates correct __jacSpawn calls for both orderings and node types (root and UUID)."""
398
+ with tempfile.TemporaryDirectory() as temp_dir:
399
+ temp_path = Path(temp_dir)
400
+
401
+ # Create project with Vite installed
402
+ package_json, output_dir = self._create_test_project_with_vite(temp_path)
403
+ runtime_path = Path(__file__).parent.parent / "plugin" / "client_runtime.jac"
404
+
405
+ # Initialize the Vite builder
406
+ builder = ViteClientBundleBuilder(
407
+ runtime_path=runtime_path,
408
+ vite_package_json=package_json,
409
+ vite_output_dir=output_dir,
410
+ vite_minify=False,
411
+ )
412
+
413
+ # Import the test module with both spawn operator orderings
414
+ fixtures_dir = Path(__file__).parent / "fixtures" / "spawn_test"
415
+ (module,) = Jac.jac_import("app", str(fixtures_dir))
416
+
417
+ # Build the bundle
418
+ bundle = builder.build(module, force=True)
419
+
420
+ # Verify bundle structure
421
+ self.assertIsNotNone(bundle)
422
+ self.assertEqual(bundle.module_name, "app")
423
+ self.assertIn("app", bundle.client_functions)
424
+
425
+ # Verify complete __jacSpawn calls for root spawn scenarios
426
+ # Standard order: root spawn test_walker()
427
+ self.assertIn('__jacSpawn("test_walker", "", {})', bundle.code)
428
+
429
+ # Standard order: root spawn parameterized_walker(value=42)
430
+ self.assertIn('__jacSpawn("parameterized_walker", "", {', bundle.code)
431
+ self.assertIn('"value": 42', bundle.code)
432
+
433
+ # Reverse order: test_walker(message="Reverse spawn!") spawn root
434
+ # Should generate: __jacSpawn("test_walker", "", {message: "Reverse spawn!"})
435
+ self.assertRegex(
436
+ bundle.code,
437
+ r'__jacSpawn\("test_walker",\s*"",\s*\{[^}]*"message":\s*"Reverse spawn!"[^}]*\}\)'
438
+ )
439
+
440
+ # Verify UUID spawn scenarios with complete calls
441
+ # Standard UUID spawn: node_id spawn test_walker()
442
+ # Should generate: __jacSpawn("test_walker", node_id, {})
443
+ self.assertIn('__jacSpawn("test_walker", node_id, {})', bundle.code)
444
+ self.assertIn('"550e8400-e29b-41d4-a716-446655440000"', bundle.code)
445
+
446
+ # Reverse UUID spawn: parameterized_walker(value=100) spawn another_node_id
447
+ # Should generate: __jacSpawn("parameterized_walker", another_node_id, {value: 100})
448
+ self.assertRegex(
449
+ bundle.code,
450
+ r'__jacSpawn\("parameterized_walker",\s*another_node_id,\s*\{[^}]*"value":\s*100[^}]*\}\)'
451
+ )
452
+ self.assertIn('"6ba7b810-9dad-11d1-80b4-00c04fd430c8"', bundle.code)
453
+
454
+ # Verify positional argument mapping for walkers
455
+ self.assertRegex(
456
+ bundle.code,
457
+ r'__jacSpawn\("positional_walker",\s*node_id,\s*\{[^}]*"label":\s*"Node positional"[^}]*"count":\s*2',
458
+ )
459
+ # Verify spread (**kwargs) handling when walker is on left-hand side
460
+ self.assertRegex(
461
+ bundle.code,
462
+ r'__jacSpawn\("positional_walker",\s*"",\s*_objectSpread\(\{\s*"label":\s*"Spread order"[^}]*"count":\s*5\s*\},\s*extra_fields\)',
463
+ )
464
+
465
+ # Verify we have at least 7 __jacSpawn calls (previous cases + new positional/spread)
466
+ self.assertTrue(
467
+ bundle.code.count('__jacSpawn') >= 7,
468
+ "Expected at least 7 __jacSpawn calls in bundle"
469
+ )
470
+
471
+ # Verify bundle was written to output directory
472
+ bundle_files = list(output_dir.glob("client.*.js"))
473
+ self.assertGreater(len(bundle_files), 0, "Expected at least one bundle file")
474
+
475
+ # Cleanup
476
+ builder.cleanup_temp_dir()
@@ -0,0 +1,139 @@
1
+ """Test create-jac-app command."""
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+ from subprocess import run
7
+ from unittest import TestCase
8
+
9
+
10
+ class TestCreateJacApp(TestCase):
11
+ """Test create-jac-app command functionality."""
12
+
13
+ def test_create_jac_app(self) -> None:
14
+ """Test create-jac-app command."""
15
+ test_project_name = "test-jac-app"
16
+
17
+ # Create a temporary directory for testing
18
+ with tempfile.TemporaryDirectory() as temp_dir:
19
+ original_cwd = os.getcwd()
20
+ try:
21
+ # Change to temp directory
22
+ os.chdir(temp_dir)
23
+
24
+ # Run create-jac-app command
25
+ result = run(
26
+ [
27
+ "jac",
28
+ "create_jac_app",
29
+ test_project_name
30
+ ],
31
+ capture_output=True,
32
+ text=True,
33
+ check=True
34
+ )
35
+
36
+ # Check that command succeeded
37
+ self.assertEqual(result.returncode, 0)
38
+ self.assertIn(f"Successfully created Jac application '{test_project_name}'!", result.stdout)
39
+
40
+ # Verify project directory was created
41
+ project_path = os.path.join(temp_dir, test_project_name)
42
+ self.assertTrue(os.path.exists(project_path))
43
+ self.assertTrue(os.path.isdir(project_path))
44
+
45
+ # Verify package.json was created and has correct content
46
+ package_json_path = os.path.join(project_path, "package.json")
47
+ self.assertTrue(os.path.exists(package_json_path))
48
+
49
+ with open(package_json_path, "r") as f:
50
+ package_data = json.load(f)
51
+
52
+ self.assertEqual(package_data["name"], test_project_name)
53
+ self.assertEqual(package_data["type"], "module")
54
+ self.assertIn("vite", package_data["devDependencies"])
55
+ self.assertIn("build", package_data["scripts"])
56
+ self.assertIn("dev", package_data["scripts"])
57
+ self.assertIn("preview", package_data["scripts"])
58
+
59
+ # Verify app.jac file was created
60
+ app_jac_path = os.path.join(project_path, "app.jac")
61
+ self.assertTrue(os.path.exists(app_jac_path))
62
+
63
+ with open(app_jac_path, "r") as f:
64
+ app_jac_content = f.read()
65
+
66
+ self.assertIn("app()", app_jac_content)
67
+
68
+ # Verify README.md was created
69
+ readme_path = os.path.join(project_path, "README.md")
70
+ self.assertTrue(os.path.exists(readme_path))
71
+
72
+ with open(readme_path, "r") as f:
73
+ readme_content = f.read()
74
+
75
+ self.assertIn(f"# {test_project_name}", readme_content)
76
+ self.assertIn("jac serve app.jac", readme_content)
77
+
78
+ # Verify node_modules was created (npm install ran)
79
+ node_modules_path = os.path.join(project_path, "node_modules")
80
+ self.assertTrue(os.path.exists(node_modules_path))
81
+
82
+ finally:
83
+ # Return to original directory
84
+ os.chdir(original_cwd)
85
+
86
+ def test_create_jac_app_invalid_name(self) -> None:
87
+ """Test create-jac-app command with invalid project name."""
88
+ with tempfile.TemporaryDirectory() as temp_dir:
89
+ original_cwd = os.getcwd()
90
+ try:
91
+ os.chdir(temp_dir)
92
+
93
+ # Test with invalid name containing spaces
94
+ result = run(
95
+ [
96
+ "jac",
97
+ "create_jac_app",
98
+ "invalid name with spaces"
99
+ ],
100
+ capture_output=True,
101
+ text=True
102
+ )
103
+
104
+ # Should fail with non-zero exit code
105
+ self.assertNotEqual(result.returncode, 0)
106
+ self.assertIn("Project name must contain only letters, numbers, hyphens, and underscores", result.stderr)
107
+
108
+ finally:
109
+ os.chdir(original_cwd)
110
+
111
+ def test_create_jac_app_existing_directory(self) -> None:
112
+ """Test create-jac-app command when directory already exists."""
113
+ test_project_name = "existing-test-app"
114
+
115
+ with tempfile.TemporaryDirectory() as temp_dir:
116
+ original_cwd = os.getcwd()
117
+ try:
118
+ os.chdir(temp_dir)
119
+
120
+ # Create the directory first
121
+ os.makedirs(test_project_name)
122
+
123
+ # Try to create app with same name
124
+ result = run(
125
+ [
126
+ "jac",
127
+ "create_jac_app",
128
+ test_project_name
129
+ ],
130
+ capture_output=True,
131
+ text=True
132
+ )
133
+
134
+ # Should fail with non-zero exit code
135
+ self.assertNotEqual(result.returncode, 0)
136
+ self.assertIn(f"Directory '{test_project_name}' already exists", result.stderr)
137
+
138
+ finally:
139
+ os.chdir(original_cwd)