jac-client 0.2.10__py3-none-any.whl → 0.2.12__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.
- jac_client/examples/all-in-one/button.jac +4 -3
- jac_client/examples/all-in-one/components/CategoryFilter.jac +36 -24
- jac_client/examples/all-in-one/components/Header.jac +12 -8
- jac_client/examples/all-in-one/components/ProfitOverview.jac +49 -35
- jac_client/examples/all-in-one/components/Summary.jac +59 -36
- jac_client/examples/all-in-one/components/TransactionForm.jac +142 -112
- jac_client/examples/all-in-one/components/TransactionItem.jac +37 -30
- jac_client/examples/all-in-one/components/TransactionList.jac +33 -26
- jac_client/examples/all-in-one/components/button.jac +4 -3
- jac_client/examples/all-in-one/components/navigation.jac +111 -117
- jac_client/examples/all-in-one/constants/categories.jac +23 -24
- jac_client/examples/all-in-one/constants/clients.jac +7 -8
- jac_client/examples/all-in-one/context/BudgetContext.jac +9 -6
- jac_client/examples/all-in-one/hooks/useBudget.jac +18 -12
- jac_client/examples/all-in-one/hooks/useLocalStorage.jac +14 -13
- jac_client/examples/all-in-one/main.jac +340 -371
- jac_client/examples/all-in-one/pages/BudgetPlanner.jac +19 -12
- jac_client/examples/all-in-one/pages/FeaturesTest.jac +31 -15
- jac_client/examples/all-in-one/pages/LandingPage.jac +113 -90
- jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +34 -39
- jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +464 -352
- jac_client/examples/all-in-one/pages/loginPage.jac +114 -119
- jac_client/examples/all-in-one/pages/nestedDemo.jac +43 -50
- jac_client/examples/all-in-one/pages/notFound.jac +14 -15
- jac_client/examples/all-in-one/pages/signupPage.jac +113 -119
- jac_client/examples/all-in-one/utils/formatters.jac +5 -8
- jac_client/examples/asset-serving/css-with-image/main.jac +77 -73
- jac_client/examples/asset-serving/image-asset/main.jac +47 -46
- jac_client/examples/asset-serving/import-alias/main.jac +93 -95
- jac_client/examples/basic/main.jac +17 -15
- jac_client/examples/basic-auth/main.jac +246 -254
- jac_client/examples/basic-auth-with-router/main.jac +272 -285
- jac_client/examples/basic-full-stack/main.jac +245 -242
- jac_client/examples/css-styling/js-styling/main.jac +41 -62
- jac_client/examples/css-styling/material-ui/main.jac +90 -90
- jac_client/examples/css-styling/pure-css/main.jac +35 -44
- jac_client/examples/css-styling/sass-example/main.jac +35 -44
- jac_client/examples/css-styling/styled-components/main.jac +38 -47
- jac_client/examples/css-styling/tailwind-example/main.jac +54 -43
- jac_client/examples/full-stack-with-auth/main.jac +407 -433
- jac_client/examples/little-x/main.jac +306 -344
- jac_client/examples/little-x/src/submit-button.jac +15 -14
- jac_client/examples/nested-folders/nested-advance/main.jac +18 -27
- jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +4 -6
- jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +9 -13
- jac_client/examples/nested-folders/nested-advance/src/level1/Card.jac +29 -32
- jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +12 -18
- jac_client/examples/nested-folders/nested-basic/main.jac +7 -5
- jac_client/examples/nested-folders/nested-basic/src/button.jac +4 -3
- jac_client/examples/nested-folders/nested-basic/src/components/button.jac +4 -3
- jac_client/examples/ts-support/main.jac +26 -26
- jac_client/examples/with-router/main.jac +186 -223
- jac_client/plugin/client_runtime.cl.jac +5 -3
- jac_client/plugin/impl/client_runtime.impl.jac +1 -1
- jac_client/plugin/plugin_config.jac +53 -99
- jac_client/plugin/src/__init__.jac +0 -2
- jac_client/plugin/src/compiler.jac +0 -1
- jac_client/plugin/src/impl/compiler.impl.jac +49 -17
- jac_client/plugin/src/impl/jac_to_js.impl.jac +5 -1
- jac_client/plugin/src/impl/package_installer.impl.jac +20 -20
- jac_client/plugin/src/impl/vite_bundler.impl.jac +146 -84
- jac_client/plugin/src/targets/impl/desktop_target.impl.jac +54 -41
- jac_client/plugin/utils/__init__.jac +3 -0
- jac_client/plugin/utils/bun_installer.jac +16 -0
- jac_client/plugin/utils/client_deps.jac +14 -0
- jac_client/plugin/utils/impl/bun_installer.impl.jac +99 -0
- jac_client/plugin/utils/impl/client_deps.impl.jac +73 -0
- jac_client/templates/client.jacpack +0 -4
- jac_client/templates/fullstack.jacpack +1 -5
- jac_client/tests/conftest.py +56 -41
- jac_client/tests/fixtures/spawn_test/app.jac +49 -52
- jac_client/tests/fixtures/with-ts/app.jac +27 -27
- jac_client/tests/test_cli.py +71 -6
- jac_client/tests/test_helpers.py +11 -18
- jac_client/tests/test_it.py +1 -1
- {jac_client-0.2.10.dist-info → jac_client-0.2.12.dist-info}/METADATA +5 -5
- jac_client-0.2.12.dist-info/RECORD +115 -0
- {jac_client-0.2.10.dist-info → jac_client-0.2.12.dist-info}/WHEEL +1 -1
- jac_client/plugin/src/babel_processor.jac +0 -18
- jac_client/plugin/src/impl/babel_processor.impl.jac +0 -89
- jac_client/plugin/utils/impl/node_installer.impl.jac +0 -249
- jac_client/plugin/utils/node_installer.jac +0 -41
- jac_client-0.2.10.dist-info/RECORD +0 -115
- {jac_client-0.2.10.dist-info → jac_client-0.2.12.dist-info}/entry_points.txt +0 -0
- {jac_client-0.2.10.dist-info → jac_client-0.2.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Implementation of Bun installer utility."""
|
|
2
|
+
|
|
3
|
+
"""Check if bun is available, add to PATH if needed, or prompt to install."""
|
|
4
|
+
impl ensure_bun_available -> bool {
|
|
5
|
+
import os;
|
|
6
|
+
import shutil;
|
|
7
|
+
# Check if bun is already in PATH
|
|
8
|
+
if shutil.which("bun") {
|
|
9
|
+
return True;
|
|
10
|
+
}
|
|
11
|
+
# Check if bun exists at ~/.bun/bin but not in PATH
|
|
12
|
+
bun_bin = os.path.expanduser("~/.bun/bin");
|
|
13
|
+
bun_path = os.path.join(bun_bin, "bun");
|
|
14
|
+
if os.path.exists(bun_path) {
|
|
15
|
+
# Add to PATH for current process
|
|
16
|
+
current_path = os.environ.get("PATH", "");
|
|
17
|
+
os.environ["PATH"] = f"{bun_bin}:{current_path}";
|
|
18
|
+
return True;
|
|
19
|
+
}
|
|
20
|
+
# Bun not found - prompt to install
|
|
21
|
+
return prompt_install_bun();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
"""Prompt user to install Bun and install if confirmed."""
|
|
25
|
+
impl prompt_install_bun -> bool {
|
|
26
|
+
import os;
|
|
27
|
+
import subprocess;
|
|
28
|
+
import sys;
|
|
29
|
+
import shutil;
|
|
30
|
+
print("\n ⚠ Bun is required but not installed.", file=sys.stderr);
|
|
31
|
+
print(
|
|
32
|
+
" Bun is a fast JavaScript runtime used for package management and bundling.",
|
|
33
|
+
file=sys.stderr
|
|
34
|
+
);
|
|
35
|
+
print(" Learn more: https://bun.sh\n", file=sys.stderr);
|
|
36
|
+
try {
|
|
37
|
+
response = input(" Install Bun now? [Y/n]: ").strip().lower();
|
|
38
|
+
} except (EOFError, KeyboardInterrupt) {
|
|
39
|
+
print("", file=sys.stderr);
|
|
40
|
+
return False;
|
|
41
|
+
}
|
|
42
|
+
if response and response not in ('y', 'yes', '') {
|
|
43
|
+
return False;
|
|
44
|
+
}
|
|
45
|
+
print("\n ⏳ Installing Bun...", flush=True);
|
|
46
|
+
try {
|
|
47
|
+
# Check if curl is available
|
|
48
|
+
subprocess.run(
|
|
49
|
+
["curl", "--version"], capture_output=True, check=True, timeout=5
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
# Install Bun via official script
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
["sh", "-c", "curl -fsSL https://bun.sh/install | bash"],
|
|
55
|
+
timeout=120,
|
|
56
|
+
check=False
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if result.returncode == 0 {
|
|
60
|
+
print(" ✔ Bun installed successfully!", flush=True);
|
|
61
|
+
# Add Bun to PATH for current session
|
|
62
|
+
bun_bin = os.path.expanduser("~/.bun/bin");
|
|
63
|
+
if os.path.exists(bun_bin) {
|
|
64
|
+
current_path = os.environ.get("PATH", "");
|
|
65
|
+
os.environ["PATH"] = f"{bun_bin}:{current_path}";
|
|
66
|
+
# Verify installation
|
|
67
|
+
if shutil.which("bun") {
|
|
68
|
+
verify = subprocess.run(
|
|
69
|
+
["bun", "--version"], capture_output=True, text=True
|
|
70
|
+
);
|
|
71
|
+
if verify.returncode == 0 {
|
|
72
|
+
print(f" ✔ Verified: Bun {verify.stdout.strip()}", flush=True);
|
|
73
|
+
return True;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
print(
|
|
78
|
+
" ⚠ Bun installed but may require terminal restart.", file=sys.stderr
|
|
79
|
+
);
|
|
80
|
+
print(" Run: source ~/.bashrc (or restart terminal)", file=sys.stderr);
|
|
81
|
+
return True;
|
|
82
|
+
} else {
|
|
83
|
+
print(" ✖ Bun installation failed.", file=sys.stderr);
|
|
84
|
+
return False;
|
|
85
|
+
}
|
|
86
|
+
} except subprocess.TimeoutExpired {
|
|
87
|
+
print(" ✖ Installation timed out.", file=sys.stderr);
|
|
88
|
+
return False;
|
|
89
|
+
} except FileNotFoundError {
|
|
90
|
+
print(
|
|
91
|
+
" ✖ curl not found. Please install Bun manually: https://bun.sh",
|
|
92
|
+
file=sys.stderr
|
|
93
|
+
);
|
|
94
|
+
return False;
|
|
95
|
+
} except Exception as e {
|
|
96
|
+
print(f" ✖ Installation failed: {e}", file=sys.stderr);
|
|
97
|
+
return False;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Implementation of client dependency checker utility."""
|
|
2
|
+
|
|
3
|
+
"""Check if client npm dependencies are configured, prompt to install if missing."""
|
|
4
|
+
impl ensure_client_deps(config_loader: object) -> bool {
|
|
5
|
+
package_config = config_loader.get_package_config();
|
|
6
|
+
dependencies = package_config.get('dependencies', {});
|
|
7
|
+
dev_dependencies = package_config.get('devDependencies', {});
|
|
8
|
+
# If either deps or devDeps has entries, assume configured
|
|
9
|
+
if dependencies or dev_dependencies {
|
|
10
|
+
return True;
|
|
11
|
+
}
|
|
12
|
+
# No deps configured — prompt the user
|
|
13
|
+
return prompt_install_client_deps(config_loader);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
"""Prompt user to install default jac-client npm dependencies."""
|
|
17
|
+
def prompt_install_client_deps(config_loader: object) -> bool {
|
|
18
|
+
import sys;
|
|
19
|
+
print("\n ⚠ Client dependencies are not configured.", file=sys.stderr);
|
|
20
|
+
print(
|
|
21
|
+
" jac-client requires npm packages (react, vite, etc.) to build client pages.",
|
|
22
|
+
file=sys.stderr
|
|
23
|
+
);
|
|
24
|
+
print(" These will be added to your jac.toml file.\n", file=sys.stderr);
|
|
25
|
+
try {
|
|
26
|
+
response = input(" Install default client dependencies now? [Y/n]: ").strip().lower();
|
|
27
|
+
} except (EOFError, KeyboardInterrupt) {
|
|
28
|
+
print("", file=sys.stderr);
|
|
29
|
+
return False;
|
|
30
|
+
}
|
|
31
|
+
if response and response not in ('y', 'yes', '') {
|
|
32
|
+
print(
|
|
33
|
+
"\n To configure manually, add [dependencies.npm] to your jac.toml.",
|
|
34
|
+
file=sys.stderr
|
|
35
|
+
);
|
|
36
|
+
print(
|
|
37
|
+
" Or create a new project with: jac create --use client\n",
|
|
38
|
+
file=sys.stderr
|
|
39
|
+
);
|
|
40
|
+
return False;
|
|
41
|
+
}
|
|
42
|
+
# Default runtime dependencies
|
|
43
|
+
default_deps: dict[str, str] = {
|
|
44
|
+
'react': '^18.2.0',
|
|
45
|
+
'react-dom': '^18.2.0',
|
|
46
|
+
'react-router-dom': '^6.22.0',
|
|
47
|
+
'react-error-boundary': '^5.0.0'
|
|
48
|
+
};
|
|
49
|
+
# Default build infrastructure dependencies
|
|
50
|
+
default_dev_deps: dict[str, str] = {
|
|
51
|
+
'vite': '^6.4.1',
|
|
52
|
+
'@vitejs/plugin-react': '^4.2.1',
|
|
53
|
+
'typescript': '^5.3.3',
|
|
54
|
+
'@types/react': '^18.2.0',
|
|
55
|
+
'@types/react-dom': '^18.2.0'
|
|
56
|
+
};
|
|
57
|
+
print("\n ⏳ Adding default client dependencies to jac.toml...", flush=True);
|
|
58
|
+
try {
|
|
59
|
+
for (name, version) in default_deps.items() {
|
|
60
|
+
config_loader.add_dependency(name, version, is_dev=False);
|
|
61
|
+
}
|
|
62
|
+
for (name, version) in default_dev_deps.items() {
|
|
63
|
+
config_loader.add_dependency(name, version, is_dev=True);
|
|
64
|
+
}
|
|
65
|
+
config_loader.save();
|
|
66
|
+
config_loader.invalidate();
|
|
67
|
+
print(" ✔ Default client dependencies added to jac.toml\n", flush=True);
|
|
68
|
+
return True;
|
|
69
|
+
} except Exception as e {
|
|
70
|
+
print(f" ✖ Failed to add dependencies: {e}", file=sys.stderr);
|
|
71
|
+
return False;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"files": {
|
|
27
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
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",
|
|
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
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
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
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",
|
|
@@ -36,10 +36,6 @@
|
|
|
36
36
|
".jac",
|
|
37
37
|
"assets"
|
|
38
38
|
],
|
|
39
|
-
"gitignore_entries": [
|
|
40
|
-
"# Ignore all build artifacts in .jac directory",
|
|
41
|
-
"*"
|
|
42
|
-
],
|
|
43
39
|
"root_gitignore_entries": [
|
|
44
40
|
"# Jac build artifacts",
|
|
45
41
|
".jac/",
|
jac_client/tests/conftest.py
CHANGED
|
@@ -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
|
|
4
|
+
1. Running bun install once per session and caching node_modules
|
|
5
5
|
2. Providing shared Vite build infrastructure
|
|
6
|
-
3. Mocking
|
|
6
|
+
3. Mocking bun install for tests that only need jac.toml manipulation
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -83,16 +83,15 @@ def _get_jac_command() -> list[str]:
|
|
|
83
83
|
return [sys.executable, "-m", "jaclang"]
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def
|
|
87
|
-
"""Get environment dict with
|
|
86
|
+
def _get_env_with_bun() -> dict[str, str]:
|
|
87
|
+
"""Get environment dict with bun in PATH."""
|
|
88
88
|
env = os.environ.copy()
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
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)
|
|
93
92
|
current_path = env.get("PATH", "")
|
|
94
|
-
if
|
|
95
|
-
env["PATH"] = f"{
|
|
93
|
+
if bun_dir not in current_path:
|
|
94
|
+
env["PATH"] = f"{bun_dir}:{current_path}"
|
|
96
95
|
return env
|
|
97
96
|
|
|
98
97
|
|
|
@@ -127,16 +126,16 @@ def reset_jac_machine(tmp_path: Path) -> Generator[None, None, None]:
|
|
|
127
126
|
Jac.loaded_modules.clear()
|
|
128
127
|
|
|
129
128
|
|
|
130
|
-
# Session-scoped cache for
|
|
131
|
-
|
|
129
|
+
# Session-scoped cache for bun installation
|
|
130
|
+
_bun_cache_dir: Path | None = None
|
|
132
131
|
|
|
133
132
|
|
|
134
133
|
def _get_minimal_jac_toml() -> str:
|
|
135
|
-
"""Get minimal jac.toml content for
|
|
134
|
+
"""Get minimal jac.toml content for bun cache setup."""
|
|
136
135
|
return """[project]
|
|
137
|
-
name = "
|
|
136
|
+
name = "bun-cache"
|
|
138
137
|
version = "0.0.1"
|
|
139
|
-
description = "Cached
|
|
138
|
+
description = "Cached bun modules"
|
|
140
139
|
entry-point = "app.jac"
|
|
141
140
|
|
|
142
141
|
[plugins.client.vite.build]
|
|
@@ -145,28 +144,28 @@ minify = false
|
|
|
145
144
|
|
|
146
145
|
|
|
147
146
|
@pytest.fixture(scope="session")
|
|
148
|
-
def
|
|
149
|
-
"""Session-scoped fixture that provides a directory with
|
|
147
|
+
def bun_cache_dir() -> Generator[Path, None, None]:
|
|
148
|
+
"""Session-scoped fixture that provides a directory with bun packages installed.
|
|
150
149
|
|
|
151
|
-
This runs
|
|
150
|
+
This runs bun install once per test session and provides the path to the
|
|
152
151
|
.jac/client/configs directory containing node_modules.
|
|
153
152
|
"""
|
|
154
|
-
global
|
|
153
|
+
global _bun_cache_dir
|
|
155
154
|
|
|
156
|
-
if
|
|
157
|
-
yield
|
|
155
|
+
if _bun_cache_dir is not None and _bun_cache_dir.exists():
|
|
156
|
+
yield _bun_cache_dir
|
|
158
157
|
return
|
|
159
158
|
|
|
160
159
|
# Create a persistent temp directory for the session
|
|
161
|
-
cache_dir = Path(tempfile.mkdtemp(prefix="
|
|
160
|
+
cache_dir = Path(tempfile.mkdtemp(prefix="jac_bun_cache_"))
|
|
162
161
|
|
|
163
162
|
# Create jac.toml
|
|
164
163
|
jac_toml = cache_dir / "jac.toml"
|
|
165
164
|
jac_toml.write_text(_get_minimal_jac_toml())
|
|
166
165
|
|
|
167
|
-
# Run jac add --npm to install packages
|
|
166
|
+
# Run jac add --npm to install packages (flag name unchanged for backward compatibility)
|
|
168
167
|
jac_cmd = _get_jac_command()
|
|
169
|
-
env =
|
|
168
|
+
env = _get_env_with_bun()
|
|
170
169
|
result = subprocess.run(
|
|
171
170
|
[*jac_cmd, "add", "--npm"],
|
|
172
171
|
cwd=cache_dir,
|
|
@@ -178,36 +177,44 @@ def npm_cache_dir() -> Generator[Path, None, None]:
|
|
|
178
177
|
if result.returncode != 0:
|
|
179
178
|
# Clean up on failure
|
|
180
179
|
shutil.rmtree(cache_dir, ignore_errors=True)
|
|
181
|
-
pytest.skip(f"Failed to set up
|
|
180
|
+
pytest.skip(f"Failed to set up bun cache: {result.stderr}")
|
|
182
181
|
|
|
183
|
-
|
|
182
|
+
_bun_cache_dir = cache_dir
|
|
184
183
|
yield cache_dir
|
|
185
184
|
|
|
186
185
|
# Cleanup after all tests complete
|
|
187
186
|
shutil.rmtree(cache_dir, ignore_errors=True)
|
|
188
187
|
|
|
189
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
|
+
|
|
190
196
|
@pytest.fixture
|
|
191
|
-
def vite_project_dir(
|
|
197
|
+
def vite_project_dir(bun_cache_dir: Path, tmp_path: Path) -> Path:
|
|
192
198
|
"""Fixture that provides a project directory with pre-installed node_modules.
|
|
193
199
|
|
|
194
|
-
This copies node_modules from the session cache instead of running
|
|
200
|
+
This copies node_modules from the session cache instead of running bun install.
|
|
195
201
|
"""
|
|
196
202
|
# Create jac.toml in the temp directory
|
|
197
203
|
jac_toml = tmp_path / "jac.toml"
|
|
198
204
|
jac_toml.write_text(_get_minimal_jac_toml())
|
|
199
205
|
|
|
200
206
|
# Copy .jac/client/configs directory (contains package.json)
|
|
201
|
-
source_configs =
|
|
207
|
+
source_configs = bun_cache_dir / ".jac" / "client" / "configs"
|
|
202
208
|
dest_configs = tmp_path / ".jac" / "client" / "configs"
|
|
203
209
|
if source_configs.exists():
|
|
204
210
|
dest_configs.parent.mkdir(parents=True, exist_ok=True)
|
|
205
211
|
shutil.copytree(source_configs, dest_configs, symlinks=True)
|
|
206
212
|
|
|
207
|
-
# Copy node_modules from
|
|
208
|
-
source_node_modules =
|
|
209
|
-
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"
|
|
210
216
|
if source_node_modules.exists():
|
|
217
|
+
dest_node_modules.parent.mkdir(parents=True, exist_ok=True)
|
|
211
218
|
shutil.copytree(source_node_modules, dest_node_modules, symlinks=True)
|
|
212
219
|
|
|
213
220
|
# Create required directories
|
|
@@ -219,7 +226,7 @@ def vite_project_dir(npm_cache_dir: Path, tmp_path: Path) -> Path:
|
|
|
219
226
|
|
|
220
227
|
|
|
221
228
|
@pytest.fixture
|
|
222
|
-
def vite_project_with_antd(
|
|
229
|
+
def vite_project_with_antd(bun_cache_dir: Path, tmp_path: Path) -> Path:
|
|
223
230
|
"""Fixture that provides a project directory with antd pre-installed."""
|
|
224
231
|
# Create jac.toml with antd dependency
|
|
225
232
|
jac_toml_content = """[project]
|
|
@@ -238,21 +245,22 @@ antd = "^6.0.0"
|
|
|
238
245
|
jac_toml.write_text(jac_toml_content)
|
|
239
246
|
|
|
240
247
|
# Copy base .jac/client/configs first for faster install
|
|
241
|
-
source_configs =
|
|
248
|
+
source_configs = bun_cache_dir / ".jac" / "client" / "configs"
|
|
242
249
|
dest_configs = tmp_path / ".jac" / "client" / "configs"
|
|
243
250
|
if source_configs.exists():
|
|
244
251
|
dest_configs.parent.mkdir(parents=True, exist_ok=True)
|
|
245
252
|
shutil.copytree(source_configs, dest_configs, symlinks=True)
|
|
246
253
|
|
|
247
|
-
# Copy base node_modules for faster install (
|
|
248
|
-
source_node_modules =
|
|
249
|
-
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"
|
|
250
257
|
if source_node_modules.exists():
|
|
258
|
+
dest_node_modules.parent.mkdir(parents=True, exist_ok=True)
|
|
251
259
|
shutil.copytree(source_node_modules, dest_node_modules, symlinks=True)
|
|
252
260
|
|
|
253
261
|
# Install antd on top (uses cached node_modules as base)
|
|
254
262
|
jac_cmd = _get_jac_command()
|
|
255
|
-
env =
|
|
263
|
+
env = _get_env_with_bun()
|
|
256
264
|
result = subprocess.run(
|
|
257
265
|
[*jac_cmd, "add", "--npm"],
|
|
258
266
|
cwd=tmp_path,
|
|
@@ -272,10 +280,10 @@ antd = "^6.0.0"
|
|
|
272
280
|
|
|
273
281
|
|
|
274
282
|
@pytest.fixture
|
|
275
|
-
def
|
|
276
|
-
"""Fixture that mocks
|
|
283
|
+
def mock_bun_install():
|
|
284
|
+
"""Fixture that mocks bun install for tests that only test jac.toml manipulation.
|
|
277
285
|
|
|
278
|
-
Use this for CLI tests (add/remove commands) that don't need actual
|
|
286
|
+
Use this for CLI tests (add/remove commands) that don't need actual packages.
|
|
279
287
|
"""
|
|
280
288
|
with patch(
|
|
281
289
|
"jac_client.plugin.src.package_installer.PackageInstaller._regenerate_and_install"
|
|
@@ -283,6 +291,13 @@ def mock_npm_install():
|
|
|
283
291
|
yield mock
|
|
284
292
|
|
|
285
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
|
+
|
|
286
301
|
@pytest.fixture
|
|
287
302
|
def cli_test_dir(tmp_path: Path) -> Path:
|
|
288
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()
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
}
|