jac-client 0.2.8__py3-none-any.whl → 0.2.9__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 (52) hide show
  1. jac_client/examples/all-in-one/{app.jac → main.jac} +5 -5
  2. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +8 -1
  3. jac_client/examples/all-in-one/pages/FeaturesTest.jac +16 -1
  4. jac_client/examples/all-in-one/pages/{FeaturesTest.cl.jac → features_test_ui.cl.jac} +11 -0
  5. jac_client/examples/all-in-one/pages/nestedDemo.jac +1 -1
  6. jac_client/examples/all-in-one/pages/notFound.jac +2 -7
  7. jac_client/plugin/cli.jac +162 -435
  8. jac_client/plugin/client.jac +25 -0
  9. jac_client/plugin/client_runtime.cl.jac +5 -1
  10. jac_client/plugin/impl/client.impl.jac +96 -55
  11. jac_client/plugin/impl/client_runtime.impl.jac +154 -0
  12. jac_client/plugin/plugin_config.jac +243 -15
  13. jac_client/plugin/src/config_loader.jac +1 -0
  14. jac_client/plugin/src/impl/compiler.impl.jac +1 -1
  15. jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
  16. jac_client/plugin/src/impl/vite_bundler.impl.jac +97 -16
  17. jac_client/plugin/src/vite_bundler.jac +6 -0
  18. jac_client/plugin/utils/__init__.jac +1 -0
  19. jac_client/plugin/utils/impl/node_installer.impl.jac +249 -0
  20. jac_client/plugin/utils/node_installer.jac +41 -0
  21. jac_client/templates/client.jacpack +72 -0
  22. jac_client/templates/fullstack.jacpack +61 -0
  23. jac_client/tests/conftest.py +48 -7
  24. jac_client/tests/test_cli.py +184 -70
  25. jac_client/tests/test_e2e.py +232 -0
  26. jac_client/tests/test_helpers.py +65 -0
  27. jac_client/tests/test_it.py +91 -135
  28. {jac_client-0.2.8.dist-info → jac_client-0.2.9.dist-info}/METADATA +4 -4
  29. {jac_client-0.2.8.dist-info → jac_client-0.2.9.dist-info}/RECORD +52 -45
  30. {jac_client-0.2.8.dist-info → jac_client-0.2.9.dist-info}/WHEEL +1 -1
  31. /jac_client/examples/all-in-one/pages/{BudgetPlanner.cl.jac → budget_planner_ui.cl.jac} +0 -0
  32. /jac_client/examples/asset-serving/css-with-image/{src/app.jac → main.jac} +0 -0
  33. /jac_client/examples/asset-serving/image-asset/{src/app.jac → main.jac} +0 -0
  34. /jac_client/examples/asset-serving/import-alias/{src/app.jac → main.jac} +0 -0
  35. /jac_client/examples/basic/{src/app.jac → main.jac} +0 -0
  36. /jac_client/examples/basic-auth/{src/app.jac → main.jac} +0 -0
  37. /jac_client/examples/basic-auth-with-router/{src/app.jac → main.jac} +0 -0
  38. /jac_client/examples/basic-full-stack/{src/app.jac → main.jac} +0 -0
  39. /jac_client/examples/css-styling/js-styling/{src/app.jac → main.jac} +0 -0
  40. /jac_client/examples/css-styling/material-ui/{src/app.jac → main.jac} +0 -0
  41. /jac_client/examples/css-styling/pure-css/{src/app.jac → main.jac} +0 -0
  42. /jac_client/examples/css-styling/sass-example/{src/app.jac → main.jac} +0 -0
  43. /jac_client/examples/css-styling/styled-components/{src/app.jac → main.jac} +0 -0
  44. /jac_client/examples/css-styling/tailwind-example/{src/app.jac → main.jac} +0 -0
  45. /jac_client/examples/full-stack-with-auth/{src/app.jac → main.jac} +0 -0
  46. /jac_client/examples/little-x/{src/app.jac → main.jac} +0 -0
  47. /jac_client/examples/nested-folders/nested-advance/{src/app.jac → main.jac} +0 -0
  48. /jac_client/examples/nested-folders/nested-basic/{src/app.jac → main.jac} +0 -0
  49. /jac_client/examples/ts-support/{src/app.jac → main.jac} +0 -0
  50. /jac_client/examples/with-router/{src/app.jac → main.jac} +0 -0
  51. {jac_client-0.2.8.dist-info → jac_client-0.2.9.dist-info}/entry_points.txt +0 -0
  52. {jac_client-0.2.8.dist-info → jac_client-0.2.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,249 @@
1
+ """Implementation of Node.js installer methods."""
2
+
3
+ """Check if Node.js is installed and accessible."""
4
+ impl NodeInstaller.is_node_installed -> bool {
5
+ try {
6
+ result = subprocess.run(
7
+ ['node', '--version'], capture_output=True, text=True, timeout=5
8
+ );
9
+ return result.returncode == 0;
10
+ } except (FileNotFoundError, subprocess.TimeoutExpired) {
11
+ return False;
12
+ }
13
+ }
14
+
15
+ """Check if npm is installed and accessible."""
16
+ impl NodeInstaller.is_npm_installed -> bool {
17
+ try {
18
+ result = subprocess.run(
19
+ ['npm', '--version'], capture_output=True, text=True, timeout=5
20
+ );
21
+ return result.returncode == 0;
22
+ } except (FileNotFoundError, subprocess.TimeoutExpired) {
23
+ return False;
24
+ }
25
+ }
26
+
27
+ """Check if NVM is installed."""
28
+ impl NodeInstaller.is_nvm_installed -> bool {
29
+ nvm_dir = Path.home() / '.nvm';
30
+ return nvm_dir.exists() and (nvm_dir / 'nvm.sh').exists();
31
+ }
32
+
33
+ """Get the currently installed Node.js version."""
34
+ impl NodeInstaller.get_node_version -> (str | None) {
35
+ try {
36
+ result = subprocess.run(
37
+ ['node', '--version'], capture_output=True, text=True, timeout=5
38
+ );
39
+ if result.returncode == 0 {
40
+ return result.stdout.strip();
41
+ }
42
+ } except (FileNotFoundError, subprocess.TimeoutExpired) { }
43
+ return None;
44
+ }
45
+
46
+ """Install NVM (Node Version Manager)."""
47
+ impl NodeInstaller.install_nvm -> tuple[bool, str] {
48
+ system = platform.system();
49
+ if system == 'Windows' {
50
+ return (
51
+ False,
52
+ 'Windows detected. Please install Node.js manually:\n' + ' 1. Download from: https://nodejs.org/\n' + ' 2. Or use nvm-windows: https://github.com/coreybutler/nvm-windows\n' + ' After installation, run "jac add --cl" again.'
53
+ );
54
+ }
55
+ print('Installing NVM (Node Version Manager)...');
56
+ try {
57
+ # Download and run NVM installation script
58
+ install_script = subprocess.run(
59
+ ['curl', '-o-', NodeInstaller.NVM_INSTALL_URL],
60
+ capture_output=True,
61
+ text=True,
62
+ timeout=30
63
+ );
64
+
65
+ if install_script.returncode != 0 {
66
+ return (
67
+ False,
68
+ f'Failed to download NVM installer: {install_script.stderr}'
69
+ );
70
+ }
71
+
72
+ # Execute the installation script
73
+ result = subprocess.run(
74
+ ['bash', '-c', install_script.stdout],
75
+ capture_output=True,
76
+ text=True,
77
+ timeout=60
78
+ );
79
+
80
+ if result.returncode != 0 {
81
+ return (False, f'NVM installation failed: {result.stderr}');
82
+ }
83
+
84
+ print('NVM installed successfully!');
85
+ return (True, 'NVM installed successfully');
86
+ } except subprocess.TimeoutExpired {
87
+ return (
88
+ False,
89
+ 'NVM installation timed out. Please check your internet connection.'
90
+ );
91
+ } except FileNotFoundError as e {
92
+ if 'curl' in str(e) {
93
+ return (
94
+ False,
95
+ 'curl command not found. Please install curl first:\n' + ' Ubuntu/Debian: sudo apt-get install curl\n' + ' macOS: curl is pre-installed\n' + ' Fedora/RHEL: sudo dnf install curl'
96
+ );
97
+ }
98
+ return (False, f'Required command not found: {e}');
99
+ } except Exception as e {
100
+ return (False, f'Unexpected error during NVM installation: {e}');
101
+ }
102
+ }
103
+
104
+ """Install Node.js using NVM."""
105
+ impl NodeInstaller.install_node_via_nvm(version: str = "20") -> tuple[bool, str] {
106
+ print(f'Installing Node.js v{version} via NVM...');
107
+ nvm_dir = Path.home() / '.nvm';
108
+ nvm_script = nvm_dir / 'nvm.sh';
109
+ if not nvm_script.exists() {
110
+ return (False, 'NVM is not installed or nvm.sh not found');
111
+ }
112
+ try {
113
+ # Source NVM and install Node.js
114
+ command = (
115
+ 'export NVM_DIR="$HOME/.nvm"\n' + '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"\n' + f'nvm install {version}\n' + f'nvm use {version}\n' + f'nvm alias default {version}\n'
116
+ );
117
+
118
+ result = subprocess.run(
119
+ ['bash', '-c', command], capture_output=True, text=True, timeout=300
120
+ );
121
+
122
+ if result.returncode != 0 {
123
+ return (False, f'Node.js installation failed: {result.stderr}');
124
+ }
125
+
126
+ print(f'Node.js v{version} installed successfully!');
127
+ return (True, f'Node.js v{version} installed successfully');
128
+ } except subprocess.TimeoutExpired {
129
+ return (False, 'Node.js installation timed out. Please try again.');
130
+ } except Exception as e {
131
+ return (False, f'Unexpected error during Node.js installation: {e}');
132
+ }
133
+ }
134
+
135
+ """Ensure Node.js is installed, installing automatically if needed.
136
+
137
+ Returns tuple of (success: bool, message: str, was_just_installed: bool)
138
+ """
139
+ impl NodeInstaller.ensure_node_installed(
140
+ interactive: bool = True
141
+ ) -> tuple[bool, str, bool] {
142
+ # Common fallback message for manual installation
143
+ manual_install_msg = (
144
+ 'Please install Node.js manually:\n' + ' • Download from: https://nodejs.org/\n' + ' • Or install NVM: https://github.com/nvm-sh/nvm\n' + ' After installation, run "jac add --cl" again.'
145
+ );
146
+ # Check if Node.js is already available
147
+ if NodeInstaller.is_node_installed() and NodeInstaller.is_npm_installed() {
148
+ version = NodeInstaller.get_node_version();
149
+ return (True, f'Node.js {version} is already installed', False);
150
+ }
151
+ print('\nNode.js is not installed or not found in PATH.');
152
+ # Check for interactive mode
153
+ if interactive {
154
+ print(
155
+ '\nJac client requires Node.js to build and run client-side applications.'
156
+ );
157
+ print(
158
+ f'Would you like to automatically install Node.js v{NodeInstaller.DEFAULT_NODE_VERSION} using NVM?'
159
+ );
160
+ response = input('Install Node.js? [Y/n]: ').strip().lower();
161
+ if response and response not in ('y', 'yes') {
162
+ return (
163
+ False,
164
+ f'Node.js installation cancelled. {manual_install_msg}',
165
+ False
166
+ );
167
+ }
168
+ }
169
+ # Wrap entire installation process in error handling
170
+ try {
171
+ # Check if NVM is installed
172
+ if not NodeInstaller.is_nvm_installed() {
173
+ success_nvm = NodeInstaller.install_nvm();
174
+ if not success_nvm[0] {
175
+ return (
176
+ False,
177
+ f'Unable to automatically install Node.js.\n\nError: {success_nvm[
178
+ 1
179
+ ]}\n\n{manual_install_msg}',
180
+ False
181
+ );
182
+ }
183
+ }
184
+
185
+ # Install Node.js via NVM
186
+ success_node = NodeInstaller.install_node_via_nvm();
187
+ if not success_node[0] {
188
+ return (
189
+ False,
190
+ f'Unable to automatically install Node.js.\n\nError: {success_node[1]}\n\n{manual_install_msg}',
191
+ False
192
+ );
193
+ }
194
+
195
+ # Node.js is now available via NVM sourcing in subprocesses
196
+ print('\n' + '=' * 70);
197
+ print('Node.js has been installed successfully!');
198
+ print('Continuing with package installation...');
199
+ print('=' * 70 + '\n');
200
+
201
+ return (
202
+ True,
203
+ f'Node.js {NodeInstaller.DEFAULT_NODE_VERSION} installed successfully',
204
+ True
205
+ );
206
+ } except Exception as e {
207
+ # Catch any unexpected errors during installation
208
+ return (
209
+ False,
210
+ f'Unable to automatically install Node.js.\n\nUnexpected error: {str(e)}\n\n{manual_install_msg}',
211
+ False
212
+ );
213
+ }
214
+ }
215
+
216
+ """Run npm command with NVM environment properly sourced.
217
+
218
+ This method automatically sources NVM in a subprocess, so npm commands work
219
+ immediately after NVM installation without requiring the user to reload their shell.
220
+ """
221
+ impl NodeInstaller.run_npm_with_nvm(
222
+ args: list, cwd: Path, timeout: int = 300
223
+ ) -> object {
224
+ # First, try running npm directly (in case it's already in PATH)
225
+ try {
226
+ return subprocess.run(
227
+ ['npm'] + args,
228
+ cwd=cwd,
229
+ capture_output=True,
230
+ text=True,
231
+ timeout=timeout,
232
+ check=True
233
+ );
234
+ } except FileNotFoundError { }
235
+ # If npm is not in PATH, source NVM and run npm in a subprocess
236
+ # This allows npm to work immediately after installation without shell reload
237
+ args_str = ' '.join(args);
238
+ command = (
239
+ 'export NVM_DIR="$HOME/.nvm"\n' + '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"\n' + f'npm {args_str}\n'
240
+ );
241
+ return subprocess.run(
242
+ ['bash', '-c', command],
243
+ cwd=cwd,
244
+ capture_output=True,
245
+ text=True,
246
+ timeout=timeout,
247
+ check=True
248
+ );
249
+ }
@@ -0,0 +1,41 @@
1
+ """Automatic Node.js installation utility using NVM."""
2
+
3
+ import os;
4
+ import platform;
5
+ import subprocess;
6
+ import from pathlib { Path }
7
+
8
+ """Handles Node.js installation and detection."""
9
+ obj NodeInstaller {
10
+ static has DEFAULT_NODE_VERSION: str = "20",
11
+ NVM_INSTALL_URL: str = "https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh";
12
+
13
+ """Check if Node.js is installed and accessible."""
14
+ static def is_node_installed -> bool;
15
+
16
+ """Check if npm is installed and accessible."""
17
+ static def is_npm_installed -> bool;
18
+
19
+ """Check if NVM is installed."""
20
+ static def is_nvm_installed -> bool;
21
+
22
+ """Get the currently installed Node.js version."""
23
+ static def get_node_version -> (str | None);
24
+
25
+ """Install NVM (Node Version Manager)."""
26
+ static def install_nvm -> tuple[bool, str];
27
+
28
+ """Install Node.js using NVM."""
29
+ static def install_node_via_nvm(version: str = "20") -> tuple[bool, str];
30
+
31
+ """Ensure Node.js is installed, installing automatically if needed.
32
+
33
+ Returns tuple of (success: bool, message: str, was_just_installed: bool)
34
+ """
35
+ static def ensure_node_installed(
36
+ interactive: bool = True
37
+ ) -> tuple[bool, str, bool];
38
+
39
+ """Run npm command with NVM environment properly sourced."""
40
+ static def run_npm_with_nvm(args: list, cwd: Path, timeout: int = 300) -> object;
41
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "client",
3
+ "description": "Jac project with frontend client setup",
4
+ "config": {
5
+ "project": {
6
+ "name": "{{name}}",
7
+ "version": "1.0.0",
8
+ "description": "Jac client application: {{name}}",
9
+ "entry-point": "main.jac"
10
+ },
11
+ "dependencies": {},
12
+ "dependencies.npm": {
13
+ "jac-client-node": "1.0.4"
14
+ },
15
+ "dependencies.npm.dev": {
16
+ "@jac-client/dev-deps": "1.0.0"
17
+ },
18
+ "dev-dependencies": {
19
+ "watchdog": ">=3.0.0"
20
+ },
21
+ "serve": {
22
+ "base_route_app": "app"
23
+ },
24
+ "plugins.client": {}
25
+ },
26
+ "files": {
27
+ "main.jac": "\"\"\"Main entry point for {{name}}.\"\"\"\n\n# Client-side imports (useState is auto-injected when using `has` variables)\ncl import from react { useEffect }\ncl import from .components.Button { Button }\n\n# Client-side component\ncl {\n def:pub app() -> any {\n has count: int = 0;\n\n useEffect(lambda -> None {\n console.log(\"Count updated:\", count);\n }, [count]);\n\n return <div style={{padding: \"2rem\", fontFamily: \"Arial, sans-serif\"}}>\n <h1>Hello, World!</h1>\n <p>Count: {count}</p>\n <div style={{display: \"flex\", gap: \"1rem\", marginTop: \"1rem\"}}>\n <Button\n label=\"Increment\"\n onClick={lambda -> None { count = count + 1; }}\n variant=\"primary\"\n />\n <Button\n label=\"Reset\"\n onClick={lambda -> None { count = 0; }}\n variant=\"secondary\"\n />\n </div>\n </div>;\n }\n}\n",
28
+ "components/Button.cl.jac": "\"\"\"Button component for the Jac client application.\"\"\"\n\ndef:pub Button(label: str, onClick: any, variant: str = \"primary\", disabled: bool = False) -> any {\n base_styles = {\n \"padding\": \"0.75rem 1.5rem\",\n \"fontSize\": \"1rem\",\n \"fontWeight\": \"600\",\n \"borderRadius\": \"0.5rem\",\n \"border\": \"none\",\n \"cursor\": \"not-allowed\" if disabled else \"pointer\",\n \"transition\": \"all 0.2s ease\"\n };\n\n variant_styles = {\n \"primary\": {\n \"backgroundColor\": \"#9ca3af\" if disabled else \"#3b82f6\",\n \"color\": \"#ffffff\"\n },\n \"secondary\": {\n \"backgroundColor\": \"#e5e7eb\" if disabled else \"#6b7280\",\n \"color\": \"#ffffff\"\n }\n };\n\n return <button\n style={{**base_styles, **variant_styles[variant]}}\n onClick={onClick}\n disabled={disabled}\n >\n {label}\n </button>;\n}\n",
29
+ "README.md": "# {{name}}\n\nA Jac client-side application with React support.\n\n## Project Structure\n\n```\n{{name}}/\n\u251c\u2500\u2500 jac.toml # Project configuration\n\u251c\u2500\u2500 main.jac # Main application entry\n\u251c\u2500\u2500 components/ # Reusable components\n\u2502 \u2514\u2500\u2500 Button.cl.jac # Example Jac component\n\u251c\u2500\u2500 assets/ # Static assets (images, fonts, etc.)\n\u2514\u2500\u2500 build/ # Build output (generated)\n```\n\n## Getting Started\n\nStart the development server:\n\n```bash\njac start main.jac\n```\n\n## Components\n\nCreate Jac components in `components/` as `.cl.jac` files and import them:\n\n```jac\ncl import from .components.Button { Button }\n```\n\n## Adding Dependencies\n\nAdd npm packages with the --cl flag:\n\n```bash\njac add --cl react-router-dom\n```\n"
30
+ },
31
+ "directories": [
32
+ "components",
33
+ "assets",
34
+ ".jac/client"
35
+ ],
36
+ "gitignore_entries": [
37
+ "# Ignore all build artifacts in .jac directory",
38
+ "*"
39
+ ],
40
+ "root_gitignore_entries": [
41
+ "# Jac project",
42
+ "packages/",
43
+ "*.jbc",
44
+ "*.jir",
45
+ "",
46
+ "# Python",
47
+ "__pycache__/",
48
+ "*.py[cod]",
49
+ ".venv/",
50
+ "venv/",
51
+ "",
52
+ "# IDE",
53
+ ".idea/",
54
+ ".vscode/",
55
+ "*.swp",
56
+ "",
57
+ "# Node.js",
58
+ "node_modules/",
59
+ "",
60
+ "# Build artifacts",
61
+ ".jac/",
62
+ "*.session",
63
+ "*.session.*"
64
+ ],
65
+ "jaclang": "0.9.8",
66
+ "plugins": [
67
+ {
68
+ "name": "jac-client",
69
+ "version": "0.2.8"
70
+ }
71
+ ]
72
+ }
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "fullstack",
3
+ "description": "Full-stack Jac app with authenticated todo list example",
4
+ "config": {
5
+ "project": {
6
+ "name": "{{name}}",
7
+ "version": "0.1.0",
8
+ "description": "A full-stack Jac application",
9
+ "entry-point": "main.jac"
10
+ },
11
+ "dependencies": {},
12
+ "dependencies.npm": {
13
+ "jac-client-node": "1.0.4"
14
+ },
15
+ "dependencies.npm.dev": {
16
+ "@jac-client/dev-deps": "1.0.0"
17
+ },
18
+ "dev-dependencies": {
19
+ "watchdog": ">=3.0.0"
20
+ },
21
+ "serve": {
22
+ "base_route_app": "app"
23
+ },
24
+ "plugins.client": {}
25
+ },
26
+ "files": {
27
+ "main.jac": "\"\"\"{{name}} - Entry Point.\n\nCombines server-side endpoints with client-side UI.\n\"\"\"\n\n# Import server-side data models and walkers\nsv {\n import from endpoints { Todo, AddTodo, ListTodos, ToggleTodo, DeleteTodo }\n}\n\n# Client-side UI\ncl {\n import from .frontend { app as ClientApp }\n\n def:pub app -> any {\n return\n <ClientApp />;\n }\n}\n",
28
+ "endpoints.sv.jac": "\"\"\"Todo App - Server-Side Data Layer.\n\nEach user gets their own todo list automatically!\nWhen authenticated, `root spawn` uses the user's personal root node.\n\"\"\"\n\n# ========================================\n# Data Models\n# ========================================\nnode Todo {\n has id: str,\n title: str,\n completed: bool = False;\n}\n\nglob todo_counter: int = 0;\n\n# ========================================\n# Walkers (API Operations)\n# ========================================\nwalker:priv AddTodo {\n \"\"\"Add a new todo item to the user's list.\"\"\"\n has title: str;\n\n can create with `root entry {\n global todo_counter;\n todo_counter += 1;\n new_id = \"todo_\" + str(todo_counter);\n new_todo = here ++> Todo(id=new_id, title=self.title, completed=False);\n if new_todo {\n report new_todo[0] ;\n }\n }\n}\n\nwalker:priv ListTodos {\n \"\"\"List all todo items for the current user.\"\"\"\n has todos: list = [];\n\n can collect with `root entry {\n visit [-->];\n }\n\n can gather with Todo entry {\n self.todos.append(\n {\"id\": here.id, \"title\": here.title, \"completed\": here.completed}\n );\n }\n\n can report_all with `root exit {\n report self.todos ;\n }\n}\n\nwalker:priv ToggleTodo {\n \"\"\"Toggle a todo's completed status.\"\"\"\n has todo_id: str;\n\n can search with `root entry {\n visit [-->];\n }\n\n can toggle with Todo entry {\n if here.id == self.todo_id {\n here.completed = not here.completed;\n report {\"id\": here.id, \"completed\": here.completed} ;\n }\n }\n}\n\nwalker:priv DeleteTodo {\n \"\"\"Delete a todo item.\"\"\"\n has todo_id: str;\n\n can search with `root entry {\n visit [-->];\n }\n\n can delete with Todo entry {\n if here.id == self.todo_id {\n del here ;\n report {\"deleted\": self.todo_id} ;\n }\n }\n}\n",
29
+ "frontend.cl.jac": "\"\"\"Todo App - Client-Side UI.\"\"\"\n\nimport from react { useEffect }\nimport from \"@jac-client/utils\" { jacSignup, jacLogin, jacLogout, jacIsLoggedIn }\n\nimport from .components.TodoItem { TodoItem }\nimport from .components.AuthForm { AuthForm }\n\nsv import from endpoints { AddTodo, ListTodos, ToggleTodo, DeleteTodo }\n\ndef:pub app -> any {\n has isLoggedIn: bool = False,\n checkingAuth: bool = True,\n isSignup: bool = False,\n username: str = \"\",\n password: str = \"\",\n error: str = \"\",\n loading: bool = False,\n todos: list = [],\n newTodoText: str = \"\",\n todosLoading: bool = True;\n\n # Check login status on mount\n useEffect(lambda -> None { isLoggedIn = jacIsLoggedIn();checkingAuth = False;}, []);\n\n # Load todos when logged in\n useEffect(\n lambda -> None { if isLoggedIn {\n fetchTodos();\n }},\n [isLoggedIn]\n );\n\n # Fetch todos from server\n async def fetchTodos -> None {\n todosLoading = True;\n result = root spawn ListTodos();\n todos = result.reports[0] if result.reports else [];\n todosLoading = False;\n }\n\n # Add a new todo\n async def addTodo -> None {\n if not newTodoText.trim() {\n return;\n }\n response = root spawn AddTodo(title=newTodoText);\n newTodo = response.reports[0];\n todos = todos.concat(\n [\n {\n \"id\": newTodo.id,\n \"title\": newTodo.title,\n \"completed\": newTodo.completed\n }\n ]\n );\n newTodoText = \"\";\n }\n\n # Toggle todo completion\n async def toggleTodo(todoId: str) -> None {\n root spawn ToggleTodo(todo_id=todoId);\n todos = todos.map(\n lambda t: any -> any { if t.id == todoId {\n return {\"id\": t.id, \"title\": t.title, \"completed\": not t.completed};\n }return t; }\n );\n }\n\n # Delete a todo\n async def deleteTodo(todoId: str) -> None {\n root spawn DeleteTodo(todo_id=todoId);\n todos = todos.filter(lambda t: any -> bool { return t.id != todoId; });\n }\n\n # Handle login\n async def handleLogin -> None {\n error = \"\";\n if not username.trim() or not password {\n error = \"Please fill in all fields\";\n return;\n }\n loading = True;\n success = await jacLogin(username, password);\n loading = False;\n if success {\n isLoggedIn = True;\n username = \"\";\n password = \"\";\n } else {\n error = \"Invalid username or password\";\n }\n }\n\n # Handle signup\n async def handleSignup -> None {\n error = \"\";\n if not username.trim() or not password {\n error = \"Please fill in all fields\";\n return;\n }\n if password.length < 4 {\n error = \"Password must be at least 4 characters\";\n return;\n }\n loading = True;\n result = await jacSignup(username, password);\n loading = False;\n if result[\"success\"] {\n isLoggedIn = True;\n username = \"\";\n password = \"\";\n } else {\n error = result[\"error\"] if result[\"error\"] else \"Signup failed\";\n }\n }\n\n # Handle logout\n def handleLogout -> None {\n jacLogout();\n isLoggedIn = False;\n todos = [];\n username = \"\";\n password = \"\";\n error = \"\";\n }\n\n # Handle form submit\n async def handleSubmit(e: any) -> None {\n e.preventDefault();\n if isSignup {\n await handleSignup();\n } else {\n await handleLogin();\n }\n }\n\n # Handle enter key for todo input\n def handleTodoKeyPress(e: any) -> None {\n if e.key == \"Enter\" {\n addTodo();\n }\n }\n\n # Loading screen\n if checkingAuth {\n return\n <div\n style={{\n \"display\": \"flex\",\n \"justifyContent\": \"center\",\n \"alignItems\": \"center\",\n \"height\": \"100vh\",\n \"fontFamily\": \"system-ui, -apple-system, sans-serif\",\n \"color\": \"#666\"\n }}\n >\n Loading...\n </div>;\n }\n\n # Logged in - Show Todo List\n if isLoggedIn {\n return\n <div\n style={{\n \"maxWidth\": \"600px\",\n \"margin\": \"2rem auto\",\n \"padding\": \"2rem\",\n \"fontFamily\": \"system-ui, -apple-system, sans-serif\"\n }}\n >\n <div\n style={{\n \"display\": \"flex\",\n \"justifyContent\": \"space-between\",\n \"alignItems\": \"center\",\n \"marginBottom\": \"1.5rem\"\n }}\n >\n <h1 style={{\"margin\": \"0\", \"color\": \"#1a1a2e\"}}>\n My Todos\n </h1>\n <button\n onClick={handleLogout}\n style={{\n \"padding\": \"0.5rem 1rem\",\n \"fontSize\": \"0.9rem\",\n \"backgroundColor\": \"#f8f9fa\",\n \"color\": \"#666\",\n \"border\": \"1px solid #ddd\",\n \"borderRadius\": \"6px\",\n \"cursor\": \"pointer\"\n }}\n >\n Log Out\n </button>\n </div>\n <div\n style={{\n \"display\": \"flex\",\n \"gap\": \"0.5rem\",\n \"marginBottom\": \"1.5rem\"\n }}\n >\n <input\n type=\"text\"\n value={newTodoText}\n onChange={lambda e: any -> None { newTodoText = e.target.value;}}\n onKeyPress={handleTodoKeyPress}\n placeholder=\"What needs to be done?\"\n style={{\n \"flex\": \"1\",\n \"padding\": \"0.75rem 1rem\",\n \"fontSize\": \"1rem\",\n \"border\": \"2px solid #e0e0e0\",\n \"borderRadius\": \"8px\",\n \"outline\": \"none\"\n }}\n />\n <button\n onClick={lambda -> None { addTodo();}}\n style={{\n \"padding\": \"0.75rem 1.5rem\",\n \"fontSize\": \"1rem\",\n \"backgroundColor\": \"#4CAF50\",\n \"color\": \"white\",\n \"border\": \"none\",\n \"borderRadius\": \"8px\",\n \"cursor\": \"pointer\"\n }}\n >\n Add\n </button>\n </div>\n {(\n <div style={{\"color\": \"#666\", \"textAlign\": \"center\"}}>\n Loading todos...\n </div>\n )\n if todosLoading\n else None}\n {(\n <div>\n {(\n <p\n style={{\n \"color\": \"#888\",\n \"textAlign\": \"center\",\n \"padding\": \"2rem\"\n }}\n >\n No todos yet. Add one above!\n </p>\n )\n if todos.length == 0\n else None}{todos.map(\n lambda todo: any -> any { return\n <TodoItem\n key={todo.id}\n todo={todo}\n onToggle={toggleTodo}\n onDelete={deleteTodo}\n />; }\n )}\n </div>\n )\n if not todosLoading\n else None}\n <div\n style={{\n \"marginTop\": \"2rem\",\n \"paddingTop\": \"1rem\",\n \"borderTop\": \"1px solid #eee\",\n \"color\": \"#888\",\n \"fontSize\": \"0.9rem\"\n }}\n >\n {todos.filter(lambda t: any -> bool { return not t.completed; }).length} items left\n </div>\n </div>;\n }\n\n # Not logged in - Show Auth Form\n return\n <AuthForm\n isSignup={isSignup}\n username={username}\n password={password}\n error={error}\n loading={loading}\n onUsernameChange={lambda e: any -> None { username = e.target.value;}}\n onPasswordChange={lambda e: any -> None { password = e.target.value;}}\n onSubmit={handleSubmit}\n onToggleMode={lambda -> None { isSignup = not isSignup;error = \"\";}}\n />;\n}\n",
30
+ "components/AuthForm.cl.jac": "\"\"\"Authentication form component for login/signup.\"\"\"\n\ndef:pub AuthForm(\n isSignup: bool,\n username: str,\n password: str,\n error: str,\n loading: bool,\n onUsernameChange: any,\n onPasswordChange: any,\n onSubmit: any,\n onToggleMode: any\n) -> any {\n return\n <div\n style={{\n \"maxWidth\": \"400px\",\n \"margin\": \"4rem auto\",\n \"padding\": \"2rem\",\n \"fontFamily\": \"system-ui, -apple-system, sans-serif\",\n \"backgroundColor\": \"#fff\",\n \"borderRadius\": \"12px\",\n \"boxShadow\": \"0 4px 6px rgba(0,0,0,0.1)\"\n }}\n >\n <h1\n style={{\n \"marginBottom\": \"0.5rem\",\n \"color\": \"#1a1a2e\",\n \"textAlign\": \"center\"\n }}\n >\n Jac Todo App\n </h1>\n <p\n style={{\n \"marginBottom\": \"1.5rem\",\n \"color\": \"#666\",\n \"textAlign\": \"center\"\n }}\n >\n {(\"Create an account\" if isSignup else \"Sign in to your account\")}\n </p>\n {(\n <div\n style={{\n \"padding\": \"0.75rem\",\n \"marginBottom\": \"1rem\",\n \"backgroundColor\": \"#fee\",\n \"color\": \"#c00\",\n \"borderRadius\": \"6px\",\n \"fontSize\": \"0.9rem\"\n }}\n >\n {error}\n </div>\n )\n if error\n else None}\n <form onSubmit={onSubmit}>\n <input\n type=\"text\"\n value={username}\n onChange={onUsernameChange}\n placeholder=\"Username\"\n style={{\n \"width\": \"100%\",\n \"padding\": \"0.75rem 1rem\",\n \"marginBottom\": \"0.75rem\",\n \"fontSize\": \"1rem\",\n \"border\": \"2px solid #e0e0e0\",\n \"borderRadius\": \"8px\",\n \"outline\": \"none\",\n \"boxSizing\": \"border-box\"\n }}\n />\n <input\n type=\"password\"\n value={password}\n onChange={onPasswordChange}\n placeholder=\"Password\"\n style={{\n \"width\": \"100%\",\n \"padding\": \"0.75rem 1rem\",\n \"marginBottom\": \"1rem\",\n \"fontSize\": \"1rem\",\n \"border\": \"2px solid #e0e0e0\",\n \"borderRadius\": \"8px\",\n \"outline\": \"none\",\n \"boxSizing\": \"border-box\"\n }}\n />\n <button\n type=\"submit\"\n disabled={loading}\n style={{\n \"width\": \"100%\",\n \"padding\": \"0.75rem\",\n \"fontSize\": \"1rem\",\n \"backgroundColor\": (\"#999\" if loading else \"#4CAF50\"),\n \"color\": \"white\",\n \"border\": \"none\",\n \"borderRadius\": \"8px\",\n \"cursor\": (\"not-allowed\" if loading else \"pointer\"),\n \"marginBottom\": \"1rem\"\n }}\n >\n {(\n (\n \"Processing...\"\n if loading\n else (\"Sign Up\" if isSignup else \"Log In\")\n )\n )}\n </button>\n </form>\n <p style={{\"textAlign\": \"center\", \"color\": \"#666\", \"fontSize\": \"0.9rem\"}}>\n {(\n \"Already have an account? \"\n if isSignup\n else \"Don't have an account? \"\n )}\n <span\n onClick={onToggleMode}\n style={{\n \"color\": \"#4CAF50\",\n \"cursor\": \"pointer\",\n \"fontWeight\": \"bold\"\n }}\n >\n {(\"Log In\" if isSignup else \"Sign Up\")}\n </span>\n </p>\n </div>;\n}\n",
31
+ "components/TodoItem.cl.jac": "\"\"\"Todo item component for displaying a single todo.\"\"\"\n\ndef:pub TodoItem(todo: dict, onToggle: any, onDelete: any) -> any {\n return\n <div\n key={todo.id}\n style={{\n \"display\": \"flex\",\n \"alignItems\": \"center\",\n \"gap\": \"0.75rem\",\n \"padding\": \"1rem\",\n \"marginBottom\": \"0.5rem\",\n \"backgroundColor\": \"#f8f9fa\",\n \"borderRadius\": \"8px\",\n \"border\": \"1px solid #e9ecef\"\n }}\n >\n <input\n type=\"checkbox\"\n checked={todo.completed}\n onChange={lambda -> None { onToggle(todo.id);}}\n style={{\"width\": \"20px\", \"height\": \"20px\", \"cursor\": \"pointer\"}}\n />\n <span\n style={{\n \"flex\": \"1\",\n \"textDecoration\": (\"line-through\" if todo.completed else \"none\"),\n \"color\": (\"#888\" if todo.completed else \"#333\")\n }}\n >\n {todo.title}\n </span>\n <button\n onClick={lambda -> None { onDelete(todo.id);}}\n style={{\n \"padding\": \"0.5rem\",\n \"backgroundColor\": \"transparent\",\n \"color\": \"#dc3545\",\n \"border\": \"none\",\n \"cursor\": \"pointer\",\n \"fontSize\": \"1.2rem\"\n }}\n >\n x\n </button>\n </div>;\n}\n",
32
+ "components/Button.cl.jac": "\"\"\"Button component for the Jac client application.\"\"\"\n\ndef:pub Button(\n label: str, onClick: any, variant: str = \"primary\", disabled: bool = False\n) -> any {\n base_styles = {\n \"padding\": \"0.75rem 1.5rem\",\n \"fontSize\": \"1rem\",\n \"fontWeight\": \"600\",\n \"borderRadius\": \"0.5rem\",\n \"border\": \"none\",\n \"cursor\": \"not-allowed\" if disabled else \"pointer\",\n \"transition\": \"all 0.2s ease\"\n };\n\n variant_styles = {\n \"primary\": {\n \"backgroundColor\": \"#9ca3af\" if disabled else \"#3b82f6\",\n \"color\": \"#ffffff\"\n },\n \"secondary\": {\n \"backgroundColor\": \"#e5e7eb\" if disabled else \"#6b7280\",\n \"color\": \"#ffffff\"\n }\n };\n\n return\n <button\n style={{** base_styles, ** variant_styles[variant]}}\n onClick={onClick}\n disabled={disabled}\n >\n {label}\n </button>;\n}\n",
33
+ "README.md": "# {{name}}\n\nA full-stack Jac application with user authentication and todo list functionality.\n\n## Project Structure\n\n```\n{{name}}/\n\u251c\u2500\u2500 jac.toml # Project configuration\n\u251c\u2500\u2500 main.jac # Main entry point (combines server + client)\n\u251c\u2500\u2500 endpoints.sv.jac # Server-side data models and walkers\n\u251c\u2500\u2500 frontend.cl.jac # Client-side React UI\n\u251c\u2500\u2500 components/ # Reusable client components\n\u2502 \u251c\u2500\u2500 AuthForm.cl.jac # Login/signup form\n\u2502 \u251c\u2500\u2500 TodoItem.cl.jac # Individual todo display\n\u2502 \u2514\u2500\u2500 Button.cl.jac # Reusable button component\n\u2514\u2500\u2500 assets/ # Static assets (images, fonts, etc.)\n```\n\n## Getting Started\n\nStart the development server:\n\n```bash\njac start main.jac\n```\n\nThen open your browser to the URL shown in the terminal.\n\n## Features\n\n- **User Authentication**: Sign up and log in with username/password\n- **Personal Todo Lists**: Each user gets their own isolated todo list\n- **CRUD Operations**: Create, read, update (toggle), and delete todos\n- **Real-time UI**: Responsive React frontend with instant updates\n\n## Architecture\n\nThis template demonstrates the full-stack Jac architecture:\n\n- **Server (`.sv.jac`)**: Defines data models (`node Todo`) and walkers for API operations\n- **Client (`.cl.jac`)**: React components with hooks and state management\n- **Entry Point (`main.jac`)**: Combines server and client imports\n\n## Adding Dependencies\n\nAdd npm packages with the --cl flag:\n\n```bash\njac add --cl react-router-dom\n```\n"
34
+ },
35
+ "directories": [
36
+ ".jac",
37
+ "assets"
38
+ ],
39
+ "gitignore_entries": [
40
+ "# Ignore all build artifacts in .jac directory",
41
+ "*"
42
+ ],
43
+ "root_gitignore_entries": [
44
+ "# Jac build artifacts",
45
+ ".jac/",
46
+ "",
47
+ "# Dependencies",
48
+ "node_modules/",
49
+ "",
50
+ "# IDE",
51
+ ".vscode/",
52
+ ".idea/"
53
+ ],
54
+ "jaclang": "0.9.8",
55
+ "plugins": [
56
+ {
57
+ "name": "jac-client",
58
+ "version": "0.2.8"
59
+ }
60
+ ]
61
+ }
@@ -15,13 +15,31 @@ import subprocess
15
15
  import sys
16
16
  import tempfile
17
17
  from collections.abc import Generator
18
+ from concurrent.futures import ThreadPoolExecutor
18
19
  from pathlib import Path
19
20
  from unittest.mock import patch
20
21
 
21
22
  import pytest
22
23
 
24
+ from jaclang.pycore.program import JacProgram
23
25
  from jaclang.pycore.runtime import JacRuntime as Jac
24
- from jaclang.pycore.runtime import JacRuntimeImpl, plugin_manager
26
+ from jaclang.pycore.runtime import JacRuntimeImpl, JacRuntimeInterface, plugin_manager
27
+
28
+ # =============================================================================
29
+ # Console Output Normalization - Disable Rich styling during tests
30
+ # =============================================================================
31
+
32
+
33
+ @pytest.fixture(autouse=True)
34
+ def disable_rich_console_formatting(monkeypatch: pytest.MonkeyPatch) -> None:
35
+ """Disable Rich console formatting for consistent test output.
36
+
37
+ Sets NO_COLOR and NO_EMOJI environment variables to ensure tests
38
+ get plain text output without ANSI codes or emoji prefixes.
39
+ """
40
+ monkeypatch.setenv("NO_COLOR", "1")
41
+ monkeypatch.setenv("NO_EMOJI", "1")
42
+
25
43
 
26
44
  # Store unregistered plugins globally for session-level management
27
45
  _external_plugins: list = []
@@ -79,11 +97,34 @@ def _get_env_with_npm() -> dict[str, str]:
79
97
 
80
98
 
81
99
  @pytest.fixture(autouse=True)
82
- def reset_jac_machine() -> Generator[None, None, None]:
100
+ def reset_jac_machine(tmp_path: Path) -> Generator[None, None, None]:
83
101
  """Reset Jac machine before and after each test."""
84
- Jac.reset_machine()
102
+ # Close existing context if any
103
+ if Jac.exec_ctx is not None:
104
+ Jac.exec_ctx.mem.close()
105
+
106
+ # Remove user .jac modules from sys.modules so they get re-imported fresh
107
+ # Keep jaclang.* and __main__ to avoid breaking dataclass references
108
+ for mod in list(Jac.loaded_modules.values()):
109
+ if not mod.__name__.startswith("jaclang.") and mod.__name__ != "__main__":
110
+ sys.modules.pop(mod.__name__, None)
111
+ Jac.loaded_modules.clear()
112
+
113
+ # Set up fresh state
114
+ Jac.base_path_dir = str(tmp_path)
115
+ Jac.program = JacProgram()
116
+ Jac.pool = ThreadPoolExecutor()
117
+ Jac.exec_ctx = JacRuntimeInterface.create_j_context(user_root=None)
118
+
85
119
  yield
86
- Jac.reset_machine()
120
+
121
+ # Cleanup after test
122
+ if Jac.exec_ctx is not None:
123
+ Jac.exec_ctx.mem.close()
124
+ for mod in list(Jac.loaded_modules.values()):
125
+ if not mod.__name__.startswith("jaclang.") and mod.__name__ != "__main__":
126
+ sys.modules.pop(mod.__name__, None)
127
+ Jac.loaded_modules.clear()
87
128
 
88
129
 
89
130
  # Session-scoped cache for npm installation
@@ -123,11 +164,11 @@ def npm_cache_dir() -> Generator[Path, None, None]:
123
164
  jac_toml = cache_dir / "jac.toml"
124
165
  jac_toml.write_text(_get_minimal_jac_toml())
125
166
 
126
- # Run jac add --cl to install packages
167
+ # Run jac add --npm to install packages
127
168
  jac_cmd = _get_jac_command()
128
169
  env = _get_env_with_npm()
129
170
  result = subprocess.run(
130
- [*jac_cmd, "add", "--cl"],
171
+ [*jac_cmd, "add", "--npm"],
131
172
  cwd=cache_dir,
132
173
  capture_output=True,
133
174
  text=True,
@@ -213,7 +254,7 @@ antd = "^6.0.0"
213
254
  jac_cmd = _get_jac_command()
214
255
  env = _get_env_with_npm()
215
256
  result = subprocess.run(
216
- [*jac_cmd, "add", "--cl"],
257
+ [*jac_cmd, "add", "--npm"],
217
258
  cwd=tmp_path,
218
259
  capture_output=True,
219
260
  text=True,