jac-client 0.2.8__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/button.jac +4 -3
  2. jac_client/examples/all-in-one/components/CategoryFilter.jac +36 -24
  3. jac_client/examples/all-in-one/components/Header.jac +12 -8
  4. jac_client/examples/all-in-one/components/ProfitOverview.jac +49 -35
  5. jac_client/examples/all-in-one/components/Summary.jac +59 -36
  6. jac_client/examples/all-in-one/components/TransactionForm.jac +142 -112
  7. jac_client/examples/all-in-one/components/TransactionItem.jac +37 -30
  8. jac_client/examples/all-in-one/components/TransactionList.jac +33 -26
  9. jac_client/examples/all-in-one/components/button.jac +4 -3
  10. jac_client/examples/all-in-one/components/navigation.jac +111 -117
  11. jac_client/examples/all-in-one/constants/categories.jac +23 -24
  12. jac_client/examples/all-in-one/constants/clients.jac +7 -8
  13. jac_client/examples/all-in-one/context/BudgetContext.jac +9 -6
  14. jac_client/examples/all-in-one/hooks/useBudget.jac +18 -12
  15. jac_client/examples/all-in-one/hooks/useLocalStorage.jac +14 -13
  16. jac_client/examples/all-in-one/main.jac +542 -0
  17. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +26 -12
  18. jac_client/examples/all-in-one/pages/FeaturesTest.jac +43 -12
  19. jac_client/examples/all-in-one/pages/LandingPage.jac +113 -90
  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 +114 -119
  23. jac_client/examples/all-in-one/pages/nestedDemo.jac +44 -51
  24. jac_client/examples/all-in-one/pages/notFound.jac +15 -21
  25. jac_client/examples/all-in-one/pages/signupPage.jac +113 -119
  26. jac_client/examples/all-in-one/utils/formatters.jac +5 -8
  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 +491 -411
  54. jac_client/plugin/client.jac +25 -0
  55. jac_client/plugin/client_runtime.cl.jac +10 -4
  56. jac_client/plugin/impl/client.impl.jac +96 -55
  57. jac_client/plugin/impl/client_runtime.impl.jac +155 -1
  58. jac_client/plugin/plugin_config.jac +211 -29
  59. jac_client/plugin/src/__init__.jac +0 -2
  60. jac_client/plugin/src/compiler.jac +0 -1
  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 +49 -17
  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 +191 -64
  69. jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
  70. jac_client/plugin/src/targets/desktop_target.jac +37 -0
  71. jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2347 -0
  72. jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
  73. jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
  74. jac_client/plugin/src/targets/register.jac +21 -0
  75. jac_client/plugin/src/targets/registry.jac +87 -0
  76. jac_client/plugin/src/targets/web_target.jac +35 -0
  77. jac_client/plugin/src/vite_bundler.jac +6 -0
  78. jac_client/plugin/utils/__init__.jac +3 -0
  79. jac_client/plugin/utils/bun_installer.jac +16 -0
  80. jac_client/plugin/utils/impl/bun_installer.impl.jac +99 -0
  81. jac_client/templates/client.jacpack +72 -0
  82. jac_client/templates/fullstack.jacpack +61 -0
  83. jac_client/tests/conftest.py +103 -47
  84. jac_client/tests/fixtures/spawn_test/app.jac +49 -52
  85. jac_client/tests/fixtures/with-ts/app.jac +27 -27
  86. jac_client/tests/test_cli.py +182 -71
  87. jac_client/tests/test_e2e.py +232 -0
  88. jac_client/tests/test_helpers.py +58 -0
  89. jac_client/tests/test_it.py +91 -135
  90. jac_client/tests/test_it_desktop.py +891 -0
  91. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/METADATA +6 -6
  92. jac_client-0.2.11.dist-info/RECORD +113 -0
  93. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/WHEEL +1 -1
  94. jac_client/examples/all-in-one/app.jac +0 -573
  95. jac_client/examples/all-in-one/pages/BudgetPlanner.cl.jac +0 -70
  96. jac_client/examples/all-in-one/pages/FeaturesTest.cl.jac +0 -552
  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 -371
  102. jac_client/examples/basic-auth-with-router/src/app.jac +0 -464
  103. jac_client/examples/basic-full-stack/src/app.jac +0 -359
  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 -89
  117. jac_client-0.2.8.dist-info/RECORD +0 -97
  118. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/entry_points.txt +0 -0
  119. {jac_client-0.2.8.dist-info → jac_client-0.2.11.dist-info}/top_level.txt +0 -0
@@ -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/runtime\" { 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
+ }
@@ -1,9 +1,9 @@
1
1
  """Pytest configuration and shared fixtures for jac-client tests.
2
2
 
3
3
  This module provides session-scoped fixtures to optimize test execution by:
4
- 1. Running npm install once per session and caching node_modules
4
+ 1. Running bun install once per session and caching node_modules
5
5
  2. Providing shared Vite build infrastructure
6
- 3. Mocking npm install for tests that only need jac.toml manipulation
6
+ 3. Mocking bun install for tests that only need jac.toml manipulation
7
7
  """
8
8
 
9
9
  from __future__ import annotations
@@ -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 = []
@@ -65,37 +83,59 @@ def _get_jac_command() -> list[str]:
65
83
  return [sys.executable, "-m", "jaclang"]
66
84
 
67
85
 
68
- def _get_env_with_npm() -> dict[str, str]:
69
- """Get environment dict with npm in PATH."""
86
+ def _get_env_with_bun() -> dict[str, str]:
87
+ """Get environment dict with bun in PATH."""
70
88
  env = os.environ.copy()
71
- # npm might be installed via nvm, ensure PATH includes common locations
72
- npm_path = shutil.which("npm")
73
- if npm_path:
74
- npm_dir = str(Path(npm_path).parent)
89
+ bun_path = shutil.which("bun")
90
+ if bun_path:
91
+ bun_dir = str(Path(bun_path).parent)
75
92
  current_path = env.get("PATH", "")
76
- if npm_dir not in current_path:
77
- env["PATH"] = f"{npm_dir}:{current_path}"
93
+ if bun_dir not in current_path:
94
+ env["PATH"] = f"{bun_dir}:{current_path}"
78
95
  return env
79
96
 
80
97
 
81
98
  @pytest.fixture(autouse=True)
82
- def reset_jac_machine() -> Generator[None, None, None]:
99
+ def reset_jac_machine(tmp_path: Path) -> Generator[None, None, None]:
83
100
  """Reset Jac machine before and after each test."""
84
- Jac.reset_machine()
101
+ # Close existing context if any
102
+ if Jac.exec_ctx is not None:
103
+ Jac.exec_ctx.mem.close()
104
+
105
+ # Remove user .jac modules from sys.modules so they get re-imported fresh
106
+ # Keep jaclang.* and __main__ to avoid breaking dataclass references
107
+ for mod in list(Jac.loaded_modules.values()):
108
+ if not mod.__name__.startswith("jaclang.") and mod.__name__ != "__main__":
109
+ sys.modules.pop(mod.__name__, None)
110
+ Jac.loaded_modules.clear()
111
+
112
+ # Set up fresh state
113
+ Jac.base_path_dir = str(tmp_path)
114
+ Jac.program = JacProgram()
115
+ Jac.pool = ThreadPoolExecutor()
116
+ Jac.exec_ctx = JacRuntimeInterface.create_j_context(user_root=None)
117
+
85
118
  yield
86
- Jac.reset_machine()
87
119
 
120
+ # Cleanup after test
121
+ if Jac.exec_ctx is not None:
122
+ Jac.exec_ctx.mem.close()
123
+ for mod in list(Jac.loaded_modules.values()):
124
+ if not mod.__name__.startswith("jaclang.") and mod.__name__ != "__main__":
125
+ sys.modules.pop(mod.__name__, None)
126
+ Jac.loaded_modules.clear()
88
127
 
89
- # Session-scoped cache for npm installation
90
- _npm_cache_dir: Path | None = None
128
+
129
+ # Session-scoped cache for bun installation
130
+ _bun_cache_dir: Path | None = None
91
131
 
92
132
 
93
133
  def _get_minimal_jac_toml() -> str:
94
- """Get minimal jac.toml content for npm cache setup."""
134
+ """Get minimal jac.toml content for bun cache setup."""
95
135
  return """[project]
96
- name = "npm-cache"
136
+ name = "bun-cache"
97
137
  version = "0.0.1"
98
- description = "Cached npm modules"
138
+ description = "Cached bun modules"
99
139
  entry-point = "app.jac"
100
140
 
101
141
  [plugins.client.vite.build]
@@ -104,30 +144,30 @@ minify = false
104
144
 
105
145
 
106
146
  @pytest.fixture(scope="session")
107
- def npm_cache_dir() -> Generator[Path, None, None]:
108
- """Session-scoped fixture that provides a directory with npm packages installed.
147
+ def bun_cache_dir() -> Generator[Path, None, None]:
148
+ """Session-scoped fixture that provides a directory with bun packages installed.
109
149
 
110
- This runs npm install once per test session and provides the path to the
150
+ This runs bun install once per test session and provides the path to the
111
151
  .jac/client/configs directory containing node_modules.
112
152
  """
113
- global _npm_cache_dir
153
+ global _bun_cache_dir
114
154
 
115
- if _npm_cache_dir is not None and _npm_cache_dir.exists():
116
- yield _npm_cache_dir
155
+ if _bun_cache_dir is not None and _bun_cache_dir.exists():
156
+ yield _bun_cache_dir
117
157
  return
118
158
 
119
159
  # Create a persistent temp directory for the session
120
- cache_dir = Path(tempfile.mkdtemp(prefix="jac_npm_cache_"))
160
+ cache_dir = Path(tempfile.mkdtemp(prefix="jac_bun_cache_"))
121
161
 
122
162
  # Create jac.toml
123
163
  jac_toml = cache_dir / "jac.toml"
124
164
  jac_toml.write_text(_get_minimal_jac_toml())
125
165
 
126
- # Run jac add --cl to install packages
166
+ # Run jac add --npm to install packages (flag name unchanged for backward compatibility)
127
167
  jac_cmd = _get_jac_command()
128
- env = _get_env_with_npm()
168
+ env = _get_env_with_bun()
129
169
  result = subprocess.run(
130
- [*jac_cmd, "add", "--cl"],
170
+ [*jac_cmd, "add", "--npm"],
131
171
  cwd=cache_dir,
132
172
  capture_output=True,
133
173
  text=True,
@@ -137,36 +177,44 @@ def npm_cache_dir() -> Generator[Path, None, None]:
137
177
  if result.returncode != 0:
138
178
  # Clean up on failure
139
179
  shutil.rmtree(cache_dir, ignore_errors=True)
140
- pytest.skip(f"Failed to set up npm cache: {result.stderr}")
180
+ pytest.skip(f"Failed to set up bun cache: {result.stderr}")
141
181
 
142
- _npm_cache_dir = cache_dir
182
+ _bun_cache_dir = cache_dir
143
183
  yield cache_dir
144
184
 
145
185
  # Cleanup after all tests complete
146
186
  shutil.rmtree(cache_dir, ignore_errors=True)
147
187
 
148
188
 
189
+ # Backward compatibility alias
190
+ @pytest.fixture(scope="session")
191
+ def npm_cache_dir(bun_cache_dir: Path) -> Generator[Path, None, None]:
192
+ """Backward compatibility alias for bun_cache_dir."""
193
+ yield bun_cache_dir
194
+
195
+
149
196
  @pytest.fixture
150
- def vite_project_dir(npm_cache_dir: Path, tmp_path: Path) -> Path:
197
+ def vite_project_dir(bun_cache_dir: Path, tmp_path: Path) -> Path:
151
198
  """Fixture that provides a project directory with pre-installed node_modules.
152
199
 
153
- This copies node_modules from the session cache instead of running npm install.
200
+ This copies node_modules from the session cache instead of running bun install.
154
201
  """
155
202
  # Create jac.toml in the temp directory
156
203
  jac_toml = tmp_path / "jac.toml"
157
204
  jac_toml.write_text(_get_minimal_jac_toml())
158
205
 
159
206
  # Copy .jac/client/configs directory (contains package.json)
160
- source_configs = npm_cache_dir / ".jac" / "client" / "configs"
207
+ source_configs = bun_cache_dir / ".jac" / "client" / "configs"
161
208
  dest_configs = tmp_path / ".jac" / "client" / "configs"
162
209
  if source_configs.exists():
163
210
  dest_configs.parent.mkdir(parents=True, exist_ok=True)
164
211
  shutil.copytree(source_configs, dest_configs, symlinks=True)
165
212
 
166
- # Copy node_modules from project root (npm installs there, not in .jac/client/configs)
167
- source_node_modules = npm_cache_dir / "node_modules"
168
- dest_node_modules = tmp_path / "node_modules"
213
+ # Copy node_modules from .jac/client/ (bun installs there)
214
+ source_node_modules = bun_cache_dir / ".jac" / "client" / "node_modules"
215
+ dest_node_modules = tmp_path / ".jac" / "client" / "node_modules"
169
216
  if source_node_modules.exists():
217
+ dest_node_modules.parent.mkdir(parents=True, exist_ok=True)
170
218
  shutil.copytree(source_node_modules, dest_node_modules, symlinks=True)
171
219
 
172
220
  # Create required directories
@@ -178,7 +226,7 @@ def vite_project_dir(npm_cache_dir: Path, tmp_path: Path) -> Path:
178
226
 
179
227
 
180
228
  @pytest.fixture
181
- def vite_project_with_antd(npm_cache_dir: Path, tmp_path: Path) -> Path:
229
+ def vite_project_with_antd(bun_cache_dir: Path, tmp_path: Path) -> Path:
182
230
  """Fixture that provides a project directory with antd pre-installed."""
183
231
  # Create jac.toml with antd dependency
184
232
  jac_toml_content = """[project]
@@ -197,23 +245,24 @@ antd = "^6.0.0"
197
245
  jac_toml.write_text(jac_toml_content)
198
246
 
199
247
  # Copy base .jac/client/configs first for faster install
200
- source_configs = npm_cache_dir / ".jac" / "client" / "configs"
248
+ source_configs = bun_cache_dir / ".jac" / "client" / "configs"
201
249
  dest_configs = tmp_path / ".jac" / "client" / "configs"
202
250
  if source_configs.exists():
203
251
  dest_configs.parent.mkdir(parents=True, exist_ok=True)
204
252
  shutil.copytree(source_configs, dest_configs, symlinks=True)
205
253
 
206
- # Copy base node_modules for faster install (npm will add antd on top)
207
- source_node_modules = npm_cache_dir / "node_modules"
208
- dest_node_modules = tmp_path / "node_modules"
254
+ # Copy base node_modules for faster install (bun will add antd on top)
255
+ source_node_modules = bun_cache_dir / ".jac" / "client" / "node_modules"
256
+ dest_node_modules = tmp_path / ".jac" / "client" / "node_modules"
209
257
  if source_node_modules.exists():
258
+ dest_node_modules.parent.mkdir(parents=True, exist_ok=True)
210
259
  shutil.copytree(source_node_modules, dest_node_modules, symlinks=True)
211
260
 
212
261
  # Install antd on top (uses cached node_modules as base)
213
262
  jac_cmd = _get_jac_command()
214
- env = _get_env_with_npm()
263
+ env = _get_env_with_bun()
215
264
  result = subprocess.run(
216
- [*jac_cmd, "add", "--cl"],
265
+ [*jac_cmd, "add", "--npm"],
217
266
  cwd=tmp_path,
218
267
  capture_output=True,
219
268
  text=True,
@@ -231,10 +280,10 @@ antd = "^6.0.0"
231
280
 
232
281
 
233
282
  @pytest.fixture
234
- def mock_npm_install():
235
- """Fixture that mocks npm install for tests that only test jac.toml manipulation.
283
+ def mock_bun_install():
284
+ """Fixture that mocks bun install for tests that only test jac.toml manipulation.
236
285
 
237
- Use this for CLI tests (add/remove commands) that don't need actual npm packages.
286
+ Use this for CLI tests (add/remove commands) that don't need actual packages.
238
287
  """
239
288
  with patch(
240
289
  "jac_client.plugin.src.package_installer.PackageInstaller._regenerate_and_install"
@@ -242,6 +291,13 @@ def mock_npm_install():
242
291
  yield mock
243
292
 
244
293
 
294
+ # Backward compatibility alias
295
+ @pytest.fixture
296
+ def mock_npm_install(mock_bun_install: Generator) -> Generator:
297
+ """Backward compatibility alias for mock_bun_install."""
298
+ yield mock_bun_install
299
+
300
+
245
301
  @pytest.fixture
246
302
  def cli_test_dir(tmp_path: Path) -> Path:
247
303
  """Fixture that provides a minimal test directory for CLI tests."""
@@ -9,7 +9,7 @@ walker test_walker {
9
9
  has message: str = "Hello from walker";
10
10
 
11
11
  can execute with `root entry {
12
- report {"result": self.message} ;
12
+ report {"result": self.message};
13
13
  }
14
14
  }
15
15
 
@@ -17,7 +17,7 @@ walker parameterized_walker {
17
17
  has value: int;
18
18
 
19
19
  can execute with `root entry {
20
- report {"computed": self.value * 2} ;
20
+ report {"computed": self.value * 2};
21
21
  }
22
22
  }
23
23
 
@@ -27,15 +27,13 @@ walker positional_walker {
27
27
  has metadata: dict = {};
28
28
 
29
29
  can execute with `root entry {
30
- report {"label": self.label, "count": self.count, "meta": self.metadata} ;
30
+ report {"label": self.label, "count": self.count, "meta": self.metadata};
31
31
  }
32
32
  }
33
33
 
34
34
  # Client-side code testing both spawn orderings
35
- cl import from react { useEffect }
36
-
37
35
  cl {
38
- def app() -> any {
36
+ def app() -> any {
39
37
  has standardResult: any = None;
40
38
  has standardComputed: any = None;
41
39
  has reverseResult: any = None;
@@ -44,7 +42,7 @@ cl {
44
42
  has positionalResult: any = None;
45
43
  has spreadResult: any = None;
46
44
 
47
- async def loadData() -> None {
45
+ async can with entry {
48
46
  # Test standard spawn order: node spawn walker()
49
47
  data1 = root spawn test_walker();
50
48
  standardResult = data1;
@@ -76,51 +74,50 @@ cl {
76
74
  spreadResult = data7;
77
75
  }
78
76
 
79
- useEffect(lambda -> None{ loadData();} , []);
80
-
81
- return <div>
82
- <h1>
83
- Spawn Operator Test
84
- </h1>
85
- <h2>
86
- Standard Order (node spawn walker)
87
- </h2>
88
- <div>
89
- Result: {JSON.stringify(standardResult)}
90
- </div>
91
- <div>
92
- Computed: {JSON.stringify(standardComputed)}
93
- </div>
94
- <h2>
95
- Reverse Order (walker spawn node)
96
- </h2>
97
- <div>
98
- Result: {JSON.stringify(reverseResult)}
99
- </div>
100
- <h2>
101
- UUID Spawn (uuid spawn walker)
102
- </h2>
103
- <div>
104
- Result: {JSON.stringify(uuidResult)}
105
- </div>
106
- <h2>
107
- Reverse UUID Spawn (walker spawn uuid)
108
- </h2>
109
- <div>
110
- Result: {JSON.stringify(reverseUuidResult)}
111
- </div>
112
- <h2>
113
- Positional Walker Arguments
114
- </h2>
115
- <div>
116
- Result: {JSON.stringify(positionalResult)}
117
- </div>
118
- <h2>
119
- Spread Walker Arguments
120
- </h2>
77
+ return
121
78
  <div>
122
- Result: {JSON.stringify(spreadResult)}
123
- </div>
124
- </div>;
79
+ <h1>
80
+ Spawn Operator Test
81
+ </h1>
82
+ <h2>
83
+ Standard Order (node spawn walker)
84
+ </h2>
85
+ <div>
86
+ Result: {JSON.stringify(standardResult)}
87
+ </div>
88
+ <div>
89
+ Computed: {JSON.stringify(standardComputed)}
90
+ </div>
91
+ <h2>
92
+ Reverse Order (walker spawn node)
93
+ </h2>
94
+ <div>
95
+ Result: {JSON.stringify(reverseResult)}
96
+ </div>
97
+ <h2>
98
+ UUID Spawn (uuid spawn walker)
99
+ </h2>
100
+ <div>
101
+ Result: {JSON.stringify(uuidResult)}
102
+ </div>
103
+ <h2>
104
+ Reverse UUID Spawn (walker spawn uuid)
105
+ </h2>
106
+ <div>
107
+ Result: {JSON.stringify(reverseUuidResult)}
108
+ </div>
109
+ <h2>
110
+ Positional Walker Arguments
111
+ </h2>
112
+ <div>
113
+ Result: {JSON.stringify(positionalResult)}
114
+ </div>
115
+ <h2>
116
+ Spread Walker Arguments
117
+ </h2>
118
+ <div>
119
+ Result: {JSON.stringify(spreadResult)}
120
+ </div>
121
+ </div>;
125
122
  }
126
123
  }
@@ -1,35 +1,35 @@
1
1
 
2
2
  # Pages
3
- cl import from react { useEffect }
4
3
  cl import from ".components/Button.tsx" { Button }
5
4
 
6
5
  cl {
7
- def app() -> any {
6
+ def app() -> any {
8
7
  has count: int = 0;
9
- useEffect(lambda -> None{ console.log("Count: ", count);} , [count]);
10
- return <div
11
- style={{padding: "2rem", fontFamily: "Arial, sans-serif"}}
12
- >
13
- <h1>
14
- Hello, World!
15
- </h1>
16
- <p>
17
- Count: {count}
18
- </p>
19
- <div
20
- style={{display: "flex", gap: "1rem", marginTop: "1rem"}}
21
- >
22
- <Button
23
- label="Increment"
24
- onClick={lambda -> None{ count = count + 1;} }
25
- variant="primary"
26
- />
27
- <Button
28
- label="Reset"
29
- onClick={lambda -> None{ count = 0;} }
30
- variant="secondary"
31
- />
32
- </div>
33
- </div>;
8
+
9
+ can with count entry {
10
+ console.log("Count: ", count);
11
+ }
12
+
13
+ return
14
+ <div style={{padding: "2rem", fontFamily: "Arial, sans-serif"}}>
15
+ <h1>
16
+ Hello, World!
17
+ </h1>
18
+ <p>
19
+ Count: {count}
20
+ </p>
21
+ <div style={{display: "flex", gap: "1rem", marginTop: "1rem"}}>
22
+ <Button
23
+ label="Increment"
24
+ onClick={lambda -> None { count = count + 1;}}
25
+ variant="primary"
26
+ />
27
+ <Button
28
+ label="Reset"
29
+ onClick={lambda -> None { count = 0;}}
30
+ variant="secondary"
31
+ />
32
+ </div>
33
+ </div>;
34
34
  }
35
35
  }