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.
- jac_client/examples/all-in-one/components/Header.jac +1 -1
- jac_client/examples/all-in-one/components/ProfitOverview.jac +1 -1
- jac_client/examples/all-in-one/components/Summary.jac +1 -1
- jac_client/examples/all-in-one/components/TransactionList.jac +2 -2
- jac_client/examples/all-in-one/components/navigation.jac +3 -9
- jac_client/examples/all-in-one/context/BudgetContext.jac +1 -1
- jac_client/examples/all-in-one/main.jac +5 -386
- jac_client/examples/all-in-one/pages/(auth)/index.jac +299 -0
- jac_client/examples/all-in-one/pages/{nestedDemo.jac → (auth)/nested.jac} +3 -13
- jac_client/examples/all-in-one/pages/{loginPage.jac → (public)/login.jac} +1 -1
- jac_client/examples/all-in-one/pages/{signupPage.jac → (public)/signup.jac} +1 -1
- jac_client/examples/all-in-one/pages/{notFound.jac → [...notFound].jac} +2 -1
- jac_client/examples/all-in-one/pages/budget.jac +11 -0
- jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +1 -1
- jac_client/examples/all-in-one/pages/features.jac +8 -0
- jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +7 -7
- jac_client/examples/all-in-one/pages/{LandingPage.jac → landing.jac} +4 -9
- jac_client/examples/all-in-one/pages/layout.jac +20 -0
- jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +1 -1
- jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +1 -1
- jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +1 -1
- jac_client/plugin/client_runtime.cl.jac +4 -2
- jac_client/plugin/impl/client_runtime.impl.jac +12 -1
- jac_client/plugin/plugin_config.jac +4 -11
- jac_client/plugin/src/compiler.jac +15 -1
- jac_client/plugin/src/impl/compiler.impl.jac +216 -23
- jac_client/plugin/src/impl/package_installer.impl.jac +3 -2
- jac_client/plugin/src/impl/route_scanner.impl.jac +201 -0
- jac_client/plugin/src/impl/vite_bundler.impl.jac +15 -11
- jac_client/plugin/src/route_scanner.jac +44 -0
- jac_client/plugin/utils/impl/bun_installer.impl.jac +16 -19
- jac_client/plugin/utils/impl/client_deps.impl.jac +12 -16
- jac_client/templates/fullstack.jacpack +3 -2
- jac_client/tests/test_e2e.py +19 -28
- jac_client/tests/test_it.py +247 -0
- {jac_client-0.2.13.dist-info → jac_client-0.2.15.dist-info}/METADATA +2 -2
- {jac_client-0.2.13.dist-info → jac_client-0.2.15.dist-info}/RECORD +40 -36
- jac_client/examples/all-in-one/pages/BudgetPlanner.jac +0 -140
- jac_client/examples/all-in-one/pages/FeaturesTest.jac +0 -157
- {jac_client-0.2.13.dist-info → jac_client-0.2.15.dist-info}/WHEEL +0 -0
- {jac_client-0.2.13.dist-info → jac_client-0.2.15.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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..."
|
|
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!"
|
|
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()}"
|
|
72
|
+
console.print(f" ✔ Verified: Bun {verify.stdout.strip()}");
|
|
73
73
|
return True;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
+
console.error(" ✖ Bun installation failed.");
|
|
84
82
|
return False;
|
|
85
83
|
}
|
|
86
84
|
} except subprocess.TimeoutExpired {
|
|
87
|
-
|
|
85
|
+
console.error(" ✖ Installation timed out.");
|
|
88
86
|
return False;
|
|
89
87
|
} except FileNotFoundError {
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
console.error("");
|
|
29
29
|
return False;
|
|
30
30
|
}
|
|
31
31
|
if response and response not in ('y', 'yes', '') {
|
|
32
|
-
|
|
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..."
|
|
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"
|
|
63
|
+
console.print(" ✔ Default client dependencies added to jac.toml\n");
|
|
68
64
|
return True;
|
|
69
65
|
} except Exception as e {
|
|
70
|
-
|
|
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
|
|
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",
|
jac_client/tests/test_e2e.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
79
|
-
pytest.fail(f"jac
|
|
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", "
|
|
74
|
+
[*jac_cmd, "start", "-p", str(server_port)],
|
|
84
75
|
cwd=project_path,
|
|
85
76
|
env=env,
|
|
86
77
|
)
|
jac_client/tests/test_it.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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"
|