jac-client 0.2.6__py3-none-any.whl → 0.2.8__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 (66) hide show
  1. jac_client/examples/all-in-one/app.jac +573 -0
  2. jac_client/examples/all-in-one/components/CategoryFilter.jac +35 -0
  3. jac_client/examples/all-in-one/components/Header.jac +13 -0
  4. jac_client/examples/all-in-one/components/ProfitOverview.jac +50 -0
  5. jac_client/examples/all-in-one/components/Summary.jac +53 -0
  6. jac_client/examples/all-in-one/components/TransactionForm.jac +158 -0
  7. jac_client/examples/all-in-one/components/TransactionItem.jac +55 -0
  8. jac_client/examples/all-in-one/components/TransactionList.jac +37 -0
  9. jac_client/examples/all-in-one/components/navigation.jac +132 -0
  10. jac_client/examples/all-in-one/constants/categories.jac +37 -0
  11. jac_client/examples/all-in-one/constants/clients.jac +13 -0
  12. jac_client/examples/all-in-one/context/BudgetContext.jac +28 -0
  13. jac_client/examples/all-in-one/hooks/useBudget.jac +116 -0
  14. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +36 -0
  15. jac_client/examples/all-in-one/pages/BudgetPlanner.cl.jac +70 -0
  16. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +126 -0
  17. jac_client/examples/all-in-one/pages/FeaturesTest.cl.jac +552 -0
  18. jac_client/examples/all-in-one/pages/FeaturesTest.jac +126 -0
  19. jac_client/examples/all-in-one/pages/LandingPage.jac +101 -0
  20. jac_client/examples/all-in-one/pages/loginPage.jac +132 -0
  21. jac_client/examples/all-in-one/pages/nestedDemo.jac +61 -0
  22. jac_client/examples/all-in-one/pages/notFound.jac +24 -0
  23. jac_client/examples/all-in-one/pages/signupPage.jac +133 -0
  24. jac_client/examples/all-in-one/utils/formatters.jac +52 -0
  25. jac_client/examples/asset-serving/css-with-image/src/app.jac +3 -3
  26. jac_client/examples/asset-serving/image-asset/src/app.jac +3 -3
  27. jac_client/examples/asset-serving/import-alias/src/app.jac +3 -3
  28. jac_client/examples/basic/src/app.jac +3 -3
  29. jac_client/examples/basic-auth/src/app.jac +31 -37
  30. jac_client/examples/basic-auth-with-router/src/app.jac +16 -16
  31. jac_client/examples/basic-full-stack/src/app.jac +24 -30
  32. jac_client/examples/css-styling/js-styling/src/app.jac +5 -5
  33. jac_client/examples/css-styling/material-ui/src/app.jac +5 -5
  34. jac_client/examples/css-styling/pure-css/src/app.jac +5 -5
  35. jac_client/examples/css-styling/sass-example/src/app.jac +5 -5
  36. jac_client/examples/css-styling/styled-components/src/app.jac +5 -5
  37. jac_client/examples/css-styling/tailwind-example/src/app.jac +5 -5
  38. jac_client/examples/full-stack-with-auth/src/app.jac +16 -16
  39. jac_client/examples/ts-support/src/app.jac +4 -4
  40. jac_client/examples/with-router/src/app.jac +4 -4
  41. jac_client/plugin/cli.jac +160 -203
  42. jac_client/plugin/client.jac +8 -15
  43. jac_client/plugin/client_runtime.cl.jac +18 -14
  44. jac_client/plugin/impl/client.impl.jac +85 -26
  45. jac_client/plugin/impl/client_runtime.impl.jac +27 -9
  46. jac_client/plugin/plugin_config.jac +11 -11
  47. jac_client/plugin/src/compiler.jac +2 -1
  48. jac_client/plugin/src/impl/babel_processor.impl.jac +22 -17
  49. jac_client/plugin/src/impl/compiler.impl.jac +55 -18
  50. jac_client/plugin/src/impl/vite_bundler.impl.jac +215 -102
  51. jac_client/plugin/src/package_installer.jac +1 -1
  52. jac_client/plugin/src/vite_bundler.jac +9 -1
  53. jac_client/tests/conftest.py +10 -8
  54. jac_client/tests/fixtures/spawn_test/app.jac +15 -18
  55. jac_client/tests/fixtures/with-ts/app.jac +4 -4
  56. jac_client/tests/test_cli.py +105 -49
  57. jac_client/tests/test_it.py +297 -82
  58. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/METADATA +16 -7
  59. jac_client-0.2.8.dist-info/RECORD +97 -0
  60. jac_client/examples/all-in-one/src/app.jac +0 -841
  61. jac_client-0.2.6.dist-info/RECORD +0 -74
  62. /jac_client/examples/all-in-one/{src/button.jac → button.jac} +0 -0
  63. /jac_client/examples/all-in-one/{src/components → components}/button.jac +0 -0
  64. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/WHEEL +0 -0
  65. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/entry_points.txt +0 -0
  66. {jac_client-0.2.6.dist-info → jac_client-0.2.8.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,24 @@
1
- """Create package.json from config.json during bundling."""
1
+ """Get the client build directory from project config."""
2
+ impl ViteBundler._get_client_dir(self: ViteBundler) -> Path {
3
+ # Try to get from project config
4
+ try {
5
+ import from jaclang.project.config { get_config }
6
+ config = get_config();
7
+ if config is not None {
8
+ return config.get_client_dir();
9
+ }
10
+ } except ImportError { }
11
+ # Fallback to default
12
+ return self.project_dir / '.jac' / 'client';
13
+ }
2
14
 
15
+ """Create package.json from config.json during bundling."""
3
16
  impl ViteBundler.create_package_json(
4
17
  self: ViteBundler, project_name: Optional[str] = None
5
18
  ) -> Path {
6
- build_dir = self.project_dir / '.client-build';
7
- build_dir.mkdir(exist_ok=True);
8
- configs_dir = build_dir / '.jac-client.configs';
19
+ build_dir = self._get_client_dir();
20
+ build_dir.mkdir(parents=True, exist_ok=True);
21
+ configs_dir = build_dir / 'configs';
9
22
  configs_dir.mkdir(exist_ok=True);
10
23
  package_config = self.config_loader.get_package_config();
11
24
  package_json_path = configs_dir / 'package.json';
@@ -29,32 +42,12 @@ impl ViteBundler.create_package_json(
29
42
  if not name {
30
43
  name = self.project_dir.name or 'jac-app';
31
44
  }
32
- # Default hardcoded dependencies (always included)
33
- default_dependencies = {
34
- 'react': '^18.2.0',
35
- 'react-dom': '^18.2.0',
36
- 'react-router-dom': '^6.22.0'
37
- };
38
- default_dev_dependencies = {
39
- 'vite': '^6.4.1',
40
- '@babel/cli': '^7.28.3',
41
- '@babel/core': '^7.28.5',
42
- '@babel/preset-env': '^7.28.5',
43
- '@babel/preset-react': '^7.28.5',
44
- '@vitejs/plugin-react': '^4.2.1',
45
- 'typescript': '^5.3.3',
46
- '@types/react': '^18.2.0',
47
- '@types/react-dom': '^18.2.0'
48
- };
49
- # Merge user dependencies from TOML with defaults (user can override)
50
- user_dependencies = package_config.get('dependencies', {});
51
- user_dev_dependencies = package_config.get('devDependencies', {});
52
- dependencies = {** default_dependencies, ** user_dependencies};
53
- dev_dependencies = {** default_dev_dependencies, ** user_dev_dependencies};
45
+ dependencies = package_config.get('dependencies', {});
46
+ dev_dependencies = package_config.get('devDependencies', {});
54
47
  scripts = {
55
- 'build': 'npm run compile && vite build --config .client-build/.jac-client.configs/vite.config.js',
56
- 'dev': 'vite dev --config .client-build/.jac-client.configs/vite.config.js',
57
- 'preview': 'vite preview --config .client-build/.jac-client.configs/vite.config.js',
48
+ 'build': 'npm run compile && vite build --config .jac/client/configs/vite.config.js',
49
+ 'dev': 'vite dev --config .jac/client/configs/vite.config.js',
50
+ 'preview': 'vite preview --config .jac/client/configs/vite.config.js',
58
51
  'compile': 'babel compiled --out-dir build --extensions ".jsx,.js" --out-file-extension .js'
59
52
  };
60
53
  user_scripts = package_config.get('scripts', {});
@@ -90,9 +83,9 @@ impl ViteBundler.create_package_json(
90
83
 
91
84
  """Create tsconfig.json during build time, merging user config from jac.toml."""
92
85
  impl ViteBundler.create_tsconfig(self: ViteBundler) -> Path {
93
- build_dir = self.project_dir / '.client-build';
94
- build_dir.mkdir(exist_ok=True);
95
- configs_dir = build_dir / '.jac-client.configs';
86
+ build_dir = self._get_client_dir();
87
+ build_dir.mkdir(parents=True, exist_ok=True);
88
+ configs_dir = build_dir / 'configs';
96
89
  configs_dir.mkdir(exist_ok=True);
97
90
  tsconfig_path = configs_dir / 'tsconfig.json';
98
91
  # Default tsconfig settings
@@ -114,7 +107,7 @@ impl ViteBundler.create_tsconfig(self: ViteBundler) -> Path {
114
107
  'noFallthroughCasesInSwitch': True
115
108
  };
116
109
  default_include = ['components/**/*'];
117
- default_exclude = ['.client-build'];
110
+ default_exclude = ['.jac'];
118
111
  # Get user config from [plugins.client.ts] in jac.toml
119
112
  user_ts_config = self.config_loader.get_ts_config();
120
113
  user_compiler_options = user_ts_config.get('compilerOptions', {});
@@ -130,21 +123,21 @@ impl ViteBundler.create_tsconfig(self: ViteBundler) -> Path {
130
123
  'include': merged_include,
131
124
  'exclude': merged_exclude
132
125
  };
133
- # Write the config to .client-build/.jac-client.configs/tsconfig.json (no root config file)
126
+ # Write the config to .jac/client/configs/tsconfig.json (no root config file)
134
127
  tsconfig_path.write_text(json.dumps(tsconfig_data, indent=2), encoding='utf-8');
135
128
  return tsconfig_path;
136
129
  }
137
130
 
138
131
  """
139
- Clean up root package.json and move package-lock.json to .jac-client.configs/.
132
+ Clean up root package.json and move package-lock.json to configs/.
140
133
 
141
- Remove root package.json and move package-lock.json to .jac-client.configs/.
134
+ Remove root package.json and move package-lock.json to configs/.
142
135
  """
143
136
  impl ViteBundler._cleanup_root_package_files(self: ViteBundler) -> None {
144
137
  root_package_json = self.project_dir / 'package.json';
145
138
  root_package_lock = self.project_dir / 'package-lock.json';
146
- build_dir = self.project_dir / '.client-build';
147
- configs_dir = build_dir / '.jac-client.configs';
139
+ build_dir = self._get_client_dir();
140
+ configs_dir = build_dir / 'configs';
148
141
  configs_package_lock = configs_dir / 'package-lock.json';
149
142
  if root_package_lock.exists() {
150
143
  configs_dir.mkdir(exist_ok=True);
@@ -164,7 +157,7 @@ Ensure root package.json exists temporarily for npm commands.
164
157
  Create root package.json temporarily if it doesn't exist.
165
158
  """
166
159
  impl ViteBundler._ensure_root_package_json(self: ViteBundler) -> None {
167
- generated_package_json = self.project_dir / '.client-build' / '.jac-client.configs' / 'package.json';
160
+ generated_package_json = self._get_client_dir() / 'configs' / 'package.json';
168
161
  root_package_json = self.project_dir / 'package.json';
169
162
  if not generated_package_json.exists() {
170
163
  self.create_package_json();
@@ -235,51 +228,28 @@ impl ViteBundler._get_plugin_var_name(self: ViteBundler, plugin_name: str) -> st
235
228
 
236
229
  """Create vite.config.js from config.json during bundling."""
237
230
  impl ViteBundler.create_vite_config(self: ViteBundler, entry_file: Path) -> Path {
238
- build_dir = self.project_dir / '.client-build';
239
- build_dir.mkdir(exist_ok=True);
240
- configs_dir = build_dir / '.jac-client.configs';
231
+ build_dir = self._get_client_dir();
232
+ build_dir.mkdir(parents=True, exist_ok=True);
233
+ configs_dir = build_dir / 'configs';
241
234
  configs_dir.mkdir(exist_ok=True);
242
235
  vite_config_data = self.config_loader.get_vite_config();
243
236
  config_path = configs_dir / 'vite.config.js';
244
237
  # TypeScript is always enabled by default
245
- build_dir = self.project_dir / '.client-build';
246
238
  try {
247
- # Entry file path relative to .client-build/ (not project root)
239
+ # Entry file path relative to client build dir (not project root)
248
240
  entry_relative = entry_file.relative_to(build_dir).as_posix();
249
241
  } except ValueError {
250
- # Fallback: try relative to project_dir and strip .client-build/ prefix
251
- try {
252
- entry_relative_full = entry_file.relative_to(self.project_dir).as_posix();
253
- prefix = '.client-build/';
254
- if entry_relative_full.startswith(prefix) {
255
- # Remove '.client-build/' prefix (15 characters)
256
- entry_relative = entry_relative_full[15:];
257
- } else {
258
- entry_relative = entry_relative_full;
259
- }
260
- } except ValueError {
261
- entry_relative = entry_file.as_posix();
262
- }
242
+ # Fallback: use absolute path
243
+ entry_relative = entry_file.as_posix();
263
244
  }
264
245
  try {
265
- # Output dir path relative to .client-build/ (not project root)
246
+ # Output dir path relative to client build dir (not project root)
266
247
  output_relative = self.output_dir.relative_to(build_dir).as_posix();
267
248
  } except ValueError {
268
- # Fallback: try relative to project_dir and strip .client-build/ prefix
269
- try {
270
- output_relative_full = self.output_dir.relative_to(self.project_dir).as_posix();
271
- prefix = '.client-build/';
272
- if output_relative_full.startswith(prefix) {
273
- # Remove '.client-build/' prefix (15 characters)
274
- output_relative = output_relative_full[15:];
275
- } else {
276
- output_relative = output_relative_full;
277
- }
278
- } except ValueError {
279
- output_relative = self.output_dir.as_posix();
280
- }
249
+ # Fallback: use absolute path
250
+ output_relative = self.output_dir.as_posix();
281
251
  }
282
- # Calculate compiled directory path for aliases (relative to .client-build/)
252
+ # Calculate compiled directory path for aliases (relative to client build dir)
283
253
  if entry_relative.endswith('/build/main.js') {
284
254
  compiled_utils_relative = entry_relative[:-13] + '/compiled/client_runtime.js';
285
255
  compiled_assets_relative = entry_relative[:-13] + '/compiled/assets';
@@ -333,9 +303,9 @@ impl ViteBundler.create_vite_config(self: ViteBundler, entry_file: Path) -> Path
333
303
  import path from "path";
334
304
  import {{ fileURLToPath }} from "url";
335
305
  {imports_section}const __dirname = path.dirname(fileURLToPath(import.meta.url));
336
- // Config is in .jac-client.configs/ inside .client-build/, so go up one level to .client-build/, then up one more to project root
306
+ // Config is in configs/ inside .jac/client/, so go up one level to .jac/client/, then up two more to project root
337
307
  const buildDir = path.resolve(__dirname, "..");
338
- const projectRoot = path.resolve(__dirname, "../..");
308
+ const projectRoot = path.resolve(__dirname, "../../..");
339
309
 
340
310
  /**
341
311
  * Vite configuration generated from config.json (in project root)
@@ -344,13 +314,13 @@ const projectRoot = path.resolve(__dirname, "../..");
344
314
 
345
315
  export default defineConfig({{
346
316
  plugins: [{(newline + plugins_str + newline + ' ') if plugins_str else ''}],
347
- root: buildDir, // base folder (.client-build/) so vite can find node_modules
317
+ root: buildDir, // base folder (.jac/client/) so vite can find node_modules
348
318
  build: {{
349
319
  rollupOptions: {{
350
320
  input: path.resolve(buildDir, "{entry_relative}"), // your compiled entry file
351
321
  output: {{
352
322
  entryFileNames: "client.[hash].js", // name of the final js file
353
- assetFileNames: "[name].[ext]",
323
+ assetFileNames: (assetInfo) => assetInfo.name?.endsWith('.css') ? 'styles.css' : '[name].[ext]',
354
324
  }},
355
325
  }},
356
326
  outDir: path.resolve(buildDir, "{output_relative}"), // final bundled output
@@ -391,14 +361,8 @@ impl ViteBundler.read_bundle(self: ViteBundler) -> tuple[str, str] {
391
361
 
392
362
  """Find the generated Vite CSS file."""
393
363
  impl ViteBundler.find_css(self: ViteBundler) -> Optional[Path] {
394
- css_file = self.output_dir / 'main.css';
395
- if css_file.exists() {
396
- return css_file;
397
- }
398
- for file in self.output_dir.glob('*.css') {
399
- return file;
400
- }
401
- return None;
364
+ css_file = self.output_dir / 'styles.css';
365
+ return css_file if css_file.exists() else None;
402
366
  }
403
367
 
404
368
  """Find the generated Vite bundle file."""
@@ -409,10 +373,10 @@ impl ViteBundler.find_bundle(self: ViteBundler) -> Optional[Path] {
409
373
  return None;
410
374
  }
411
375
 
412
- """Run Vite build with generated config in .client-build/.jac-client.configs/."""
376
+ """Run Vite build with generated config in .jac/client/configs/."""
413
377
  impl ViteBundler.build(self: ViteBundler, entry_file: Optional[Path] = None) -> None {
414
378
  self.output_dir.mkdir(parents=True, exist_ok=True);
415
- generated_package_json = self.project_dir / '.client-build' / '.jac-client.configs' / 'package.json';
379
+ generated_package_json = self._get_client_dir() / 'configs' / 'package.json';
416
380
  if not generated_package_json.exists() {
417
381
  self.create_package_json();
418
382
  } else {
@@ -420,18 +384,18 @@ impl ViteBundler.build(self: ViteBundler, entry_file: Optional[Path] = None) ->
420
384
  self.create_tsconfig();
421
385
  }
422
386
  try {
423
- build_dir = self.project_dir / '.client-build';
387
+ build_dir = self._get_client_dir();
424
388
  node_modules = build_dir / 'node_modules';
425
389
  if not node_modules.exists() {
426
- # Temporarily copy package.json to .client-build/ for npm install
390
+ # Temporarily copy package.json to client build dir for npm install
427
391
  build_package_json = build_dir / 'package.json';
428
- configs_package_json = build_dir / '.jac-client.configs' / 'package.json';
392
+ configs_package_json = build_dir / 'configs' / 'package.json';
429
393
  if configs_package_json.exists() and not build_package_json.exists() {
430
394
  import shutil;
431
395
  shutil.copy2(configs_package_json, build_package_json);
432
396
  }
433
397
  try {
434
- # Install to .client-build/node_modules
398
+ # Install to .jac/client/node_modules
435
399
  subprocess.run(
436
400
  ['npm', 'install'],
437
401
  cwd=build_dir,
@@ -449,27 +413,24 @@ impl ViteBundler.build(self: ViteBundler, entry_file: Optional[Path] = None) ->
449
413
  ) ;
450
414
  }
451
415
  }
452
- build_dir = self.project_dir / '.client-build';
453
416
  if self.config_path {
454
417
  # Make config path relative to build_dir (where vite runs from)
455
- config_str = str(self.config_path);
456
- if '.client-build' in config_str {
457
- # Config is in .client-build/, make it relative to build_dir
418
+ try {
458
419
  config_rel = self.config_path.relative_to(build_dir);
459
420
  command = ['npx', 'vite', 'build', '--config', str(config_rel)];
460
- } else {
461
- # Config is outside .client-build/, use absolute path
421
+ } except ValueError {
422
+ # Config is outside client build dir, use absolute path
462
423
  command = ['npx', 'vite', 'build', '--config', str(self.config_path)];
463
424
  }
464
425
  } elif entry_file {
465
426
  generated_config = self.create_vite_config(entry_file);
466
- # Config is in .client-build/.jac-client.configs/, make it relative to build_dir
427
+ # Config is in configs/, make it relative to build_dir
467
428
  config_rel = generated_config.relative_to(build_dir);
468
429
  command = ['npx', 'vite', 'build', '--config', str(config_rel)];
469
430
  } else {
470
431
  command = ['npm', 'run', 'build'];
471
432
  }
472
- # Run vite from .client-build/ directory so it can find node_modules
433
+ # Run vite from client build directory so it can find node_modules
473
434
  result = subprocess.run(
474
435
  command, cwd=build_dir, check=False, capture_output=True, text=True
475
436
  );
@@ -480,15 +441,15 @@ impl ViteBundler.build(self: ViteBundler, entry_file: Optional[Path] = None) ->
480
441
  ) from None ;
481
442
  }
482
443
  } finally {
483
- # Clean up temporary package.json in .client-build/
444
+ # Clean up temporary package.json in client build dir
484
445
  build_package_json = build_dir / 'package.json';
485
446
  if build_package_json.exists() {
486
447
  build_package_json.unlink();
487
448
  }
488
- # Move package-lock.json to .jac-client.configs/ if it exists
449
+ # Move package-lock.json to configs/ if it exists
489
450
  build_package_lock = build_dir / 'package-lock.json';
490
451
  if build_package_lock.exists() {
491
- configs_package_lock = build_dir / '.jac-client.configs' / 'package-lock.json';
452
+ configs_package_lock = build_dir / 'configs' / 'package-lock.json';
492
453
  if configs_package_lock.exists() {
493
454
  configs_package_lock.unlink();
494
455
  }
@@ -506,8 +467,160 @@ impl ViteBundler.init(
506
467
  config_path: Optional[Path] = None
507
468
  ) {
508
469
  self.project_dir = project_dir;
509
- self.output_dir = output_dir or (project_dir / '.client-build' / 'dist');
510
470
  self.minify = minify;
511
471
  self.config_path = config_path;
512
472
  self.config_loader = JacClientConfig(project_dir);
473
+ # Set output_dir after config_loader is initialized so _get_client_dir works
474
+ self.output_dir = output_dir or (self._get_client_dir() / 'dist');
475
+ }
476
+
477
+ """Create a dev-mode vite config with API proxy for HMR."""
478
+ impl ViteBundler.create_dev_vite_config(
479
+ self: ViteBundler, entry_file: Path, api_port: int = 8000
480
+ ) -> Path {
481
+ build_dir = self._get_client_dir();
482
+ build_dir.mkdir(parents=True, exist_ok=True);
483
+ configs_dir = build_dir / 'configs';
484
+ configs_dir.mkdir(exist_ok=True);
485
+ config_path = configs_dir / 'vite.dev.config.js';
486
+ # Get entry file relative path
487
+ try {
488
+ entry_relative = entry_file.relative_to(build_dir).as_posix();
489
+ } except ValueError {
490
+ entry_relative = entry_file.as_posix();
491
+ }
492
+ # Calculate paths for aliases
493
+ if entry_relative.endswith('/build/main.js') {
494
+ compiled_utils_relative = entry_relative[:-13] + '/compiled/client_runtime.js';
495
+ compiled_assets_relative = entry_relative[:-13] + '/compiled/assets';
496
+ } elif entry_relative.endswith('build/main.js') {
497
+ compiled_utils_relative = 'compiled/client_runtime.js';
498
+ compiled_assets_relative = 'compiled/assets';
499
+ } else {
500
+ compiled_utils_relative = 'compiled/client_runtime.js';
501
+ compiled_assets_relative = 'compiled/assets';
502
+ }
503
+ # Extensions for TypeScript
504
+ extensions = ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'];
505
+ extensions_str = ', '.join(f'"{ext}"' for ext in extensions);
506
+ # Generate dev config with proxy for API routes
507
+ config_content = f'''import {{ defineConfig }} from "vite";
508
+ import path from "path";
509
+ import {{ fileURLToPath }} from "url";
510
+ import react from "@vitejs/plugin-react";
511
+
512
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
513
+ const buildDir = path.resolve(__dirname, "..");
514
+ const projectRoot = path.resolve(__dirname, "../../..");
515
+
516
+ /**
517
+ * Vite DEV configuration for HMR mode
518
+ * Proxies API routes to Python server at localhost:{api_port}
519
+ */
520
+ export default defineConfig({{
521
+ plugins: [react()],
522
+ root: buildDir,
523
+ publicDir: false,
524
+ server: {{
525
+ watch: {{
526
+ usePolling: true,
527
+ interval: 100,
528
+ }},
529
+ proxy: {{
530
+ "/walker": {{
531
+ target: "http://localhost:{api_port}",
532
+ changeOrigin: true,
533
+ }},
534
+ "/function": {{
535
+ target: "http://localhost:{api_port}",
536
+ changeOrigin: true,
537
+ }},
538
+ "/user": {{
539
+ target: "http://localhost:{api_port}",
540
+ changeOrigin: true,
541
+ }},
542
+ "/introspect": {{
543
+ target: "http://localhost:{api_port}",
544
+ changeOrigin: true,
545
+ }},
546
+ }},
547
+ }},
548
+ resolve: {{
549
+ alias: {{
550
+ "@jac-client/utils": path.resolve(buildDir, "{compiled_utils_relative}"),
551
+ "@jac-client/assets": path.resolve(buildDir, "{compiled_assets_relative}"),
552
+ }},
553
+ extensions: [{extensions_str}],
554
+ }},
555
+ }});
556
+ ''';
557
+ config_path.write_text(config_content, encoding='utf-8');
558
+ return config_path;
559
+ }
560
+
561
+ """Start Vite dev server as a subprocess."""
562
+ impl ViteBundler.start_dev_server(self: ViteBundler, port: int = 3000) -> Any {
563
+ build_dir = self._get_client_dir();
564
+ node_modules = build_dir / 'node_modules';
565
+ # Create/update index.html for dev server (load from compiled/ for HMR)
566
+ index_html = build_dir / 'index.html';
567
+ index_content = '''<!DOCTYPE html>
568
+ <html lang="en">
569
+ <head>
570
+ <meta charset="UTF-8" />
571
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
572
+ <title>Jac App (Dev)</title>
573
+ </head>
574
+ <body>
575
+ <div id="root"></div>
576
+ <script type="module" src="/compiled/_entry.js"></script>
577
+ </body>
578
+ </html>
579
+ ''';
580
+ index_html.write_text(index_content, encoding='utf-8');
581
+ # Ensure dependencies are installed
582
+ if not node_modules.exists() {
583
+ generated_package_json = build_dir / 'configs' / 'package.json';
584
+ if not generated_package_json.exists() {
585
+ self.create_package_json();
586
+ }
587
+ # Temporarily copy package.json for npm install
588
+ build_package_json = build_dir / 'package.json';
589
+ if not build_package_json.exists() {
590
+ shutil.copy2(generated_package_json, build_package_json);
591
+ }
592
+ try {
593
+ print("[Vite] Installing dependencies...");
594
+ subprocess.run(
595
+ ['npm', 'install'],
596
+ cwd=build_dir,
597
+ check=True,
598
+ capture_output=True,
599
+ text=True
600
+ );
601
+ } except subprocess.CalledProcessError as e {
602
+ print(f"[Vite] Error installing dependencies: {e.stderr}");
603
+ raise ;
604
+ } finally {
605
+ # Clean up temp package.json
606
+ if build_package_json.exists() {
607
+ build_package_json.unlink();
608
+ }
609
+ }
610
+ }
611
+ # Find the dev config
612
+ dev_config = build_dir / 'configs' / 'vite.dev.config.js';
613
+ if not dev_config.exists() {
614
+ raise ClientBundleError(
615
+ "Dev config not found. Call create_dev_vite_config first."
616
+ ) ;
617
+ }
618
+ config_rel = dev_config.relative_to(build_dir);
619
+ print(f"[Vite] Starting dev server on port {port}...");
620
+ # Start Vite in dev mode (let output go to terminal for HMR visibility)
621
+ process = subprocess.Popen(
622
+ ['npx', 'vite', '--config', str(config_rel), '--port', str(port)],
623
+ cwd=build_dir
624
+ );
625
+ return process;
513
626
  }
@@ -22,5 +22,5 @@ class PackageInstaller {
22
22
  self: PackageInstaller, package_name: str, is_dev: bool = False
23
23
  ) -> None;
24
24
 
25
- def list_packages(self: PackageInstaller) -> dict[str, dict[str, str]];
25
+ def list_packages(self: PackageInstaller) -> dict[str, dict[(str, str)]];
26
26
  }
@@ -4,7 +4,7 @@ import json;
4
4
  import shutil;
5
5
  import subprocess;
6
6
  import from pathlib { Path }
7
- import from typing { Optional }
7
+ import from typing { Any, Optional }
8
8
  import from jaclang.runtimelib.client_bundle { ClientBundleError }
9
9
  import from .config_loader { JacClientConfig }
10
10
  """Handles Vite bundling operations."""
@@ -17,6 +17,7 @@ class ViteBundler {
17
17
  config_path: Optional[Path] = None
18
18
  );
19
19
 
20
+ def _get_client_dir(self: ViteBundler) -> Path;
20
21
  def build(self: ViteBundler, entry_file: Optional[Path] = None) -> None;
21
22
  def find_bundle(self: ViteBundler) -> Optional[Path];
22
23
  def find_css(self: ViteBundler) -> Optional[Path];
@@ -33,4 +34,11 @@ class ViteBundler {
33
34
  ) -> Path;
34
35
 
35
36
  def create_tsconfig(self: ViteBundler) -> Path;
37
+ """Create a dev-mode vite config with API proxy for HMR."""
38
+ def create_dev_vite_config(
39
+ self: ViteBundler, entry_file: Path, api_port: int = 8000
40
+ ) -> Path;
41
+
42
+ """Start Vite dev server as a subprocess."""
43
+ def start_dev_server(self: ViteBundler, port: int = 3000) -> Any;
36
44
  }
@@ -108,7 +108,7 @@ def npm_cache_dir() -> Generator[Path, None, None]:
108
108
  """Session-scoped fixture that provides a directory with npm packages installed.
109
109
 
110
110
  This runs npm install once per test session and provides the path to the
111
- .jac-client.configs directory containing node_modules.
111
+ .jac/client/configs directory containing node_modules.
112
112
  """
113
113
  global _npm_cache_dir
114
114
 
@@ -156,13 +156,14 @@ def vite_project_dir(npm_cache_dir: Path, tmp_path: Path) -> Path:
156
156
  jac_toml = tmp_path / "jac.toml"
157
157
  jac_toml.write_text(_get_minimal_jac_toml())
158
158
 
159
- # Copy .jac-client.configs directory (contains package.json)
160
- source_configs = npm_cache_dir / ".jac-client.configs"
161
- dest_configs = tmp_path / ".jac-client.configs"
159
+ # Copy .jac/client/configs directory (contains package.json)
160
+ source_configs = npm_cache_dir / ".jac" / "client" / "configs"
161
+ dest_configs = tmp_path / ".jac" / "client" / "configs"
162
162
  if source_configs.exists():
163
+ dest_configs.parent.mkdir(parents=True, exist_ok=True)
163
164
  shutil.copytree(source_configs, dest_configs, symlinks=True)
164
165
 
165
- # Copy node_modules from project root (npm installs there, not in .jac-client.configs)
166
+ # Copy node_modules from project root (npm installs there, not in .jac/client/configs)
166
167
  source_node_modules = npm_cache_dir / "node_modules"
167
168
  dest_node_modules = tmp_path / "node_modules"
168
169
  if source_node_modules.exists():
@@ -195,10 +196,11 @@ antd = "^6.0.0"
195
196
  jac_toml = tmp_path / "jac.toml"
196
197
  jac_toml.write_text(jac_toml_content)
197
198
 
198
- # Copy base .jac-client.configs first for faster install
199
- source_configs = npm_cache_dir / ".jac-client.configs"
200
- dest_configs = tmp_path / ".jac-client.configs"
199
+ # Copy base .jac/client/configs first for faster install
200
+ source_configs = npm_cache_dir / ".jac" / "client" / "configs"
201
+ dest_configs = tmp_path / ".jac" / "client" / "configs"
201
202
  if source_configs.exists():
203
+ dest_configs.parent.mkdir(parents=True, exist_ok=True)
202
204
  shutil.copytree(source_configs, dest_configs, symlinks=True)
203
205
 
204
206
  # Copy base node_modules for faster install (npm will add antd on top)
@@ -32,51 +32,48 @@ walker positional_walker {
32
32
  }
33
33
 
34
34
  # Client-side code testing both spawn orderings
35
- cl import from react {
36
- useState,
37
- useEffect
38
- }
35
+ cl import from react { useEffect }
39
36
 
40
37
  cl {
41
38
  def app() -> any {
42
- [standardResult, setStandardResult] = useState(None);
43
- [standardComputed, setStandardComputed] = useState(None);
44
- [reverseResult, setReverseResult] = useState(None);
45
- [uuidResult, setUuidResult] = useState(None);
46
- [reverseUuidResult, setReverseUuidResult] = useState(None);
47
- [positionalResult, setPositionalResult] = useState(None);
48
- [spreadResult, setSpreadResult] = useState(None);
39
+ has standardResult: any = None;
40
+ has standardComputed: any = None;
41
+ has reverseResult: any = None;
42
+ has uuidResult: any = None;
43
+ has reverseUuidResult: any = None;
44
+ has positionalResult: any = None;
45
+ has spreadResult: any = None;
49
46
 
50
47
  async def loadData() -> None {
51
48
  # Test standard spawn order: node spawn walker()
52
49
  data1 = root spawn test_walker();
53
- setStandardResult(data1);
50
+ standardResult = data1;
54
51
 
55
52
  data2 = root spawn parameterized_walker(value=42);
56
- setStandardComputed(data2);
53
+ standardComputed = data2;
57
54
 
58
55
  # Test reverse spawn order: walker() spawn node
59
56
  data3 = test_walker(message="Reverse spawn!") spawn root;
60
- setReverseResult(data3);
57
+ reverseResult = data3;
61
58
 
62
59
  # Test spawn with UUID string: uuid_string spawn walker()
63
60
  node_id = "550e8400-e29b-41d4-a716-446655440000";
64
61
  data4 = node_id spawn test_walker();
65
- setUuidResult(data4);
62
+ uuidResult = data4;
66
63
 
67
64
  # Test reverse spawn with UUID string: walker() spawn uuid_string
68
65
  another_node_id = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
69
66
  data5 = parameterized_walker(value=100) spawn another_node_id;
70
- setReverseUuidResult(data5);
67
+ reverseUuidResult = data5;
71
68
 
72
69
  # Test positional walker arguments inferred from has fields
73
70
  data6 = node_id spawn positional_walker("Node positional", 2);
74
- setPositionalResult(data6);
71
+ positionalResult = data6;
75
72
 
76
73
  # Test **kwargs via spread when walker is on left-hand side
77
74
  extra_fields = {"metadata": {"source": "client-side"}};
78
75
  data7 = positional_walker("Spread order", 5, **extra_fields) spawn root;
79
- setSpreadResult(data7);
76
+ spreadResult = data7;
80
77
  }
81
78
 
82
79
  useEffect(lambda -> None{ loadData();} , []);
@@ -1,11 +1,11 @@
1
1
 
2
2
  # Pages
3
- cl import from react { useState, useEffect }
3
+ cl import from react { useEffect }
4
4
  cl import from ".components/Button.tsx" { Button }
5
5
 
6
6
  cl {
7
7
  def app() -> any {
8
- [count, setCount] = useState(0);
8
+ has count: int = 0;
9
9
  useEffect(lambda -> None{ console.log("Count: ", count);} , [count]);
10
10
  return <div
11
11
  style={{padding: "2rem", fontFamily: "Arial, sans-serif"}}
@@ -21,12 +21,12 @@ cl {
21
21
  >
22
22
  <Button
23
23
  label="Increment"
24
- onClick={lambda -> None{ setCount(count + 1);} }
24
+ onClick={lambda -> None{ count = count + 1;} }
25
25
  variant="primary"
26
26
  />
27
27
  <Button
28
28
  label="Reset"
29
- onClick={lambda -> None{ setCount(0);} }
29
+ onClick={lambda -> None{ count = 0;} }
30
30
  variant="secondary"
31
31
  />
32
32
  </div>