jac-client 0.2.13__py3-none-any.whl → 0.2.15__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 (42) hide show
  1. jac_client/examples/all-in-one/components/Header.jac +1 -1
  2. jac_client/examples/all-in-one/components/ProfitOverview.jac +1 -1
  3. jac_client/examples/all-in-one/components/Summary.jac +1 -1
  4. jac_client/examples/all-in-one/components/TransactionList.jac +2 -2
  5. jac_client/examples/all-in-one/components/navigation.jac +3 -9
  6. jac_client/examples/all-in-one/context/BudgetContext.jac +1 -1
  7. jac_client/examples/all-in-one/main.jac +5 -386
  8. jac_client/examples/all-in-one/pages/(auth)/index.jac +299 -0
  9. jac_client/examples/all-in-one/pages/{nestedDemo.jac → (auth)/nested.jac} +3 -13
  10. jac_client/examples/all-in-one/pages/{loginPage.jac → (public)/login.jac} +1 -1
  11. jac_client/examples/all-in-one/pages/{signupPage.jac → (public)/signup.jac} +1 -1
  12. jac_client/examples/all-in-one/pages/{notFound.jac → [...notFound].jac} +2 -1
  13. jac_client/examples/all-in-one/pages/budget.jac +11 -0
  14. jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +1 -1
  15. jac_client/examples/all-in-one/pages/features.jac +8 -0
  16. jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +7 -7
  17. jac_client/examples/all-in-one/pages/{LandingPage.jac → landing.jac} +4 -9
  18. jac_client/examples/all-in-one/pages/layout.jac +20 -0
  19. jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +1 -1
  20. jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +1 -1
  21. jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +1 -1
  22. jac_client/plugin/client_runtime.cl.jac +4 -2
  23. jac_client/plugin/impl/client_runtime.impl.jac +12 -1
  24. jac_client/plugin/plugin_config.jac +4 -11
  25. jac_client/plugin/src/compiler.jac +15 -1
  26. jac_client/plugin/src/impl/compiler.impl.jac +216 -23
  27. jac_client/plugin/src/impl/package_installer.impl.jac +3 -2
  28. jac_client/plugin/src/impl/route_scanner.impl.jac +201 -0
  29. jac_client/plugin/src/impl/vite_bundler.impl.jac +15 -11
  30. jac_client/plugin/src/route_scanner.jac +44 -0
  31. jac_client/plugin/utils/impl/bun_installer.impl.jac +16 -19
  32. jac_client/plugin/utils/impl/client_deps.impl.jac +12 -16
  33. jac_client/templates/fullstack.jacpack +3 -2
  34. jac_client/tests/test_e2e.py +19 -28
  35. jac_client/tests/test_it.py +247 -0
  36. {jac_client-0.2.13.dist-info → jac_client-0.2.15.dist-info}/METADATA +2 -2
  37. {jac_client-0.2.13.dist-info → jac_client-0.2.15.dist-info}/RECORD +40 -36
  38. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +0 -140
  39. jac_client/examples/all-in-one/pages/FeaturesTest.jac +0 -157
  40. {jac_client-0.2.13.dist-info → jac_client-0.2.15.dist-info}/WHEEL +0 -0
  41. {jac_client-0.2.13.dist-info → jac_client-0.2.15.dist-info}/entry_points.txt +0 -0
  42. {jac_client-0.2.13.dist-info → jac_client-0.2.15.dist-info}/top_level.txt +0 -0
@@ -27,22 +27,22 @@ impl prompt_install_bun -> bool {
27
27
  import subprocess;
28
28
  import sys;
29
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
30
+ import from jaclang.cli.console { console }
31
+ console.error("\n ⚠ Bun is required but not installed.");
32
+ console.error(
33
+ " Bun is a fast JavaScript runtime used for package management and bundling."
34
34
  );
35
- print(" Learn more: https://bun.sh\n", file=sys.stderr);
35
+ console.error(" Learn more: https://bun.sh\n");
36
36
  try {
37
37
  response = input(" Install Bun now? [Y/n]: ").strip().lower();
38
38
  } except (EOFError, KeyboardInterrupt) {
39
- print("", file=sys.stderr);
39
+ console.error("");
40
40
  return False;
41
41
  }
42
42
  if response and response not in ('y', 'yes', '') {
43
43
  return False;
44
44
  }
45
- print("\n ⏳ Installing Bun...", flush=True);
45
+ console.print("\n ⏳ Installing Bun...");
46
46
  try {
47
47
  # Check if curl is available
48
48
  subprocess.run(
@@ -57,7 +57,7 @@ impl prompt_install_bun -> bool {
57
57
  );
58
58
 
59
59
  if result.returncode == 0 {
60
- print(" ✔ Bun installed successfully!", flush=True);
60
+ console.print(" ✔ Bun installed successfully!");
61
61
  # Add Bun to PATH for current session
62
62
  bun_bin = os.path.expanduser("~/.bun/bin");
63
63
  if os.path.exists(bun_bin) {
@@ -69,31 +69,28 @@ impl prompt_install_bun -> bool {
69
69
  ["bun", "--version"], capture_output=True, text=True
70
70
  );
71
71
  if verify.returncode == 0 {
72
- print(f" ✔ Verified: Bun {verify.stdout.strip()}", flush=True);
72
+ console.print(f" ✔ Verified: Bun {verify.stdout.strip()}");
73
73
  return True;
74
74
  }
75
75
  }
76
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);
77
+ console.error(" ⚠ Bun installed but may require terminal restart.");
78
+ console.error(" Run: source ~/.bashrc (or restart terminal)");
81
79
  return True;
82
80
  } else {
83
- print(" ✖ Bun installation failed.", file=sys.stderr);
81
+ console.error(" ✖ Bun installation failed.");
84
82
  return False;
85
83
  }
86
84
  } except subprocess.TimeoutExpired {
87
- print(" ✖ Installation timed out.", file=sys.stderr);
85
+ console.error(" ✖ Installation timed out.");
88
86
  return False;
89
87
  } except FileNotFoundError {
90
- print(
91
- " ✖ curl not found. Please install Bun manually: https://bun.sh",
92
- file=sys.stderr
88
+ console.error(
89
+ " ✖ curl not found. Please install Bun manually: https://bun.sh"
93
90
  );
94
91
  return False;
95
92
  } except Exception as e {
96
- print(f" ✖ Installation failed: {e}", file=sys.stderr);
93
+ console.error(f" ✖ Installation failed: {e}");
97
94
  return False;
98
95
  }
99
96
  }
@@ -16,27 +16,23 @@ impl ensure_client_deps(config_loader: object) -> bool {
16
16
  """Prompt user to install default jac-client npm dependencies."""
17
17
  def prompt_install_client_deps(config_loader: object) -> bool {
18
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
19
+ import from jaclang.cli.console { console }
20
+ console.error("\n ⚠ Client dependencies are not configured.");
21
+ console.error(
22
+ " jac-client requires npm packages (react, vite, etc.) to build client pages."
23
23
  );
24
- print(" These will be added to your jac.toml file.\n", file=sys.stderr);
24
+ console.error(" These will be added to your jac.toml file.\n");
25
25
  try {
26
26
  response = input(" Install default client dependencies now? [Y/n]: ").strip().lower();
27
27
  } except (EOFError, KeyboardInterrupt) {
28
- print("", file=sys.stderr);
28
+ console.error("");
29
29
  return False;
30
30
  }
31
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
32
+ console.error(
33
+ "\n To configure manually, add [dependencies.npm] to your jac.toml."
39
34
  );
35
+ console.error(" Or create a new project with: jac create --use client\n");
40
36
  return False;
41
37
  }
42
38
  # Default runtime dependencies
@@ -54,7 +50,7 @@ def prompt_install_client_deps(config_loader: object) -> bool {
54
50
  '@types/react': '^18.2.0',
55
51
  '@types/react-dom': '^18.2.0'
56
52
  };
57
- print("\n ⏳ Adding default client dependencies to jac.toml...", flush=True);
53
+ console.print("\n ⏳ Adding default client dependencies to jac.toml...");
58
54
  try {
59
55
  for (name, version) in default_deps.items() {
60
56
  config_loader.add_dependency(name, version, is_dev=False);
@@ -64,10 +60,10 @@ def prompt_install_client_deps(config_loader: object) -> bool {
64
60
  }
65
61
  config_loader.save();
66
62
  config_loader.invalidate();
67
- print(" ✔ Default client dependencies added to jac.toml\n", flush=True);
63
+ console.print(" ✔ Default client dependencies added to jac.toml\n");
68
64
  return True;
69
65
  } except Exception as e {
70
- print(f" ✖ Failed to add dependencies: {e}", file=sys.stderr);
66
+ console.error(f" ✖ Failed to add dependencies: {e}");
71
67
  return False;
72
68
  }
73
69
  }
@@ -26,11 +26,12 @@
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/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",
29
+ "frontend.cl.jac": "\"\"\"Todo App - Client-Side UI.\"\"\"\n\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 can with entry {\n isLoggedIn = jacIsLoggedIn();\n checkingAuth = False;\n }\n\n can with [isLoggedIn] entry {\n if isLoggedIn {\n fetchTodos();\n }\n }\n\n async def fetchTodos -> None;\n async def addTodo -> None;\n async def toggleTodo(todoId: str) -> None;\n async def deleteTodo(todoId: str) -> None;\n async def handleLogin -> None;\n async def handleSignup -> None;\n def handleLogout -> None;\n async def handleSubmit(e: any) -> None;\n def handleTodoKeyPress(e: any) -> None;\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}{[\n <TodoItem\n key={todo.id}\n todo={todo}\n onToggle={toggleTodo}\n onDelete={deleteTodo}\n /> for todo in todos\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
+ "frontend.impl.jac": "\"\"\"Implementations for the Todo App frontend component.\"\"\"\n\nimpl app.fetchTodos -> None {\n todosLoading = True;\n result = root spawn ListTodos();\n todos = result.reports[0] if result.reports else [];\n todosLoading = False;\n}\n\nimpl app.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\nimpl app.toggleTodo(todoId: str) -> None {\n root spawn ToggleTodo(todo_id=todoId);\n todos = todos.map(\n lambda t: any -> any {\n if t.id == todoId {\n return {\"id\": t.id, \"title\": t.title, \"completed\": not t.completed};\n }\n return t;\n }\n );\n}\n\nimpl app.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\nimpl app.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\nimpl app.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\nimpl app.handleLogout -> None {\n jacLogout();\n isLoggedIn = False;\n todos = [];\n username = \"\";\n password = \"\";\n error = \"\";\n}\n\nimpl app.handleSubmit(e: any) -> None {\n e.preventDefault();\n if isSignup {\n await handleSignup();\n } else {\n await handleLogin();\n }\n}\n\nimpl app.handleTodoKeyPress(e: any) -> None {\n if e.key == \"Enter\" {\n addTodo();\n }\n}\n",
30
31
  "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
32
  "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
33
  "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
+ "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 (declarations + JSX)\n\u251c\u2500\u2500 frontend.impl.jac # Client-side function implementations\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 state declarations and JSX templates\n- **Implementations (`.impl.jac`)**: Separated function bodies for clean code organization\n- **Entry Point (`main.jac`)**: Combines server and client imports\n\n### Jac Patterns Used\n\n- **`can with entry`**: Lifecycle effects that replace React's `useEffect`\n- **JSX Comprehensions**: `{[<Component /> for item in items]}` instead of `.map()`\n- **Impl Separation**: Declarations in `.cl.jac`, implementations in `.impl.jac`\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
  },
35
36
  "directories": [
36
37
  ".jac",
@@ -4,10 +4,9 @@ from __future__ import annotations
4
4
 
5
5
  import gc
6
6
  import os
7
- import shutil
8
7
  import tempfile
9
8
  import time
10
- from subprocess import PIPE, Popen, run
9
+ from subprocess import Popen, run
11
10
 
12
11
  import pytest
13
12
 
@@ -27,6 +26,8 @@ from .test_helpers import (
27
26
  def running_server():
28
27
  """Start the all-in-one jac server for the test module and yield its URL.
29
28
 
29
+ Uses jacpack to bundle and extract the all-in-one example.
30
+
30
31
  Yields a dict with keys `port` and `url`.
31
32
  """
32
33
  tests_dir = os.path.dirname(__file__)
@@ -44,43 +45,33 @@ def running_server():
44
45
  original_cwd = os.getcwd()
45
46
  try:
46
47
  os.chdir(temp_dir)
47
- process = Popen(
48
- [*jac_cmd, "create", "--use", "client", app_name],
49
- stdin=PIPE,
50
- stdout=PIPE,
51
- stderr=PIPE,
48
+
49
+ # Create jacpack file from all-in-one example
50
+ jacpack_path = os.path.join(temp_dir, "all-in-one.jacpack")
51
+ pack_result = run(
52
+ [*jac_cmd, "jacpack", "pack", all_in_one_path, "-o", jacpack_path],
53
+ capture_output=True,
52
54
  text=True,
53
55
  env=env,
54
56
  )
55
- stdout, stderr = process.communicate()
56
- if process.returncode != 0:
57
- pytest.fail(f"jac create --use client failed: {stderr}")
58
-
59
- project_path = os.path.join(temp_dir, app_name)
57
+ if pack_result.returncode != 0:
58
+ pytest.fail(f"jac jacpack pack failed: {pack_result.stderr}")
60
59
 
61
- for entry in os.listdir(all_in_one_path):
62
- if entry in {"node_modules", "build", "dist", ".pytest_cache"}:
63
- continue
64
- src = os.path.join(all_in_one_path, entry)
65
- dst = os.path.join(project_path, entry)
66
- if os.path.isdir(src):
67
- shutil.copytree(src, dst, dirs_exist_ok=True)
68
- else:
69
- shutil.copy2(src, dst)
70
-
71
- jac_add_result = run(
72
- [*jac_cmd, "add", "--npm"],
73
- cwd=project_path,
60
+ # Create project from jacpack file
61
+ create_result = run(
62
+ [*jac_cmd, "create", app_name, "--use", jacpack_path],
74
63
  capture_output=True,
75
64
  text=True,
76
65
  env=env,
77
66
  )
78
- if jac_add_result.returncode != 0:
79
- pytest.fail(f"jac add --npm failed: {jac_add_result.stderr}")
67
+ if create_result.returncode != 0:
68
+ pytest.fail(f"jac create --use jacpack failed: {create_result.stderr}")
69
+
70
+ project_path = os.path.join(temp_dir, app_name)
80
71
 
81
72
  server_port = get_free_port()
82
73
  server = Popen(
83
- [*jac_cmd, "start", "main.jac", "-p", str(server_port)],
74
+ [*jac_cmd, "start", "-p", str(server_port)],
84
75
  cwd=project_path,
85
76
  env=env,
86
77
  )
@@ -976,3 +976,250 @@ def test_configurable_api_base_url_in_bundle() -> None:
976
976
  print(f"[DEBUG] Restoring working directory to {original_cwd}")
977
977
  os.chdir(original_cwd)
978
978
  gc.collect()
979
+
980
+
981
+ def _setup_all_in_one_project(temp_dir: str, app_name: str) -> str:
982
+ """Shared helper: scaffold a jac client app, copy all-in-one into it, install npm deps.
983
+
984
+ Returns the project directory path.
985
+ """
986
+ tests_dir = os.path.dirname(__file__)
987
+ jac_client_root = os.path.dirname(tests_dir)
988
+ all_in_one_path = os.path.join(jac_client_root, "examples", "all-in-one")
989
+
990
+ assert os.path.isdir(all_in_one_path), "all-in-one example directory missing"
991
+
992
+ jac_cmd = get_jac_command()
993
+ env = get_env_with_npm()
994
+
995
+ # Create a new Jac client app
996
+ process = Popen(
997
+ [*jac_cmd, "create", "--use", "client", app_name],
998
+ stdin=PIPE,
999
+ stdout=PIPE,
1000
+ stderr=PIPE,
1001
+ text=True,
1002
+ env=env,
1003
+ )
1004
+ stdout, stderr = process.communicate()
1005
+ if process.returncode != 0 and "unrecognized arguments: --use" in stderr:
1006
+ pytest.fail(
1007
+ "Test failed: installed `jac` CLI does not support `create --use client`."
1008
+ )
1009
+ assert process.returncode == 0, (
1010
+ f"jac create --use client failed\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}\n"
1011
+ )
1012
+
1013
+ project_path = os.path.join(temp_dir, app_name)
1014
+ assert os.path.isdir(project_path)
1015
+
1016
+ # Copy all-in-one contents (skip build artifacts)
1017
+ for entry in os.listdir(all_in_one_path):
1018
+ src = os.path.join(all_in_one_path, entry)
1019
+ dst = os.path.join(project_path, entry)
1020
+ if entry in {"node_modules", "build", "dist", ".pytest_cache"}:
1021
+ continue
1022
+ if os.path.isdir(src):
1023
+ shutil.copytree(src, dst, dirs_exist_ok=True)
1024
+ else:
1025
+ shutil.copy2(src, dst)
1026
+
1027
+ # Install npm packages
1028
+ jac_add_result = run(
1029
+ [*jac_cmd, "add", "--npm"],
1030
+ cwd=project_path,
1031
+ capture_output=True,
1032
+ text=True,
1033
+ env=env,
1034
+ )
1035
+ if jac_add_result.returncode != 0:
1036
+ pytest.fail(
1037
+ f"jac add --npm failed\nSTDOUT:\n{jac_add_result.stdout}\n"
1038
+ f"STDERR:\n{jac_add_result.stderr}\n"
1039
+ )
1040
+
1041
+ return project_path
1042
+
1043
+
1044
+ def test_profile_config_applies_to_server() -> None:
1045
+ """Verify that ``--profile prod`` loads jac.prod.toml and its settings take effect.
1046
+
1047
+ The prod profile overrides ``[plugins.client.app_meta_data] title``.
1048
+ We start the server with ``--profile prod`` and confirm the HTML ``<title>``
1049
+ reflects the prod value, proving the profile overlay pipeline works end-to-end.
1050
+ """
1051
+ print("[DEBUG] Starting test_profile_config_applies_to_server")
1052
+
1053
+ prod_title = "All-In-One Prod"
1054
+ base_title = "All-In-One"
1055
+ app_name = "e2e-profile-test"
1056
+
1057
+ with tempfile.TemporaryDirectory() as temp_dir:
1058
+ print(f"[DEBUG] Created temporary directory at {temp_dir}")
1059
+ original_cwd = os.getcwd()
1060
+ try:
1061
+ os.chdir(temp_dir)
1062
+
1063
+ project_path = _setup_all_in_one_project(temp_dir, app_name)
1064
+ print(f"[DEBUG] Project set up at {project_path}")
1065
+
1066
+ prod_toml = os.path.join(project_path, "jac.prod.toml")
1067
+ assert os.path.isfile(prod_toml), (
1068
+ "jac.prod.toml should be copied from all-in-one example"
1069
+ )
1070
+
1071
+ server: Popen[bytes] | None = None
1072
+ server_port = get_free_port()
1073
+ jac_cmd = get_jac_command()
1074
+ env = get_env_with_npm()
1075
+ try:
1076
+ print(
1077
+ f"[DEBUG] Starting server with "
1078
+ f"'jac start main.jac -p {server_port} --profile prod'"
1079
+ )
1080
+ server = Popen(
1081
+ [
1082
+ *jac_cmd,
1083
+ "start",
1084
+ "main.jac",
1085
+ "-p",
1086
+ str(server_port),
1087
+ "--profile",
1088
+ "prod",
1089
+ ],
1090
+ cwd=project_path,
1091
+ env=env,
1092
+ )
1093
+
1094
+ print(f"[DEBUG] Waiting for server on 127.0.0.1:{server_port}")
1095
+ wait_for_port("127.0.0.1", server_port, timeout=90.0)
1096
+ print(
1097
+ f"[DEBUG] Server accepting connections on 127.0.0.1:{server_port}"
1098
+ )
1099
+
1100
+ root_bytes = _wait_for_endpoint(
1101
+ f"http://127.0.0.1:{server_port}",
1102
+ timeout=120.0,
1103
+ poll_interval=2.0,
1104
+ request_timeout=30.0,
1105
+ )
1106
+ root_body = root_bytes.decode("utf-8", errors="ignore")
1107
+ print(f"[DEBUG] Root response (truncated):\n{root_body[:500]}")
1108
+ assert "<html" in root_body.lower(), "Root should return HTML"
1109
+
1110
+ assert f"<title>{prod_title}</title>" in root_body, (
1111
+ f"Expected prod title '{prod_title}' in HTML, "
1112
+ f"but found base title instead. "
1113
+ f"This means --profile prod did not load jac.prod.toml correctly.\n"
1114
+ f"HTML (first 500 chars): {root_body[:500]}"
1115
+ )
1116
+ assert f"<title>{base_title}</title>" not in root_body, (
1117
+ "Base title should be overridden by prod profile"
1118
+ )
1119
+ print(
1120
+ f"[DEBUG] Confirmed title='{prod_title}' in HTML "
1121
+ f"- profile config applied successfully"
1122
+ )
1123
+
1124
+ finally:
1125
+ if server is not None:
1126
+ print("[DEBUG] Terminating server process")
1127
+ server.terminate()
1128
+ try:
1129
+ server.wait(timeout=15)
1130
+ except Exception:
1131
+ server.kill()
1132
+ server.wait(timeout=5)
1133
+ time.sleep(1)
1134
+ gc.collect()
1135
+
1136
+ finally:
1137
+ os.chdir(original_cwd)
1138
+ gc.collect()
1139
+
1140
+
1141
+ def test_no_profile_omits_profile_settings() -> None:
1142
+ """Verify that without ``--profile``, prod-only settings are NOT applied.
1143
+
1144
+ Starts the server without any profile flag and confirms the HTML
1145
+ ``<title>`` uses the base config value, not the prod override.
1146
+ This is the control test for ``test_profile_config_applies_to_server``.
1147
+ """
1148
+ print("[DEBUG] Starting test_no_profile_omits_profile_settings")
1149
+
1150
+ prod_title = "All-In-One Prod"
1151
+ base_title = "All-In-One"
1152
+ app_name = "e2e-no-profile-test"
1153
+
1154
+ with tempfile.TemporaryDirectory() as temp_dir:
1155
+ print(f"[DEBUG] Created temporary directory at {temp_dir}")
1156
+ original_cwd = os.getcwd()
1157
+ try:
1158
+ os.chdir(temp_dir)
1159
+
1160
+ project_path = _setup_all_in_one_project(temp_dir, app_name)
1161
+ print(f"[DEBUG] Project set up at {project_path}")
1162
+
1163
+ local_toml = os.path.join(project_path, "jac.local.toml")
1164
+ if os.path.isfile(local_toml):
1165
+ os.remove(local_toml)
1166
+
1167
+ server: Popen[bytes] | None = None
1168
+ server_port = get_free_port()
1169
+ jac_cmd = get_jac_command()
1170
+ env = get_env_with_npm()
1171
+ try:
1172
+ print(
1173
+ f"[DEBUG] Starting server with "
1174
+ f"'jac start main.jac -p {server_port}' (no profile)"
1175
+ )
1176
+ server = Popen(
1177
+ [*jac_cmd, "start", "main.jac", "-p", str(server_port)],
1178
+ cwd=project_path,
1179
+ env=env,
1180
+ )
1181
+
1182
+ print(f"[DEBUG] Waiting for server on 127.0.0.1:{server_port}")
1183
+ wait_for_port("127.0.0.1", server_port, timeout=90.0)
1184
+ print(
1185
+ f"[DEBUG] Server accepting connections on 127.0.0.1:{server_port}"
1186
+ )
1187
+
1188
+ root_bytes = _wait_for_endpoint(
1189
+ f"http://127.0.0.1:{server_port}",
1190
+ timeout=120.0,
1191
+ poll_interval=2.0,
1192
+ request_timeout=30.0,
1193
+ )
1194
+ root_body = root_bytes.decode("utf-8", errors="ignore")
1195
+ assert "<html" in root_body.lower()
1196
+
1197
+ assert f"<title>{base_title}</title>" in root_body, (
1198
+ f"Expected base title '{base_title}' in HTML when no profile is set.\n"
1199
+ f"HTML (first 500 chars): {root_body[:500]}"
1200
+ )
1201
+ assert f"<title>{prod_title}</title>" not in root_body, (
1202
+ f"Prod title '{prod_title}' should NOT appear "
1203
+ f"when no profile is specified. "
1204
+ f"Profile settings are leaking without --profile."
1205
+ )
1206
+ print(
1207
+ f"[DEBUG] Confirmed title='{base_title}' in HTML "
1208
+ f"- profile settings correctly isolated"
1209
+ )
1210
+
1211
+ finally:
1212
+ if server is not None:
1213
+ print("[DEBUG] Terminating server process")
1214
+ server.terminate()
1215
+ try:
1216
+ server.wait(timeout=15)
1217
+ except Exception:
1218
+ server.kill()
1219
+ server.wait(timeout=5)
1220
+ time.sleep(1)
1221
+ gc.collect()
1222
+
1223
+ finally:
1224
+ os.chdir(original_cwd)
1225
+ gc.collect()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jac-client
3
- Version: 0.2.13
3
+ Version: 0.2.15
4
4
  Summary: Build full-stack web applications with Jac - one language for frontend and backend.
5
5
  Author-email: Jason Mars <jason@mars.ninja>
6
6
  Maintainer-email: Jason Mars <jason@mars.ninja>
@@ -11,7 +11,7 @@ Project-URL: Documentation, https://jac-lang.org
11
11
  Keywords: jac,jaclang,jaseci,frontend,full-stack,web-development
12
12
  Requires-Python: >=3.12
13
13
  Description-Content-Type: text/markdown
14
- Requires-Dist: jaclang>=0.9.13
14
+ Requires-Dist: jaclang>=0.9.15
15
15
  Provides-Extra: dev
16
16
  Requires-Dist: python-dotenv==1.0.1; extra == "dev"
17
17
  Requires-Dist: pytest==8.3.5; extra == "dev"