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
@@ -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
111
- .jac-client.configs directory containing node_modules.
150
+ This runs bun install once per test session and provides the path to the
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,35 +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
- # Copy .jac-client.configs directory (contains package.json)
160
- source_configs = npm_cache_dir / ".jac-client.configs"
161
- dest_configs = tmp_path / ".jac-client.configs"
206
+ # Copy .jac/client/configs directory (contains package.json)
207
+ source_configs = bun_cache_dir / ".jac" / "client" / "configs"
208
+ dest_configs = tmp_path / ".jac" / "client" / "configs"
162
209
  if source_configs.exists():
210
+ dest_configs.parent.mkdir(parents=True, exist_ok=True)
163
211
  shutil.copytree(source_configs, dest_configs, symlinks=True)
164
212
 
165
- # Copy node_modules from project root (npm installs there, not in .jac-client.configs)
166
- source_node_modules = npm_cache_dir / "node_modules"
167
- 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"
168
216
  if source_node_modules.exists():
217
+ dest_node_modules.parent.mkdir(parents=True, exist_ok=True)
169
218
  shutil.copytree(source_node_modules, dest_node_modules, symlinks=True)
170
219
 
171
220
  # Create required directories
@@ -177,7 +226,7 @@ def vite_project_dir(npm_cache_dir: Path, tmp_path: Path) -> Path:
177
226
 
178
227
 
179
228
  @pytest.fixture
180
- 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:
181
230
  """Fixture that provides a project directory with antd pre-installed."""
182
231
  # Create jac.toml with antd dependency
183
232
  jac_toml_content = """[project]
@@ -195,23 +244,25 @@ antd = "^6.0.0"
195
244
  jac_toml = tmp_path / "jac.toml"
196
245
  jac_toml.write_text(jac_toml_content)
197
246
 
198
- # Copy base .jac-client.configs first for faster install
199
- source_configs = npm_cache_dir / ".jac-client.configs"
200
- dest_configs = tmp_path / ".jac-client.configs"
247
+ # Copy base .jac/client/configs first for faster install
248
+ source_configs = bun_cache_dir / ".jac" / "client" / "configs"
249
+ dest_configs = tmp_path / ".jac" / "client" / "configs"
201
250
  if source_configs.exists():
251
+ dest_configs.parent.mkdir(parents=True, exist_ok=True)
202
252
  shutil.copytree(source_configs, dest_configs, symlinks=True)
203
253
 
204
- # Copy base node_modules for faster install (npm will add antd on top)
205
- source_node_modules = npm_cache_dir / "node_modules"
206
- 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"
207
257
  if source_node_modules.exists():
258
+ dest_node_modules.parent.mkdir(parents=True, exist_ok=True)
208
259
  shutil.copytree(source_node_modules, dest_node_modules, symlinks=True)
209
260
 
210
261
  # Install antd on top (uses cached node_modules as base)
211
262
  jac_cmd = _get_jac_command()
212
- env = _get_env_with_npm()
263
+ env = _get_env_with_bun()
213
264
  result = subprocess.run(
214
- [*jac_cmd, "add", "--cl"],
265
+ [*jac_cmd, "add", "--npm"],
215
266
  cwd=tmp_path,
216
267
  capture_output=True,
217
268
  text=True,
@@ -229,10 +280,10 @@ antd = "^6.0.0"
229
280
 
230
281
 
231
282
  @pytest.fixture
232
- def mock_npm_install():
233
- """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.
234
285
 
235
- 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.
236
287
  """
237
288
  with patch(
238
289
  "jac_client.plugin.src.package_installer.PackageInstaller._regenerate_and_install"
@@ -240,6 +291,13 @@ def mock_npm_install():
240
291
  yield mock
241
292
 
242
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
+
243
301
  @pytest.fixture
244
302
  def cli_test_dir(tmp_path: Path) -> Path:
245
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,103 +27,97 @@ 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 {
36
- useState,
37
- useEffect
38
- }
39
-
40
35
  cl {
41
- def app() -> any {
42
- [standardResult, setStandardResult] = useState(None);
43
- [standardComputed, setStandardComputed] = useState(None);
44
- [reverseResult, setReverseResult] = useState(None);
45
- [uuidResult, setUuidResult] = useState(None);
46
- [reverseUuidResult, setReverseUuidResult] = useState(None);
47
- [positionalResult, setPositionalResult] = useState(None);
48
- [spreadResult, setSpreadResult] = useState(None);
49
-
50
- async def loadData() -> None {
36
+ def app() -> any {
37
+ has standardResult: any = None;
38
+ has standardComputed: any = None;
39
+ has reverseResult: any = None;
40
+ has uuidResult: any = None;
41
+ has reverseUuidResult: any = None;
42
+ has positionalResult: any = None;
43
+ has spreadResult: any = None;
44
+
45
+ async can with entry {
51
46
  # Test standard spawn order: node spawn walker()
52
47
  data1 = root spawn test_walker();
53
- setStandardResult(data1);
48
+ standardResult = data1;
54
49
 
55
50
  data2 = root spawn parameterized_walker(value=42);
56
- setStandardComputed(data2);
51
+ standardComputed = data2;
57
52
 
58
53
  # Test reverse spawn order: walker() spawn node
59
54
  data3 = test_walker(message="Reverse spawn!") spawn root;
60
- setReverseResult(data3);
55
+ reverseResult = data3;
61
56
 
62
57
  # Test spawn with UUID string: uuid_string spawn walker()
63
58
  node_id = "550e8400-e29b-41d4-a716-446655440000";
64
59
  data4 = node_id spawn test_walker();
65
- setUuidResult(data4);
60
+ uuidResult = data4;
66
61
 
67
62
  # Test reverse spawn with UUID string: walker() spawn uuid_string
68
63
  another_node_id = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
69
64
  data5 = parameterized_walker(value=100) spawn another_node_id;
70
- setReverseUuidResult(data5);
65
+ reverseUuidResult = data5;
71
66
 
72
67
  # Test positional walker arguments inferred from has fields
73
68
  data6 = node_id spawn positional_walker("Node positional", 2);
74
- setPositionalResult(data6);
69
+ positionalResult = data6;
75
70
 
76
71
  # Test **kwargs via spread when walker is on left-hand side
77
72
  extra_fields = {"metadata": {"source": "client-side"}};
78
73
  data7 = positional_walker("Spread order", 5, **extra_fields) spawn root;
79
- setSpreadResult(data7);
74
+ spreadResult = data7;
80
75
  }
81
76
 
82
- useEffect(lambda -> None{ loadData();} , []);
83
-
84
- return <div>
85
- <h1>
86
- Spawn Operator Test
87
- </h1>
88
- <h2>
89
- Standard Order (node spawn walker)
90
- </h2>
91
- <div>
92
- Result: {JSON.stringify(standardResult)}
93
- </div>
94
- <div>
95
- Computed: {JSON.stringify(standardComputed)}
96
- </div>
97
- <h2>
98
- Reverse Order (walker spawn node)
99
- </h2>
100
- <div>
101
- Result: {JSON.stringify(reverseResult)}
102
- </div>
103
- <h2>
104
- UUID Spawn (uuid spawn walker)
105
- </h2>
106
- <div>
107
- Result: {JSON.stringify(uuidResult)}
108
- </div>
109
- <h2>
110
- Reverse UUID Spawn (walker spawn uuid)
111
- </h2>
112
- <div>
113
- Result: {JSON.stringify(reverseUuidResult)}
114
- </div>
115
- <h2>
116
- Positional Walker Arguments
117
- </h2>
118
- <div>
119
- Result: {JSON.stringify(positionalResult)}
120
- </div>
121
- <h2>
122
- Spread Walker Arguments
123
- </h2>
77
+ return
124
78
  <div>
125
- Result: {JSON.stringify(spreadResult)}
126
- </div>
127
- </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>;
128
122
  }
129
123
  }
@@ -1,35 +1,35 @@
1
1
 
2
2
  # Pages
3
- cl import from react { useState, useEffect }
4
3
  cl import from ".components/Button.tsx" { Button }
5
4
 
6
5
  cl {
7
- def app() -> any {
8
- [count, setCount] = useState(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{ setCount(count + 1);} }
25
- variant="primary"
26
- />
27
- <Button
28
- label="Reset"
29
- onClick={lambda -> None{ setCount(0);} }
30
- variant="secondary"
31
- />
32
- </div>
33
- </div>;
6
+ def app() -> any {
7
+ has count: int = 0;
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
  }