jac-client 0.2.6__py3-none-any.whl → 0.2.11__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 (119) hide show
  1. jac_client/examples/all-in-one/{src/button.jac → button.jac} +4 -3
  2. jac_client/examples/all-in-one/components/CategoryFilter.jac +47 -0
  3. jac_client/examples/all-in-one/components/Header.jac +17 -0
  4. jac_client/examples/all-in-one/components/ProfitOverview.jac +64 -0
  5. jac_client/examples/all-in-one/components/Summary.jac +76 -0
  6. jac_client/examples/all-in-one/components/TransactionForm.jac +188 -0
  7. jac_client/examples/all-in-one/components/TransactionItem.jac +62 -0
  8. jac_client/examples/all-in-one/components/TransactionList.jac +44 -0
  9. jac_client/examples/all-in-one/components/button.jac +8 -0
  10. jac_client/examples/all-in-one/components/navigation.jac +126 -0
  11. jac_client/examples/all-in-one/constants/categories.jac +36 -0
  12. jac_client/examples/all-in-one/constants/clients.jac +12 -0
  13. jac_client/examples/all-in-one/context/BudgetContext.jac +31 -0
  14. jac_client/examples/all-in-one/hooks/useBudget.jac +122 -0
  15. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +37 -0
  16. jac_client/examples/all-in-one/main.jac +542 -0
  17. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +140 -0
  18. jac_client/examples/all-in-one/pages/FeaturesTest.jac +157 -0
  19. jac_client/examples/all-in-one/pages/LandingPage.jac +124 -0
  20. jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +65 -0
  21. jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +675 -0
  22. jac_client/examples/all-in-one/pages/loginPage.jac +127 -0
  23. jac_client/examples/all-in-one/pages/nestedDemo.jac +54 -0
  24. jac_client/examples/all-in-one/pages/notFound.jac +18 -0
  25. jac_client/examples/all-in-one/pages/signupPage.jac +127 -0
  26. jac_client/examples/all-in-one/utils/formatters.jac +49 -0
  27. jac_client/examples/asset-serving/css-with-image/main.jac +92 -0
  28. jac_client/examples/asset-serving/image-asset/main.jac +56 -0
  29. jac_client/examples/asset-serving/import-alias/main.jac +109 -0
  30. jac_client/examples/basic/main.jac +23 -0
  31. jac_client/examples/basic-auth/main.jac +363 -0
  32. jac_client/examples/basic-auth-with-router/main.jac +451 -0
  33. jac_client/examples/basic-full-stack/main.jac +362 -0
  34. jac_client/examples/css-styling/js-styling/main.jac +63 -0
  35. jac_client/examples/css-styling/material-ui/main.jac +122 -0
  36. jac_client/examples/css-styling/pure-css/main.jac +55 -0
  37. jac_client/examples/css-styling/sass-example/main.jac +55 -0
  38. jac_client/examples/css-styling/styled-components/main.jac +62 -0
  39. jac_client/examples/css-styling/tailwind-example/main.jac +74 -0
  40. jac_client/examples/full-stack-with-auth/main.jac +696 -0
  41. jac_client/examples/little-x/main.jac +681 -0
  42. jac_client/examples/little-x/src/submit-button.jac +15 -14
  43. jac_client/examples/nested-folders/nested-advance/main.jac +26 -0
  44. jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +4 -6
  45. jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +9 -13
  46. jac_client/examples/nested-folders/nested-advance/src/level1/Card.jac +29 -32
  47. jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +12 -18
  48. jac_client/examples/nested-folders/nested-basic/{src/app.jac → main.jac} +7 -5
  49. jac_client/examples/nested-folders/nested-basic/src/button.jac +4 -3
  50. jac_client/examples/nested-folders/nested-basic/src/components/button.jac +4 -3
  51. jac_client/examples/ts-support/main.jac +35 -0
  52. jac_client/examples/with-router/main.jac +286 -0
  53. jac_client/plugin/cli.jac +507 -470
  54. jac_client/plugin/client.jac +30 -12
  55. jac_client/plugin/client_runtime.cl.jac +25 -15
  56. jac_client/plugin/impl/client.impl.jac +126 -26
  57. jac_client/plugin/impl/client_runtime.impl.jac +182 -10
  58. jac_client/plugin/plugin_config.jac +216 -34
  59. jac_client/plugin/src/__init__.jac +0 -2
  60. jac_client/plugin/src/compiler.jac +2 -2
  61. jac_client/plugin/src/config_loader.jac +1 -0
  62. jac_client/plugin/src/desktop_config.jac +31 -0
  63. jac_client/plugin/src/impl/compiler.impl.jac +99 -30
  64. jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
  65. jac_client/plugin/src/impl/desktop_config.impl.jac +191 -0
  66. jac_client/plugin/src/impl/jac_to_js.impl.jac +5 -1
  67. jac_client/plugin/src/impl/package_installer.impl.jac +20 -20
  68. jac_client/plugin/src/impl/vite_bundler.impl.jac +384 -144
  69. jac_client/plugin/src/package_installer.jac +1 -1
  70. jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
  71. jac_client/plugin/src/targets/desktop_target.jac +37 -0
  72. jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2347 -0
  73. jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
  74. jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
  75. jac_client/plugin/src/targets/register.jac +21 -0
  76. jac_client/plugin/src/targets/registry.jac +87 -0
  77. jac_client/plugin/src/targets/web_target.jac +35 -0
  78. jac_client/plugin/src/vite_bundler.jac +15 -1
  79. jac_client/plugin/utils/__init__.jac +3 -0
  80. jac_client/plugin/utils/bun_installer.jac +16 -0
  81. jac_client/plugin/utils/impl/bun_installer.impl.jac +99 -0
  82. jac_client/templates/client.jacpack +72 -0
  83. jac_client/templates/fullstack.jacpack +61 -0
  84. jac_client/tests/conftest.py +110 -52
  85. jac_client/tests/fixtures/spawn_test/app.jac +64 -70
  86. jac_client/tests/fixtures/with-ts/app.jac +28 -28
  87. jac_client/tests/test_cli.py +280 -113
  88. jac_client/tests/test_e2e.py +232 -0
  89. jac_client/tests/test_helpers.py +58 -0
  90. jac_client/tests/test_it.py +325 -154
  91. jac_client/tests/test_it_desktop.py +891 -0
  92. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/METADATA +20 -11
  93. jac_client-0.2.11.dist-info/RECORD +113 -0
  94. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/WHEEL +1 -1
  95. jac_client/examples/all-in-one/src/app.jac +0 -841
  96. jac_client/examples/all-in-one/src/components/button.jac +0 -7
  97. jac_client/examples/asset-serving/css-with-image/src/app.jac +0 -88
  98. jac_client/examples/asset-serving/image-asset/src/app.jac +0 -55
  99. jac_client/examples/asset-serving/import-alias/src/app.jac +0 -111
  100. jac_client/examples/basic/src/app.jac +0 -21
  101. jac_client/examples/basic-auth/src/app.jac +0 -377
  102. jac_client/examples/basic-auth-with-router/src/app.jac +0 -464
  103. jac_client/examples/basic-full-stack/src/app.jac +0 -365
  104. jac_client/examples/css-styling/js-styling/src/app.jac +0 -84
  105. jac_client/examples/css-styling/material-ui/src/app.jac +0 -122
  106. jac_client/examples/css-styling/pure-css/src/app.jac +0 -64
  107. jac_client/examples/css-styling/sass-example/src/app.jac +0 -64
  108. jac_client/examples/css-styling/styled-components/src/app.jac +0 -71
  109. jac_client/examples/css-styling/tailwind-example/src/app.jac +0 -63
  110. jac_client/examples/full-stack-with-auth/src/app.jac +0 -722
  111. jac_client/examples/little-x/src/app.jac +0 -719
  112. jac_client/examples/nested-folders/nested-advance/src/app.jac +0 -35
  113. jac_client/examples/ts-support/src/app.jac +0 -35
  114. jac_client/examples/with-router/src/app.jac +0 -323
  115. jac_client/plugin/src/babel_processor.jac +0 -18
  116. jac_client/plugin/src/impl/babel_processor.impl.jac +0 -84
  117. jac_client-0.2.6.dist-info/RECORD +0 -74
  118. {jac_client-0.2.6.dist-info → jac_client-0.2.11.dist-info}/entry_points.txt +0 -0
  119. {jac_client-0.2.6.dist-info → jac_client-0.2.11.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,39 +42,16 @@ 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', {});
47
+ # Vite handles JSX/TSX transpilation natively with Bun - no Babel compile step needed
54
48
  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',
58
- 'compile': 'babel compiled --out-dir build --extensions ".jsx,.js" --out-file-extension .js'
49
+ 'build': 'vite build --config .jac/client/configs/vite.config.js',
50
+ 'dev': 'vite dev --config .jac/client/configs/vite.config.js',
51
+ 'preview': 'vite preview --config .jac/client/configs/vite.config.js'
59
52
  };
60
53
  user_scripts = package_config.get('scripts', {});
61
54
  scripts.update(user_scripts);
62
- babel_config = {
63
- 'presets': [['@babel/preset-env', {'modules': False}], '@babel/preset-react']
64
- };
65
55
  package_data = {
66
56
  'name': name,
67
57
  'version': package_config.get('version', '1.0.0'),
@@ -70,8 +60,7 @@ impl ViteBundler.create_package_json(
70
60
  'main': 'index.js',
71
61
  'scripts': scripts,
72
62
  'dependencies': dependencies,
73
- 'devDependencies': dev_dependencies,
74
- 'babel': babel_config
63
+ 'devDependencies': dev_dependencies
75
64
  };
76
65
  for (key, value) in package_config.items() {
77
66
  if (
@@ -85,14 +74,16 @@ impl ViteBundler.create_package_json(
85
74
  }
86
75
  # Generate tsconfig.json during build time
87
76
  self.create_tsconfig();
77
+ # Generate config files (postcss, tailwind, etc.) from jac.toml
78
+ self.create_config_files();
88
79
  return package_json_path;
89
80
  }
90
81
 
91
82
  """Create tsconfig.json during build time, merging user config from jac.toml."""
92
83
  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';
84
+ build_dir = self._get_client_dir();
85
+ build_dir.mkdir(parents=True, exist_ok=True);
86
+ configs_dir = build_dir / 'configs';
96
87
  configs_dir.mkdir(exist_ok=True);
97
88
  tsconfig_path = configs_dir / 'tsconfig.json';
98
89
  # Default tsconfig settings
@@ -114,7 +105,7 @@ impl ViteBundler.create_tsconfig(self: ViteBundler) -> Path {
114
105
  'noFallthroughCasesInSwitch': True
115
106
  };
116
107
  default_include = ['components/**/*'];
117
- default_exclude = ['.client-build'];
108
+ default_exclude = ['.jac'];
118
109
  # Get user config from [plugins.client.ts] in jac.toml
119
110
  user_ts_config = self.config_loader.get_ts_config();
120
111
  user_compiler_options = user_ts_config.get('compilerOptions', {});
@@ -130,28 +121,28 @@ impl ViteBundler.create_tsconfig(self: ViteBundler) -> Path {
130
121
  'include': merged_include,
131
122
  'exclude': merged_exclude
132
123
  };
133
- # Write the config to .client-build/.jac-client.configs/tsconfig.json (no root config file)
124
+ # Write the config to .jac/client/configs/tsconfig.json (no root config file)
134
125
  tsconfig_path.write_text(json.dumps(tsconfig_data, indent=2), encoding='utf-8');
135
126
  return tsconfig_path;
136
127
  }
137
128
 
138
129
  """
139
- Clean up root package.json and move package-lock.json to .jac-client.configs/.
130
+ Clean up root package.json and move bun.lockb to configs/.
140
131
 
141
- Remove root package.json and move package-lock.json to .jac-client.configs/.
132
+ Remove root package.json and move bun.lockb to configs/.
142
133
  """
143
134
  impl ViteBundler._cleanup_root_package_files(self: ViteBundler) -> None {
144
135
  root_package_json = self.project_dir / 'package.json';
145
- 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';
148
- configs_package_lock = configs_dir / 'package-lock.json';
149
- if root_package_lock.exists() {
136
+ root_bun_lockb = self.project_dir / 'bun.lockb';
137
+ build_dir = self._get_client_dir();
138
+ configs_dir = build_dir / 'configs';
139
+ configs_bun_lockb = configs_dir / 'bun.lockb';
140
+ if root_bun_lockb.exists() {
150
141
  configs_dir.mkdir(exist_ok=True);
151
- if configs_package_lock.exists() {
152
- configs_package_lock.unlink();
142
+ if configs_bun_lockb.exists() {
143
+ configs_bun_lockb.unlink();
153
144
  }
154
- shutil.move(str(root_package_lock), str(configs_package_lock));
145
+ shutil.move(str(root_bun_lockb), str(configs_bun_lockb));
155
146
  }
156
147
  if root_package_json.exists() {
157
148
  root_package_json.unlink();
@@ -159,12 +150,12 @@ impl ViteBundler._cleanup_root_package_files(self: ViteBundler) -> None {
159
150
  }
160
151
 
161
152
  """
162
- Ensure root package.json exists temporarily for npm commands.
153
+ Ensure root package.json exists temporarily for bun commands.
163
154
 
164
155
  Create root package.json temporarily if it doesn't exist.
165
156
  """
166
157
  impl ViteBundler._ensure_root_package_json(self: ViteBundler) -> None {
167
- generated_package_json = self.project_dir / '.client-build' / '.jac-client.configs' / 'package.json';
158
+ generated_package_json = self._get_client_dir() / 'configs' / 'package.json';
168
159
  root_package_json = self.project_dir / 'package.json';
169
160
  if not generated_package_json.exists() {
170
161
  self.create_package_json();
@@ -235,51 +226,28 @@ impl ViteBundler._get_plugin_var_name(self: ViteBundler, plugin_name: str) -> st
235
226
 
236
227
  """Create vite.config.js from config.json during bundling."""
237
228
  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';
229
+ build_dir = self._get_client_dir();
230
+ build_dir.mkdir(parents=True, exist_ok=True);
231
+ configs_dir = build_dir / 'configs';
241
232
  configs_dir.mkdir(exist_ok=True);
242
233
  vite_config_data = self.config_loader.get_vite_config();
243
234
  config_path = configs_dir / 'vite.config.js';
244
235
  # TypeScript is always enabled by default
245
- build_dir = self.project_dir / '.client-build';
246
236
  try {
247
- # Entry file path relative to .client-build/ (not project root)
237
+ # Entry file path relative to client build dir (not project root)
248
238
  entry_relative = entry_file.relative_to(build_dir).as_posix();
249
239
  } 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
- }
240
+ # Fallback: use absolute path
241
+ entry_relative = entry_file.as_posix();
263
242
  }
264
243
  try {
265
- # Output dir path relative to .client-build/ (not project root)
244
+ # Output dir path relative to client build dir (not project root)
266
245
  output_relative = self.output_dir.relative_to(build_dir).as_posix();
267
246
  } 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
- }
247
+ # Fallback: use absolute path
248
+ output_relative = self.output_dir.as_posix();
281
249
  }
282
- # Calculate compiled directory path for aliases (relative to .client-build/)
250
+ # Calculate compiled directory path for aliases (relative to client build dir)
283
251
  if entry_relative.endswith('/build/main.js') {
284
252
  compiled_utils_relative = entry_relative[:-13] + '/compiled/client_runtime.js';
285
253
  compiled_assets_relative = entry_relative[:-13] + '/compiled/assets';
@@ -331,11 +299,55 @@ impl ViteBundler.create_vite_config(self: ViteBundler, entry_file: Path) -> Path
331
299
  else '';
332
300
  config_content = f'''import {{ defineConfig }} from "vite";
333
301
  import path from "path";
302
+ import fs from "fs";
334
303
  import {{ fileURLToPath }} from "url";
335
304
  {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
305
+ // Config is in configs/ inside .jac/client/, so go up one level to .jac/client/, then up two more to project root
337
306
  const buildDir = path.resolve(__dirname, "..");
338
- const projectRoot = path.resolve(__dirname, "../..");
307
+ const projectRoot = path.resolve(__dirname, "../../..");
308
+
309
+ // Jac source mapper plugin - maps errors back to original .jac files
310
+ function jacSourceMapper() {{
311
+ const sourceMap = new Map(); // compiled path -> original jac path
312
+
313
+ return {{
314
+ name: 'jac-source-mapper',
315
+ enforce: 'pre',
316
+
317
+ // Extract source mapping from compiled files
318
+ transform(code, id) {{
319
+ if (id.includes('/compiled/') && id.endsWith('.js')) {{
320
+ const match = code.match(/^\\/\\* Source: (.+?) \\*\\//);
321
+ if (match) {{
322
+ sourceMap.set(id, match[1]);
323
+ }}
324
+ }}
325
+ return null;
326
+ }},
327
+
328
+ // Enhance error messages with original source info
329
+ buildEnd() {{
330
+ // Store source map for error reporting
331
+ this._jacSourceMap = sourceMap;
332
+ }},
333
+
334
+ // Handle resolve errors to show original source
335
+ resolveId(source, importer) {{
336
+ if (importer && sourceMap.has(importer)) {{
337
+ const originalSource = sourceMap.get(importer);
338
+ // Check for common issues like double slashes
339
+ if (source.includes('//') && !source.startsWith('http')) {{
340
+ this.error({{
341
+ message: `Cannot resolve "${{source}}" - path contains invalid double slash. Check your import in the original Jac file.`,
342
+ id: originalSource,
343
+ loc: {{ line: 1, column: 0 }}
344
+ }});
345
+ }}
346
+ }}
347
+ return null;
348
+ }}
349
+ }};
350
+ }}
339
351
 
340
352
  /**
341
353
  * Vite configuration generated from config.json (in project root)
@@ -343,14 +355,20 @@ const projectRoot = path.resolve(__dirname, "../..");
343
355
  */
344
356
 
345
357
  export default defineConfig({{
346
- plugins: [{(newline + plugins_str + newline + ' ') if plugins_str else ''}],
347
- root: buildDir, // base folder (.client-build/) so vite can find node_modules
358
+ plugins: [
359
+ jacSourceMapper(),{(newline + plugins_str + newline + ' ') if plugins_str else ''}],
360
+ root: buildDir, // base folder (.jac/client/) so vite can find node_modules
348
361
  build: {{
362
+ sourcemap: true, // Enable source maps for better error messages
349
363
  rollupOptions: {{
350
364
  input: path.resolve(buildDir, "{entry_relative}"), // your compiled entry file
351
365
  output: {{
352
366
  entryFileNames: "client.[hash].js", // name of the final js file
353
- assetFileNames: "[name].[ext]",
367
+ assetFileNames: (assetInfo) => assetInfo.name?.endsWith('.css') ? 'styles.css' : '[name].[ext]',
368
+ sourcemapPathTransform: (relativeSourcePath) => {{
369
+ // Transform source map paths to point to original location
370
+ return relativeSourcePath;
371
+ }},
354
372
  }},
355
373
  }},
356
374
  outDir: path.resolve(buildDir, "{output_relative}"), // final bundled output
@@ -360,7 +378,7 @@ export default defineConfig({{
360
378
  publicDir: false,
361
379
  {server_section} resolve: {{
362
380
  alias: {{
363
- "@jac-client/utils": path.resolve(buildDir, "{compiled_utils_relative}"),
381
+ "@jac/runtime": path.resolve(buildDir, "{compiled_utils_relative}"),
364
382
  "@jac-client/assets": path.resolve(buildDir, "{compiled_assets_relative}"),
365
383
  }},
366
384
  extensions: [{extensions_str}],
@@ -391,14 +409,8 @@ impl ViteBundler.read_bundle(self: ViteBundler) -> tuple[str, str] {
391
409
 
392
410
  """Find the generated Vite CSS file."""
393
411
  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;
412
+ css_file = self.output_dir / 'styles.css';
413
+ return css_file if css_file.exists() else None;
402
414
  }
403
415
 
404
416
  """Find the generated Vite bundle file."""
@@ -409,10 +421,18 @@ impl ViteBundler.find_bundle(self: ViteBundler) -> Optional[Path] {
409
421
  return None;
410
422
  }
411
423
 
412
- """Run Vite build with generated config in .client-build/.jac-client.configs/."""
424
+ """Run Vite build with generated config in .jac/client/configs/."""
413
425
  impl ViteBundler.build(self: ViteBundler, entry_file: Optional[Path] = None) -> None {
426
+ import sys;
427
+ import time;
428
+ import shutil;
429
+ import from jac_client.plugin.utils { ensure_bun_available }
430
+ # Ensure bun is available before proceeding
431
+ if not ensure_bun_available() {
432
+ raise ClientBundleError('Bun is required. Install manually: https://bun.sh') from None ;
433
+ }
414
434
  self.output_dir.mkdir(parents=True, exist_ok=True);
415
- generated_package_json = self.project_dir / '.client-build' / '.jac-client.configs' / 'package.json';
435
+ generated_package_json = self._get_client_dir() / 'configs' / 'package.json';
416
436
  if not generated_package_json.exists() {
417
437
  self.create_package_json();
418
438
  } else {
@@ -420,79 +440,89 @@ impl ViteBundler.build(self: ViteBundler, entry_file: Optional[Path] = None) ->
420
440
  self.create_tsconfig();
421
441
  }
422
442
  try {
423
- build_dir = self.project_dir / '.client-build';
443
+ build_dir = self._get_client_dir();
424
444
  node_modules = build_dir / 'node_modules';
425
445
  if not node_modules.exists() {
426
- # Temporarily copy package.json to .client-build/ for npm install
446
+ # Temporarily copy package.json to client build dir for bun install
427
447
  build_package_json = build_dir / 'package.json';
428
- configs_package_json = build_dir / '.jac-client.configs' / 'package.json';
448
+ configs_package_json = build_dir / 'configs' / 'package.json';
429
449
  if configs_package_json.exists() and not build_package_json.exists() {
430
450
  import shutil;
431
451
  shutil.copy2(configs_package_json, build_package_json);
432
452
  }
433
453
  try {
434
- # Install to .client-build/node_modules
435
- subprocess.run(
436
- ['npm', 'install'],
437
- cwd=build_dir,
438
- check=True,
439
- capture_output=True,
440
- text=True
454
+ # Install to .jac/client/node_modules with progress feedback
455
+ print("\n ⏳ Installing dependencies...\n", flush=True);
456
+ start_time = time.time();
457
+ result = subprocess.run(
458
+ ['bun', 'install'], cwd=build_dir, check=False, text=True
441
459
  );
442
- } except subprocess.CalledProcessError as e {
443
- raise e from ClientBundleError(
444
- f"Failed to install npm dependencies: {e.stderr}"
445
- ) ;
460
+ elapsed = time.time() - start_time;
461
+ if result.returncode != 0 {
462
+ print(
463
+ f"\n ✖ bun install failed after {elapsed:.1f}s",
464
+ file=sys.stderr
465
+ );
466
+ raise ClientBundleError("Failed to install dependencies") ;
467
+ }
468
+ print(f"\n ✔ Dependencies installed ({elapsed:.1f}s)", flush=True);
446
469
  } except FileNotFoundError {
447
- raise None from ClientBundleError(
448
- 'npm command not found. Ensure Node.js and npm are installed.'
449
- ) ;
470
+ # This shouldn't happen since we check for bun at the start
471
+ raise ClientBundleError(
472
+ 'Bun command not found. Install Bun: https://bun.sh'
473
+ ) from None ;
450
474
  }
451
475
  }
452
- build_dir = self.project_dir / '.client-build';
453
476
  if self.config_path {
454
477
  # 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
478
+ try {
458
479
  config_rel = self.config_path.relative_to(build_dir);
459
- command = ['npx', 'vite', 'build', '--config', str(config_rel)];
460
- } else {
461
- # Config is outside .client-build/, use absolute path
462
- command = ['npx', 'vite', 'build', '--config', str(self.config_path)];
480
+ command = ['bun', 'x', 'vite', 'build', '--config', str(config_rel)];
481
+ } except ValueError {
482
+ # Config is outside client build dir, use absolute path
483
+ command = [
484
+ 'bun',
485
+ 'x',
486
+ 'vite',
487
+ 'build',
488
+ '--config',
489
+ str(self.config_path)
490
+ ];
463
491
  }
464
492
  } elif entry_file {
465
493
  generated_config = self.create_vite_config(entry_file);
466
- # Config is in .client-build/.jac-client.configs/, make it relative to build_dir
494
+ # Config is in configs/, make it relative to build_dir
467
495
  config_rel = generated_config.relative_to(build_dir);
468
- command = ['npx', 'vite', 'build', '--config', str(config_rel)];
496
+ command = ['bun', 'x', 'vite', 'build', '--config', str(config_rel)];
469
497
  } else {
470
- command = ['npm', 'run', 'build'];
498
+ command = ['bun', 'run', 'build'];
471
499
  }
472
- # Run vite from .client-build/ directory so it can find node_modules
473
- result = subprocess.run(
474
- command, cwd=build_dir, check=False, capture_output=True, text=True
475
- );
500
+ # Run vite from client build directory so it can find node_modules
501
+ print("\n ⏳ Building client bundle...\n", flush=True);
502
+ start_time = time.time();
503
+ result = subprocess.run(command, cwd=build_dir, check=False, text=True);
504
+ elapsed = time.time() - start_time;
476
505
  if result.returncode != 0 {
477
- error_msg = result.stderr or result.stdout or 'Unknown error';
506
+ print(f"\n ✖ Vite build failed after {elapsed:.1f}s", file=sys.stderr);
478
507
  raise ClientBundleError(
479
- f"Vite build failed:\n{error_msg}\nCommand: {' '.join(command)}"
508
+ f"Vite build failed (see output above)\nCommand: {' '.join(command)}"
480
509
  ) from None ;
481
510
  }
511
+ print(f"\n ✔ Client bundle built ({elapsed:.1f}s)", flush=True);
482
512
  } finally {
483
- # Clean up temporary package.json in .client-build/
513
+ # Clean up temporary package.json in client build dir
484
514
  build_package_json = build_dir / 'package.json';
485
515
  if build_package_json.exists() {
486
516
  build_package_json.unlink();
487
517
  }
488
- # Move package-lock.json to .jac-client.configs/ if it exists
489
- build_package_lock = build_dir / 'package-lock.json';
490
- if build_package_lock.exists() {
491
- configs_package_lock = build_dir / '.jac-client.configs' / 'package-lock.json';
492
- if configs_package_lock.exists() {
493
- configs_package_lock.unlink();
518
+ # Move bun.lockb to configs/ if it exists
519
+ build_bun_lockb = build_dir / 'bun.lockb';
520
+ if build_bun_lockb.exists() {
521
+ configs_bun_lockb = build_dir / 'configs' / 'bun.lockb';
522
+ if configs_bun_lockb.exists() {
523
+ configs_bun_lockb.unlink();
494
524
  }
495
- build_package_lock.rename(configs_package_lock);
525
+ build_bun_lockb.rename(configs_bun_lockb);
496
526
  }
497
527
  }
498
528
  }
@@ -506,8 +536,218 @@ impl ViteBundler.init(
506
536
  config_path: Optional[Path] = None
507
537
  ) {
508
538
  self.project_dir = project_dir;
509
- self.output_dir = output_dir or (project_dir / '.client-build' / 'dist');
510
539
  self.minify = minify;
511
540
  self.config_path = config_path;
512
541
  self.config_loader = JacClientConfig(project_dir);
542
+ # Set output_dir after config_loader is initialized so _get_client_dir works
543
+ self.output_dir = output_dir or (self._get_client_dir() / 'dist');
544
+ }
545
+
546
+ """Create a dev-mode vite config with API proxy for HMR."""
547
+ impl ViteBundler.create_dev_vite_config(
548
+ self: ViteBundler, entry_file: Path, api_port: int = 8000
549
+ ) -> Path {
550
+ build_dir = self._get_client_dir();
551
+ build_dir.mkdir(parents=True, exist_ok=True);
552
+ configs_dir = build_dir / 'configs';
553
+ configs_dir.mkdir(exist_ok=True);
554
+ config_path = configs_dir / 'vite.dev.config.js';
555
+ # Get entry file relative path
556
+ try {
557
+ entry_relative = entry_file.relative_to(build_dir).as_posix();
558
+ } except ValueError {
559
+ entry_relative = entry_file.as_posix();
560
+ }
561
+ # Calculate paths for aliases
562
+ if entry_relative.endswith('/build/main.js') {
563
+ compiled_utils_relative = entry_relative[:-13] + '/compiled/client_runtime.js';
564
+ compiled_assets_relative = entry_relative[:-13] + '/compiled/assets';
565
+ } elif entry_relative.endswith('build/main.js') {
566
+ compiled_utils_relative = 'compiled/client_runtime.js';
567
+ compiled_assets_relative = 'compiled/assets';
568
+ } else {
569
+ compiled_utils_relative = 'compiled/client_runtime.js';
570
+ compiled_assets_relative = 'compiled/assets';
571
+ }
572
+ # Extensions for TypeScript
573
+ extensions = ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'];
574
+ extensions_str = ', '.join(f'"{ext}"' for ext in extensions);
575
+ # Generate dev config with proxy for API routes
576
+ config_content = f'''import {{ defineConfig }} from "vite";
577
+ import path from "path";
578
+ import {{ fileURLToPath }} from "url";
579
+ import react from "@vitejs/plugin-react";
580
+
581
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
582
+ const buildDir = path.resolve(__dirname, "..");
583
+ const projectRoot = path.resolve(__dirname, "../../..");
584
+
585
+ /**
586
+ * Vite DEV configuration for HMR mode
587
+ * Proxies API routes to Python server at localhost:{api_port}
588
+ */
589
+ export default defineConfig({{
590
+ plugins: [react()],
591
+ root: buildDir,
592
+ publicDir: false,
593
+ build: {{
594
+ sourcemap: true, // Enable source maps for better error messages
595
+ }},
596
+ server: {{
597
+ watch: {{
598
+ usePolling: true,
599
+ interval: 100,
600
+ }},
601
+ proxy: {{
602
+ "/walker": {{
603
+ target: "http://localhost:{api_port}",
604
+ changeOrigin: true,
605
+ }},
606
+ "/function": {{
607
+ target: "http://localhost:{api_port}",
608
+ changeOrigin: true,
609
+ }},
610
+ "/user": {{
611
+ target: "http://localhost:{api_port}",
612
+ changeOrigin: true,
613
+ }},
614
+ "/introspect": {{
615
+ target: "http://localhost:{api_port}",
616
+ changeOrigin: true,
617
+ }},
618
+ }},
619
+ }},
620
+ resolve: {{
621
+ alias: {{
622
+ "@jac/runtime": path.resolve(buildDir, "{compiled_utils_relative}"),
623
+ "@jac-client/assets": path.resolve(buildDir, "{compiled_assets_relative}"),
624
+ }},
625
+ extensions: [{extensions_str}],
626
+ }},
627
+ }});
628
+ ''';
629
+ config_path.write_text(config_content, encoding='utf-8');
630
+ return config_path;
631
+ }
632
+
633
+ """Create config files from jac.toml [plugins.client.configs].
634
+
635
+ Generates JavaScript config files (e.g., postcss.config.js, tailwind.config.js)
636
+ from TOML configuration. Each key in [plugins.client.configs] becomes a config file.
637
+
638
+ Example jac.toml:
639
+ [plugins.client.configs.postcss]
640
+ plugins = ["tailwindcss", "autoprefixer"]
641
+
642
+ [plugins.client.configs.tailwind]
643
+ content = ["./src/**/*.{js,jsx}"]
644
+
645
+ This generates:
646
+ - .jac/client/configs/postcss.config.js
647
+ - .jac/client/configs/tailwind.config.js
648
+ """
649
+ impl ViteBundler.create_config_files(self: ViteBundler) -> list[Path] {
650
+ configs = self.config_loader.get_configs();
651
+ if not configs {
652
+ return [];
653
+ }
654
+ build_dir = self._get_client_dir();
655
+ configs_dir = build_dir / 'configs';
656
+ configs_dir.mkdir(parents=True, exist_ok=True);
657
+ created_files: list[Path] = [];
658
+ for (config_name, config_data) in configs.items() {
659
+ config_path = configs_dir / f'{config_name}.config.js';
660
+
661
+ # Convert the TOML config to JavaScript module.exports
662
+ js_content = _toml_config_to_js(config_name, config_data);
663
+ config_path.write_text(js_content, encoding='utf-8');
664
+ created_files.append(config_path);
665
+ }
666
+ return created_files;
667
+ }
668
+
669
+ """Convert TOML config data to JavaScript config file content.
670
+
671
+ Generates a generic module.exports with the config data as JSON.
672
+ """
673
+ def _toml_config_to_js(config_name: str, config_data: dict) -> str {
674
+ return f"module.exports = {json.dumps(config_data, indent=2)};\n";
675
+ }
676
+
677
+ """Start Vite dev server as a subprocess."""
678
+ impl ViteBundler.start_dev_server(self: ViteBundler, port: int = 3000) -> Any {
679
+ import sys;
680
+ import time;
681
+ import from jac_client.plugin.utils { ensure_bun_available }
682
+ # Ensure bun is available before starting dev server
683
+ if not ensure_bun_available() {
684
+ raise ClientBundleError(
685
+ 'Bun is required for dev server. Install manually: https://bun.sh'
686
+ ) from None ;
687
+ }
688
+ build_dir = self._get_client_dir();
689
+ node_modules = build_dir / 'node_modules';
690
+ # Create/update index.html for dev server (load from compiled/ for HMR)
691
+ index_html = build_dir / 'index.html';
692
+ index_content = '''<!DOCTYPE html>
693
+ <html lang="en">
694
+ <head>
695
+ <meta charset="UTF-8" />
696
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
697
+ <title>Jac App (Dev)</title>
698
+ </head>
699
+ <body>
700
+ <div id="root"></div>
701
+ <script type="module" src="/compiled/_entry.js"></script>
702
+ </body>
703
+ </html>
704
+ ''';
705
+ index_html.write_text(index_content, encoding='utf-8');
706
+ # Ensure dependencies are installed
707
+ if not node_modules.exists() {
708
+ generated_package_json = build_dir / 'configs' / 'package.json';
709
+ if not generated_package_json.exists() {
710
+ self.create_package_json();
711
+ }
712
+ # Temporarily copy package.json for bun install
713
+ build_package_json = build_dir / 'package.json';
714
+ if not build_package_json.exists() {
715
+ shutil.copy2(generated_package_json, build_package_json);
716
+ }
717
+ try {
718
+ print("\n ⏳ Installing dependencies...\n", flush=True);
719
+ start_time = time.time();
720
+ result = subprocess.run(
721
+ ['bun', 'install'], cwd=build_dir, check=False, text=True
722
+ );
723
+ elapsed = time.time() - start_time;
724
+ if result.returncode != 0 {
725
+ print(
726
+ f"\n ✖ bun install failed after {elapsed:.1f}s", file=sys.stderr
727
+ );
728
+ raise ClientBundleError("Failed to install dependencies") ;
729
+ }
730
+ print(f"\n ✔ Dependencies installed ({elapsed:.1f}s)", flush=True);
731
+ } finally {
732
+ # Clean up temp package.json
733
+ if build_package_json.exists() {
734
+ build_package_json.unlink();
735
+ }
736
+ }
737
+ }
738
+ # Find the dev config
739
+ dev_config = build_dir / 'configs' / 'vite.dev.config.js';
740
+ if not dev_config.exists() {
741
+ raise ClientBundleError(
742
+ "Dev config not found. Call create_dev_vite_config first."
743
+ ) ;
744
+ }
745
+ config_rel = dev_config.relative_to(build_dir);
746
+ logger.debug(f"Starting Vite dev server on port {port}");
747
+ # Start Vite in dev mode (let output go to terminal for HMR visibility)
748
+ process = subprocess.Popen(
749
+ ['bun', 'x', 'vite', '--config', str(config_rel), '--port', str(port)],
750
+ cwd=build_dir
751
+ );
752
+ return process;
513
753
  }