create-caspian-app 0.8.0-rc.2 → 1.0.0
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.
- package/README.md +523 -202
- package/dist/.github/copilot-instructions.md +15 -1
- package/dist/AGENTS.md +11 -1
- package/dist/index.js +1 -1
- package/dist/settings/browser_log.py +498 -0
- package/dist/settings/bs-config.json +6 -6
- package/dist/settings/bs-config.ts +20 -1
- package/dist/settings/check.py +17 -1
- package/dist/settings/dev-log-bridge.ts +332 -21
- package/dist/tests/README.md +33 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# Caspian — The Native Python Web Framework for the Reactive Web
|
|
2
2
|
|
|
3
|
-
Caspian is a
|
|
3
|
+
Caspian is a FastAPI-powered full-stack framework that brings reactive UI to Python without forcing a JavaScript backend. You write file-system routes, plain HTML templates, and `async def` Python — Caspian wires the browser to your server.
|
|
4
4
|
|
|
5
|
-
- **FastAPI
|
|
6
|
-
- **
|
|
7
|
-
- **
|
|
8
|
-
- **File-system routing** with nested layouts and
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **Session
|
|
5
|
+
- **FastAPI engine** — async-native, with the Starlette/FastAPI middleware and ecosystem underneath
|
|
6
|
+
- **PulsePoint** — a shipped browser-side reactive runtime with a React-style hook API and **plain HTML** templates (no JSX, no build step required)
|
|
7
|
+
- **"Zero-API" RPC** — call Python functions from the browser with `pp.rpc()`; no controllers, no route handlers, no fetch boilerplate
|
|
8
|
+
- **File-system routing** with nested layouts, dynamic segments, and route groups (Next.js App Router mental model)
|
|
9
|
+
- **Python components** — reusable `@component` functions rendered as HTML-first `x-*` tags
|
|
10
|
+
- **Prisma ORM** with a generated, type-safe Python client
|
|
11
|
+
- **Session auth** with RBAC and OAuth providers, plus fail-closed security defaults
|
|
12
|
+
- **Optional**: Tailwind CSS, TypeScript tooling, MCP server, WebSockets — each gated by one config flag
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
@@ -16,183 +17,288 @@ Caspian is a high-performance, FastAPI-powered full-stack framework that brings
|
|
|
16
17
|
|
|
17
18
|
### Requirements
|
|
18
19
|
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
20
|
+
- **Python** `3.14` or newer
|
|
21
|
+
- **Node.js** with `npm` and `npx` available (used for the CLI, Prisma, Tailwind, and the dev stack)
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
```bash
|
|
24
|
+
node -v
|
|
25
|
+
python -V
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Create an app
|
|
23
29
|
|
|
24
30
|
```bash
|
|
25
31
|
npx create-caspian-app@latest
|
|
26
32
|
```
|
|
27
33
|
|
|
28
|
-
The wizard walks through
|
|
34
|
+
The interactive wizard walks through:
|
|
29
35
|
|
|
30
36
|
- Project name
|
|
31
|
-
- Feature toggles: backend-only
|
|
32
|
-
- Starter kit selection (basic
|
|
37
|
+
- Feature toggles: backend-only, Tailwind CSS, Prisma, MCP, TypeScript
|
|
38
|
+
- Starter kit selection (`basic`, `fullstack`, `api`, `realtime`, or a custom Git source)
|
|
33
39
|
|
|
34
|
-
### Run dev server
|
|
40
|
+
### Run the dev server
|
|
35
41
|
|
|
36
42
|
```bash
|
|
37
43
|
cd my-app
|
|
38
44
|
npm run dev
|
|
39
45
|
```
|
|
40
46
|
|
|
41
|
-
>
|
|
47
|
+
> The generated `package.json` is the source of truth for what `npm run dev` does. Caspian projects typically run a **BrowserSync proxy plus PostCSS/Vite watchers**, not a Vite dev server that owns the page. When the dev stack is running, check `settings/bs-config.json` for the active local URL and port — the proxy does not always land on the default port.
|
|
42
48
|
|
|
43
49
|
---
|
|
44
50
|
|
|
45
51
|
## What "Reactive Python" looks like
|
|
46
52
|
|
|
47
|
-
A
|
|
53
|
+
A route is a folder. The markup lives in `index.html`, the server logic in `index.py`. Templates are **plain HTML** with `{expression}` interpolation and a small `<script>` for state.
|
|
48
54
|
|
|
49
|
-
### Route template
|
|
55
|
+
### Route template — `src/app/todos/index.html`
|
|
50
56
|
|
|
51
57
|
```html
|
|
52
|
-
<!-- @import { Badge } from "
|
|
58
|
+
<!-- @import { Badge } from "../../components/ui/Badge.py" -->
|
|
53
59
|
|
|
54
60
|
<section>
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
<x-badge variant="default">Tasks: {todos.length}</x-badge>
|
|
62
|
+
|
|
63
|
+
<form onsubmit="{addTodo(event)}">
|
|
64
|
+
<input name="title" required />
|
|
65
|
+
<button type="submit" disabled="{isSaving}">Add</button>
|
|
66
|
+
</form>
|
|
58
67
|
|
|
59
68
|
<ul>
|
|
60
69
|
<template pp-for="(todo, index) in todos">
|
|
61
70
|
<li key="{todo.id}" class="p-2 border-b">
|
|
62
71
|
{index + 1}. {todo.title}
|
|
63
|
-
<button onclick="removeTodo(todo.id)">Remove</button>
|
|
72
|
+
<button onclick="{removeTodo(todo.id)}">Remove</button>
|
|
64
73
|
</li>
|
|
65
74
|
</template>
|
|
66
75
|
</ul>
|
|
67
76
|
|
|
77
|
+
<p hidden="{todos.length > 0}">Nothing here yet.</p>
|
|
78
|
+
|
|
68
79
|
<script>
|
|
69
80
|
const [todos, setTodos] = pp.state([]);
|
|
70
|
-
|
|
81
|
+
const [isSaving, setIsSaving] = pp.state(false);
|
|
82
|
+
|
|
83
|
+
pp.effect(() => {
|
|
84
|
+
pp.rpc("list_todos").then(setTodos);
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
async function addTodo(event) {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
setIsSaving(true);
|
|
90
|
+
try {
|
|
91
|
+
const data = Object.fromEntries(
|
|
92
|
+
new FormData(event.currentTarget).entries(),
|
|
93
|
+
);
|
|
94
|
+
const created = await pp.rpc("create_todo", data);
|
|
95
|
+
setTodos([created, ...todos]);
|
|
96
|
+
event.currentTarget.reset();
|
|
97
|
+
} finally {
|
|
98
|
+
setIsSaving(false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function removeTodo(id) {
|
|
103
|
+
await pp.rpc("delete_todo", { id });
|
|
71
104
|
setTodos(todos.filter((todo) => todo.id !== id));
|
|
72
105
|
}
|
|
73
106
|
</script>
|
|
74
107
|
</section>
|
|
75
108
|
```
|
|
76
109
|
|
|
77
|
-
### Backend
|
|
110
|
+
### Backend — `src/app/todos/index.py`
|
|
78
111
|
|
|
79
112
|
```python
|
|
113
|
+
from casp.layout import Metadata, render_page
|
|
80
114
|
from casp.rpc import rpc
|
|
81
|
-
from casp.validate import Validate
|
|
82
|
-
from src.lib.prisma
|
|
115
|
+
from casp.validate import Rule, Validate
|
|
116
|
+
from src.lib.prisma import prisma
|
|
117
|
+
|
|
118
|
+
metadata = Metadata(
|
|
119
|
+
title="Todos",
|
|
120
|
+
description="A tiny Caspian todo list.",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def page():
|
|
125
|
+
return render_page(__file__)
|
|
126
|
+
|
|
83
127
|
|
|
84
128
|
@rpc()
|
|
85
|
-
async def
|
|
86
|
-
|
|
87
|
-
|
|
129
|
+
async def list_todos():
|
|
130
|
+
todos = await prisma.todo.find_many(order_by={"id": "desc"})
|
|
131
|
+
return [todo.to_dict() for todo in todos]
|
|
88
132
|
|
|
89
|
-
new_todo = await prisma.todo.create(data={
|
|
90
|
-
"title": title,
|
|
91
|
-
"completed": False
|
|
92
|
-
})
|
|
93
|
-
return new_todo.to_dict()
|
|
94
133
|
|
|
95
|
-
@rpc(
|
|
96
|
-
async def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
134
|
+
@rpc()
|
|
135
|
+
async def create_todo(title: str):
|
|
136
|
+
checked = Validate.with_rules(title, [Rule.REQUIRED, Rule.min(3)])
|
|
137
|
+
if checked is not True:
|
|
138
|
+
raise ValueError("Title must be at least 3 characters.")
|
|
100
139
|
|
|
101
|
-
|
|
140
|
+
todo = await prisma.todo.create(data={"title": title.strip(), "completed": False})
|
|
141
|
+
return todo.to_dict()
|
|
102
142
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const newTodo = await pp.rpc("create_todo", data);
|
|
109
|
-
setTodos([newTodo, ...todos]);
|
|
110
|
-
e.target.reset();
|
|
111
|
-
}
|
|
112
|
-
</script>
|
|
143
|
+
|
|
144
|
+
@rpc(require_auth=True)
|
|
145
|
+
async def delete_todo(id: int):
|
|
146
|
+
await prisma.todo.delete(where={"id": int(id)})
|
|
147
|
+
return {"deleted": True}
|
|
113
148
|
```
|
|
114
149
|
|
|
150
|
+
That is the whole loop: no API routes, no client, no serializer. `pp.rpc("create_todo", data)` posts to the current route, Caspian resolves the decorated function, filters the payload against its signature, runs it, and returns JSON.
|
|
151
|
+
|
|
115
152
|
---
|
|
116
153
|
|
|
117
|
-
##
|
|
154
|
+
## Core concepts
|
|
118
155
|
|
|
119
|
-
###
|
|
156
|
+
### 1. Routing
|
|
120
157
|
|
|
121
|
-
Your
|
|
158
|
+
Your directory structure under `src/app` is your URL structure.
|
|
122
159
|
|
|
123
|
-
|
|
160
|
+
| File | URL |
|
|
161
|
+
| ------------------------------- | ------------- |
|
|
162
|
+
| `src/app/index.html` | `/` |
|
|
163
|
+
| `src/app/about/index.html` | `/about` |
|
|
164
|
+
| `src/app/blog/posts/index.html` | `/blog/posts` |
|
|
124
165
|
|
|
125
|
-
|
|
166
|
+
```
|
|
167
|
+
src/app/users/[id]/index.html -> /users/123 (dynamic segment)
|
|
168
|
+
src/app/docs/[...slug]/index.html -> /docs/a/b/c (catch-all)
|
|
169
|
+
src/app/(auth)/login/index.html -> /login (route group, no URL segment)
|
|
170
|
+
src/app/dashboard/layout.html -> wraps every /dashboard/* page
|
|
171
|
+
```
|
|
126
172
|
|
|
127
|
-
|
|
173
|
+
#### Route file conventions
|
|
128
174
|
|
|
129
|
-
|
|
175
|
+
| File | Owns |
|
|
176
|
+
| ---------------- | -------------------------------------------------------------------------------- |
|
|
177
|
+
| `index.html` | The page's authored markup (single root element) |
|
|
178
|
+
| `index.py` | `metadata`, `page()`, route-owned `@rpc()` actions, redirects, first-render data |
|
|
179
|
+
| `layout.html` | A section wrapper containing `<slot />` |
|
|
180
|
+
| `layout.py` | Layout-level props and server logic |
|
|
181
|
+
| `loading.html` | Navigation loading state for the route |
|
|
182
|
+
| `not-found.html` | 404 UI |
|
|
183
|
+
| `error.html` | Error UI |
|
|
130
184
|
|
|
131
|
-
|
|
185
|
+
`page()` renders the sibling template with `render_page(__file__, context)`. It can also return a
|
|
186
|
+
`(page_html, layout_props)` tuple, whose dict keys become `{{ layout.* }}` in a parent layout.
|
|
132
187
|
|
|
133
|
-
|
|
188
|
+
> **Rule:** `index.py` never inlines page HTML. Markup belongs in `index.html`.
|
|
134
189
|
|
|
135
|
-
|
|
190
|
+
---
|
|
136
191
|
|
|
137
|
-
|
|
192
|
+
### 2. Templates are plain HTML — not JSX
|
|
138
193
|
|
|
139
|
-
|
|
194
|
+
PulsePoint borrows React's **hook API** and its **component decomposition model**. It does **not** borrow JSX. This is the single most common source of broken pages.
|
|
140
195
|
|
|
141
|
-
|
|
196
|
+
```html
|
|
197
|
+
<!-- ❌ These silently corrupt the page -->
|
|
198
|
+
<div class="{cls}">…</div>
|
|
199
|
+
<!-- unquoted brace attr -->
|
|
200
|
+
{isOpen &&
|
|
201
|
+
<div>Panel</div>
|
|
202
|
+
} {items.map(item => (
|
|
203
|
+
<li>{item.name}</li>
|
|
204
|
+
))}
|
|
205
|
+
<button className="btn" onClick="{save}">Save</button>
|
|
206
|
+
|
|
207
|
+
<!-- ✅ The PulsePoint equivalents -->
|
|
208
|
+
<div class="{cls}">…</div>
|
|
209
|
+
<div hidden="{!isOpen}">Panel</div>
|
|
210
|
+
<template pp-for="item in items"><li key="{item.id}">{item.name}</li></template>
|
|
211
|
+
<button class="btn" onclick="{save()}">Save</button>
|
|
212
|
+
```
|
|
142
213
|
|
|
143
|
-
|
|
214
|
+
An unquoted `class={...}` is **invalid HTML**: the parser splits it on spaces into junk attributes, the component root never compiles, and the route serves a blank page **with no console error**.
|
|
144
215
|
|
|
145
|
-
|
|
216
|
+
**Sanity check:** delete every `{}` from your template. What remains must still be valid HTML.
|
|
146
217
|
|
|
147
|
-
|
|
218
|
+
There is no `pp-if`, `pp-show`, `pp-else`, or `pp-key`. Conditionals are `hidden="{...}"` or a ternary inside `{...}`. Keyed lists use plain `key`.
|
|
148
219
|
|
|
149
|
-
|
|
220
|
+
#### The complete author-facing template surface
|
|
150
221
|
|
|
151
|
-
|
|
222
|
+
| Syntax | Where | Purpose |
|
|
223
|
+
| --------------------------------------------------------------- | ------------------------------------------ | ----------------------------- |
|
|
224
|
+
| `{expression}` | Text nodes and **quoted** attribute values | Interpolation |
|
|
225
|
+
| `onclick`, `oninput`, `onchange`, `onsubmit`, any `on*` | Any element | Event binding |
|
|
226
|
+
| `pp-for="item in items"` / `"(item, index) in items"` | **`<template>` only** | Keyed list rendering |
|
|
227
|
+
| `key="{expr}"` | The repeated element | Diffing identity |
|
|
228
|
+
| `pp-ref="name"` / `pp-ref="{expr}"` | Native elements and `x-*` tags | Imperative element access |
|
|
229
|
+
| `pp-style="{cssText}"` | Any element | Dynamic inline style (string) |
|
|
230
|
+
| `pp-spread="{...obj}"` | Any element | Spread object into attributes |
|
|
231
|
+
| `<token.provider value="{v}">` (lowercase) | Anywhere | Context provider |
|
|
232
|
+
| `pp-spa="true"` / `pp-spa="false"` | `<body>` / an `<a>` | SPA navigation opt-in/out |
|
|
233
|
+
| `pp-reset-scroll="true"`, `pp-scroll-key="name"` | A scroll container | Scroll restoration control |
|
|
234
|
+
| `pp-loading-content`, `pp-loading-url`, `pp-loading-transition` | Navigation regions | Loading UI |
|
|
152
235
|
|
|
153
|
-
|
|
236
|
+
Never handwrite runtime-managed attributes (`pp-component`, `type="text/pp"`, `pp-owner`, `pp-ref-owner`, `pp-ref-forward`, `data-pp-*`, …). The render pipeline and the browser runtime write those.
|
|
154
237
|
|
|
155
|
-
|
|
156
|
-
| ------------------------------- | ------------- |
|
|
157
|
-
| `src/app/index.html` | `/` |
|
|
158
|
-
| `src/app/about/index.html` | `/about` |
|
|
159
|
-
| `src/app/blog/posts/index.html` | `/blog/posts` |
|
|
238
|
+
#### The single-root rule
|
|
160
239
|
|
|
161
|
-
|
|
240
|
+
Every route, layout, and component template must have **exactly one** top-level element (or one imported `x-*` root), with any owned `<script>` **inside** that root. Caspian injects `pp-component` on the final root and errors if it cannot find one. `<!-- @import ... -->` comments sit above the root and do not count as content.
|
|
162
241
|
|
|
163
|
-
|
|
164
|
-
src/app/users/[id]/index.html -> /users/123
|
|
165
|
-
```
|
|
242
|
+
---
|
|
166
243
|
|
|
167
|
-
|
|
244
|
+
### 3. PulsePoint hooks
|
|
168
245
|
|
|
169
|
-
|
|
170
|
-
src/app/docs/[...slug]/index.html -> /docs/getting-started/setup
|
|
171
|
-
```
|
|
246
|
+
Component `<script>` blocks are plain JavaScript. The `pp` object mirrors React hooks:
|
|
172
247
|
|
|
173
|
-
|
|
248
|
+
| Hook | Returns / purpose |
|
|
249
|
+
| --------------------------------------------- | ---------------------------------------------------------------------- |
|
|
250
|
+
| `pp.state(initial)` | `[value, setValue]` — setter accepts a value or an updater |
|
|
251
|
+
| `pp.effect(fn, deps?)` | After render; may return a cleanup function |
|
|
252
|
+
| `pp.layoutEffect(fn, deps?)` | Synchronously after DOM mutation |
|
|
253
|
+
| `pp.ref(initial?)` | `{ current }` |
|
|
254
|
+
| `pp.memo(fn, deps)` / `pp.callback(fn, deps)` | Memoized value / stable function |
|
|
255
|
+
| `pp.reducer(reducer, initial)` | `[state, dispatch]` |
|
|
256
|
+
| `pp.context(token)` | Read a context value from an ancestor provider |
|
|
257
|
+
| `pp.portal(ref, target?)` | Render into another DOM target, preserving logical ancestry |
|
|
258
|
+
| `pp.id()` | Stable unique id for `id`/`for`/`aria-*` pairing |
|
|
259
|
+
| `pp.errorBoundary()` | `[error, reset]` — catches render and effect throws in descendants |
|
|
260
|
+
| `pp.syncExternalStore(subscribe, snapshot)` | Subscribe to a source the component doesn't own (`matchMedia`, stores) |
|
|
261
|
+
| `pp.imperativeHandle(ref, create, deps?)` | Publish an imperative API to a parent's ref |
|
|
262
|
+
| `pp.transition()` | `[isPending, startTransition]` |
|
|
263
|
+
| `pp.deferredValue(value, initial?)` | Lags one commit behind the source |
|
|
264
|
+
| `pp.optimistic(passthrough, reducer?)` | Optimistic UI that reconciles against a confirmed value |
|
|
265
|
+
| `pp.props` | Props bag derived from the rendered root's attributes |
|
|
174
266
|
|
|
175
|
-
|
|
176
|
-
src/app/(auth)/login/index.html -> /login
|
|
177
|
-
src/app/(auth)/register/index.html -> /register
|
|
178
|
-
```
|
|
267
|
+
Runtime utilities: `pp.createContext`, `pp.mount`, `pp.redirect`, `pp.rpc`, `pp.enablePerf`, `pp.disablePerf`, `pp.getPerfStats`, `pp.resetPerfStats`.
|
|
179
268
|
|
|
180
|
-
|
|
269
|
+
React APIs with **no** PulsePoint equivalent: `forwardRef`, `memo()` as a wrapper, `lazy`, `Suspense`, `useInsertionEffect`, `useActionState`, `useFormStatus`, free-function `startTransition`.
|
|
181
270
|
|
|
182
|
-
|
|
271
|
+
Inside an `on*` attribute the runtime injects `event` plus the aliases `e`, `$event`, `target`, `currentTarget`, and `el`.
|
|
183
272
|
|
|
184
|
-
|
|
185
|
-
- Section: e.g., `/dashboard/settings` inherits root + dashboard layout automatically
|
|
273
|
+
#### Context
|
|
186
274
|
|
|
187
|
-
|
|
275
|
+
Providers are authored as **lowercase** tags derived from the token variable name:
|
|
276
|
+
|
|
277
|
+
```html
|
|
278
|
+
<section>
|
|
279
|
+
<script>
|
|
280
|
+
const ThemeContext = pp.createContext("light");
|
|
281
|
+
const [theme, setTheme] = pp.state("dark");
|
|
282
|
+
</script>
|
|
283
|
+
|
|
284
|
+
<themecontext.provider value="{theme}">
|
|
285
|
+
<button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
|
|
286
|
+
Theme: {theme}
|
|
287
|
+
</button>
|
|
288
|
+
<x-child-panel />
|
|
289
|
+
</themecontext.provider>
|
|
290
|
+
</section>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
A descendant reads it with `const theme = pp.context(ThemeContext);`.
|
|
188
294
|
|
|
189
295
|
---
|
|
190
296
|
|
|
191
|
-
### Components
|
|
297
|
+
### 4. Components
|
|
192
298
|
|
|
193
|
-
Components are Python functions decorated with `@component
|
|
299
|
+
Components are Python functions decorated with `@component`, imported at the top of a template and rendered as kebab-cased `x-*` tags.
|
|
194
300
|
|
|
195
|
-
####
|
|
301
|
+
#### Return a string (small, presentational)
|
|
196
302
|
|
|
197
303
|
```python
|
|
198
304
|
from casp.component_decorator import component
|
|
@@ -200,31 +306,43 @@ from casp.html_attrs import get_attributes, merge_classes
|
|
|
200
306
|
|
|
201
307
|
@component
|
|
202
308
|
def Container(children: str = "", **props) -> str:
|
|
203
|
-
|
|
204
|
-
final_class = merge_classes("mx-auto max-w-7xl px-4", incoming_class)
|
|
309
|
+
final_class = merge_classes("mx-auto max-w-7xl px-4", props.pop("class", ""))
|
|
205
310
|
attributes = get_attributes({"class": final_class}, props)
|
|
206
|
-
return f
|
|
311
|
+
return f"<div {attributes}>{children}</div>"
|
|
207
312
|
```
|
|
208
313
|
|
|
209
|
-
####
|
|
314
|
+
#### Single-file with `html(...)` (small/medium, the common case)
|
|
315
|
+
|
|
316
|
+
Keep markup, server interpolation, and the PulsePoint script inline. Three brace dialects coexist:
|
|
317
|
+
`{{ value }}` is server-side Jinja, `{{ value | json }}` safely serializes into a `<script>`,
|
|
318
|
+
`{# … #}` is a Jinja comment, and `{ value }` is left untouched for PulsePoint.
|
|
210
319
|
|
|
211
320
|
```python
|
|
212
|
-
from
|
|
213
|
-
from casp.component_decorator import component
|
|
321
|
+
from casp.component_decorator import component, html
|
|
214
322
|
from casp.html_attrs import get_attributes, merge_classes
|
|
215
323
|
|
|
216
|
-
ButtonVariant = Literal["default", "outline", "destructive"]
|
|
217
|
-
|
|
218
324
|
@component
|
|
219
|
-
def
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
"
|
|
325
|
+
def UserCard(user=None, **props):
|
|
326
|
+
attributes = get_attributes({
|
|
327
|
+
"class": merge_classes("card", props.pop("class", "")),
|
|
328
|
+
"user-name": user["name"],
|
|
223
329
|
}, props)
|
|
224
|
-
|
|
330
|
+
|
|
331
|
+
# html
|
|
332
|
+
return html("""
|
|
333
|
+
<div {{ attributes }}>
|
|
334
|
+
<h3>{{ user.name }}</h3>
|
|
335
|
+
<button onclick="setLikes(likes + 1)">Likes: {likes}</button>
|
|
336
|
+
<script>
|
|
337
|
+
const [likes, setLikes] = pp.state({{ user.likes | json }});
|
|
338
|
+
</script>
|
|
339
|
+
</div>
|
|
340
|
+
""", attributes=attributes, user=user)
|
|
225
341
|
```
|
|
226
342
|
|
|
227
|
-
|
|
343
|
+
> Use a raw string (`r"""…"""`) when the inline `<script>` contains backslashes (regex, `\n`).
|
|
344
|
+
|
|
345
|
+
#### Template-backed (`render_html`) — for large markup
|
|
228
346
|
|
|
229
347
|
`Counter.py`:
|
|
230
348
|
|
|
@@ -240,7 +358,7 @@ def Counter(label: str = "Clicks") -> str:
|
|
|
240
358
|
|
|
241
359
|
```html
|
|
242
360
|
<div>
|
|
243
|
-
<h3>
|
|
361
|
+
<h3>{{ label }}</h3>
|
|
244
362
|
<button onclick="setCount(count + 1)">{count}</button>
|
|
245
363
|
|
|
246
364
|
<script>
|
|
@@ -249,168 +367,371 @@ def Counter(label: str = "Clicks") -> str:
|
|
|
249
367
|
</div>
|
|
250
368
|
```
|
|
251
369
|
|
|
370
|
+
#### Importing and rendering
|
|
371
|
+
|
|
372
|
+
```html
|
|
373
|
+
<!-- @import Container from "../components" -->
|
|
374
|
+
<!-- @import { Button, Card as UserCard } from "../components/ui" -->
|
|
375
|
+
<!-- @import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from "../components/Breadcrumb.py" -->
|
|
376
|
+
|
|
377
|
+
<x-container class="py-10">
|
|
378
|
+
<x-button variant="outline">Continue</x-button>
|
|
379
|
+
</x-container>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
- `Container` → `<x-container />`, `CommandDialog` → `<x-command-dialog />`.
|
|
383
|
+
- If one Python file exports several components, import them **from that file path**, not from the folder.
|
|
384
|
+
- `@import` is a file-level directive: it must sit **above** the authored root, never inside it.
|
|
385
|
+
- For component-to-component composition, prefer real Python imports inside the component module; a component's own `x-*` tags resolve from its module imports.
|
|
386
|
+
|
|
387
|
+
#### Props: every prop the template reads must be re-emitted on the root
|
|
388
|
+
|
|
389
|
+
This is the most common silent failure in Python components:
|
|
390
|
+
|
|
391
|
+
1. Attributes on the `x-*` tag arrive in Python as **raw string kwargs** (`open="{permOpen}"` arrives as the literal `"{permOpen}"`), kebab-case converted to camelCase.
|
|
392
|
+
2. The Python component must deliberately re-emit them onto its single rendered root via `get_attributes({...}, props)` + `{{ attributes }}`.
|
|
393
|
+
3. PulsePoint derives `pp.props` from **the rendered root's attributes** and evaluates pure `{expr}` values in the parent's scope.
|
|
394
|
+
|
|
395
|
+
A prop accepted by Python but not re-emitted is silently `undefined` in the browser — no server error, no console warning. Forwarding also does not preserve types: a brace expression keeps its real type, a literal (`volume="0"`) arrives as a **string**, a valueless attribute arrives as `true`, `None`/`False`/`""` are omitted entirely, and JS reserved words such as `class` are dropped from `pp.props`.
|
|
396
|
+
|
|
397
|
+
#### Tailwind class merging
|
|
398
|
+
|
|
399
|
+
When `tailwindcss: true`, Python `merge_classes(...)` emits a frontend-ready `{twMerge(...)}` expression, and the browser's global `twMerge(...)` resolves conflicts. That pair is the only supported merge path.
|
|
400
|
+
|
|
252
401
|
---
|
|
253
402
|
|
|
254
|
-
###
|
|
403
|
+
### 5. Data: `pp.rpc()` and `@rpc()`
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
await pp.rpc(name, data?, optionsOrAbort?)
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- Posts to the **current route**; resolves the `@rpc()` function of that name in the route's `index.py`.
|
|
410
|
+
- Smart serialization — switches to `FormData` automatically when a `File` is present.
|
|
411
|
+
- CSRF token injected as `X-CSRF-Token`.
|
|
412
|
+
- Server redirect headers are honored through `pp.redirect()`.
|
|
413
|
+
- Passing `true` as the third argument means `{ abortPrevious: true }`.
|
|
414
|
+
|
|
415
|
+
Options: `abortPrevious`, `onStream`, `onStreamError`, `onStreamComplete`, `onUploadProgress`, `onUploadComplete`.
|
|
416
|
+
|
|
417
|
+
**Payload safety:** RPC keys are filtered against the function signature, so a parameter is client-settable only when it is declared. Declaring `**kwargs` opts into the whole payload — do that deliberately.
|
|
418
|
+
|
|
419
|
+
#### Upload progress
|
|
420
|
+
|
|
421
|
+
```html
|
|
422
|
+
<input type="file" onchange="{upload(event.target.files?.[0])}" />
|
|
423
|
+
<progress max="100" value="{percent ?? 0}"></progress>
|
|
424
|
+
|
|
425
|
+
<script>
|
|
426
|
+
const [percent, setPercent] = pp.state(null);
|
|
427
|
+
|
|
428
|
+
async function upload(file) {
|
|
429
|
+
if (!file) return;
|
|
430
|
+
await pp.rpc(
|
|
431
|
+
"upload_asset",
|
|
432
|
+
{ file },
|
|
433
|
+
{
|
|
434
|
+
onUploadProgress: ({ percent }) => setPercent(percent),
|
|
435
|
+
onUploadComplete: () => setPercent(100),
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
</script>
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
`onUploadProgress` receives `{ loaded, total, percent }`; `total` and `percent` are `null` when the length is not computable.
|
|
255
443
|
|
|
256
|
-
|
|
444
|
+
#### Streaming (the path for AI/LLM token output)
|
|
257
445
|
|
|
258
|
-
|
|
259
|
-
| ---------------------------------- | ------------------------------------------- |
|
|
260
|
-
| `pp.state(initial)` | Returns `[value, setValue]` |
|
|
261
|
-
| `pp.effect(callback, deps?)` | Runs after render; returns cleanup function |
|
|
262
|
-
| `pp.layoutEffect(callback, deps?)` | Runs synchronously after DOM mutation |
|
|
263
|
-
| `pp.ref(initialValue?)` | Returns `{ current }` |
|
|
264
|
-
| `pp.createContext(defaultValue)` | Creates a context token |
|
|
265
|
-
| `<Context.Provider value="{...}">` | Provide context to descendants |
|
|
266
|
-
| `pp.context(token)` | Read context in a descendant |
|
|
267
|
-
| `pp.portal(ref, target?)` | Portal rendering to a ref target |
|
|
268
|
-
| `pp.rpc(name, data?, options?)` | Call a backend RPC action |
|
|
269
|
-
| `pp.redirect(url)` | SPA-aware navigation |
|
|
446
|
+
A generator `@rpc()` becomes a `text/event-stream` response:
|
|
270
447
|
|
|
271
|
-
|
|
448
|
+
```python
|
|
449
|
+
@rpc()
|
|
450
|
+
async def ask_question(topic: str):
|
|
451
|
+
async for chunk in llm.stream(topic):
|
|
452
|
+
yield chunk
|
|
453
|
+
```
|
|
272
454
|
|
|
273
|
-
|
|
455
|
+
```js
|
|
456
|
+
pp.rpc(
|
|
457
|
+
"ask_question",
|
|
458
|
+
{ topic },
|
|
459
|
+
{
|
|
460
|
+
onStream: (chunk) => setAnswer((current) => current + chunk),
|
|
461
|
+
onStreamComplete: () => setIsStreaming(false),
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
```
|
|
274
465
|
|
|
275
|
-
|
|
276
|
-
- CSRF token injection via `X-CSRF-Token`
|
|
277
|
-
- Auto-redirects when server returns redirect headers
|
|
278
|
-
- Upload progress callbacks when `onUploadProgress` is provided
|
|
466
|
+
Do not reinvent one-way streaming with raw `fetch`/`ReadableStream`, `EventSource`, or a WebSocket.
|
|
279
467
|
|
|
280
468
|
---
|
|
281
469
|
|
|
282
|
-
###
|
|
470
|
+
### 6. Validation
|
|
471
|
+
|
|
472
|
+
```python
|
|
473
|
+
from casp.validate import Rule, Validate
|
|
474
|
+
|
|
475
|
+
email = Validate.email(" User@Example.com ") # -> "User@Example.com"
|
|
476
|
+
count = Validate.int("42") # -> 42
|
|
477
|
+
bad = Validate.url("not-a-url") # -> None
|
|
478
|
+
|
|
479
|
+
checked = Validate.with_rules(password, [Rule.REQUIRED, Rule.min(8)])
|
|
480
|
+
if checked is not True:
|
|
481
|
+
return {"error": checked}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
`Validate` covers strings and identifiers (`string`, `email`, `url`, `ip`, `uuid`, `ulid`, `cuid`, `cuid2`, `nanoid`), numbers (`int`, `big_int`, `float`, `decimal`), dates (`date`, `date_time`), `boolean`, and structured values (`json`, `enum`, `enum_class`). `Validate.string()` trims and HTML-escapes by default.
|
|
283
485
|
|
|
284
|
-
|
|
486
|
+
Browser-side checks are UX only. Server-side validation at the RPC/route boundary is authoritative.
|
|
285
487
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
| `auth.get_payload()` | Get user payload from session |
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
### 7. Authentication
|
|
491
|
+
|
|
492
|
+
Session-based, configured centrally in `src/lib/auth/auth_config.py` and wired in `main.py`.
|
|
292
493
|
|
|
293
494
|
```python
|
|
294
|
-
from casp.auth import Auth,
|
|
495
|
+
from casp.auth import Auth, GithubProvider, GoogleProvider, configure_auth
|
|
295
496
|
from src.lib.auth.auth_config import build_auth_settings
|
|
296
497
|
|
|
297
498
|
configure_auth(build_auth_settings())
|
|
298
499
|
Auth.set_providers(GithubProvider(), GoogleProvider())
|
|
299
500
|
```
|
|
300
501
|
|
|
301
|
-
|
|
502
|
+
| Method | Purpose |
|
|
503
|
+
| ------------------------------------------------------------ | ----------------------------------------------------------------- |
|
|
504
|
+
| `auth.sign_in(data, token_validity=None, redirect_to=False)` | Store the payload, set a CSRF token; returns `"ok"` or a redirect |
|
|
505
|
+
| `auth.sign_out(redirect_to=None)` | Clear the session and redirect |
|
|
506
|
+
| `auth.is_authenticated()` | `False` when the payload is missing, malformed, or expired |
|
|
507
|
+
| `auth.get_payload()` | Read the signed-in payload |
|
|
508
|
+
| `auth.refresh_session()` | Extend expiry when `token_auto_refresh=True` |
|
|
509
|
+
| `auth.check_role(user, allowed_roles)` | RBAC check against the configured role field |
|
|
302
510
|
|
|
303
|
-
|
|
511
|
+
Guards: `@rpc(require_auth=True)` for actions, `@guest_only()` / route-level guards for pages, and central public-vs-private route policy in `auth_config.py`.
|
|
304
512
|
|
|
305
|
-
|
|
513
|
+
**OAuth is already wired.** `Auth.set_providers(...)` registers `/api/auth/signin/{google,github}` and `/api/auth/callback/{google,github}`. Link a button and set the credentials in `.env` — do not hand-roll the flow.
|
|
306
514
|
|
|
307
|
-
|
|
308
|
-
npx create-caspian-app my-app
|
|
309
|
-
npx create-caspian-app my-app --starter-kit=fullstack
|
|
310
|
-
npx create-caspian-app my-app --tailwindcss --typescript --prisma
|
|
311
|
-
```
|
|
515
|
+
**Redirect ownership is centralized.** Do not re-implement `next=` handling or post-login routing in a sign-in page; `auth_config.py` owns protected-route redirects, auth-route redirects, and `default_signin_redirect` (defaults to `/dashboard`).
|
|
312
516
|
|
|
313
|
-
|
|
517
|
+
---
|
|
314
518
|
|
|
315
|
-
|
|
316
|
-
| --------------------- | ---------------------------------------------- |
|
|
317
|
-
| `--backend-only` | Skip frontend assets |
|
|
318
|
-
| `--tailwindcss` | Enable Tailwind CSS |
|
|
319
|
-
| `--prisma` | Enable Prisma ORM |
|
|
320
|
-
| `--mcp` | Enable MCP server scaffolding |
|
|
321
|
-
| `--typescript` | Enable TypeScript tooling |
|
|
322
|
-
| `--starter-kit=<kit>` | Use a preset (basic, fullstack, api, realtime) |
|
|
323
|
-
| `-y` | Non-interactive mode |
|
|
519
|
+
### 8. Database (Prisma)
|
|
324
520
|
|
|
325
|
-
|
|
521
|
+
Enabled by `"prisma": true`. Define one `prisma/schema.prisma` and generate a typed Python client into `src/lib/prisma/`.
|
|
326
522
|
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
523
|
+
```python
|
|
524
|
+
from src.lib.prisma import prisma
|
|
525
|
+
|
|
526
|
+
users = await prisma.user.find_many(
|
|
527
|
+
where={"active": True},
|
|
528
|
+
include={"userRole": True},
|
|
529
|
+
order_by={"createdAt": "desc"},
|
|
530
|
+
)
|
|
331
531
|
```
|
|
332
532
|
|
|
333
|
-
|
|
533
|
+
After schema changes, in this order:
|
|
334
534
|
|
|
335
535
|
```bash
|
|
336
536
|
npx prisma migrate dev
|
|
337
|
-
|
|
338
|
-
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
```bash
|
|
339
540
|
npx ppy generate
|
|
340
541
|
```
|
|
341
542
|
|
|
543
|
+
If the seed flow depends on the new schema, run `npx prisma generate` first, then `npx prisma db seed` — **`db seed` may clear or overwrite tables; confirm the datasource before running it.**
|
|
544
|
+
|
|
545
|
+
`src/lib/prisma/__init__.py`, `db.py`, `models.py`, and `settings/prisma-schema.json` are generated by `npx ppy generate`. Never hand-edit them, and never add a second data layer (raw drivers, hand-written SQL helpers, JSON stores, browser-side fetches) alongside the ORM.
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
### 9. Optional features
|
|
550
|
+
|
|
551
|
+
Every optional capability is gated by one flag in `caspian.config.json`. **That file is the single source of truth** — a doc or example mentioning a feature does not mean it is enabled in your project. To turn one on after scaffold, set the flag and run `npx casp update project`.
|
|
552
|
+
|
|
553
|
+
| Flag | Enables |
|
|
554
|
+
| ------------- | -------------------------------------------------------------------- |
|
|
555
|
+
| `backendOnly` | API/service mode with no frontend assets |
|
|
556
|
+
| `tailwindcss` | Tailwind v4 + PostCSS pipeline, `merge_classes` / `twMerge` contract |
|
|
557
|
+
| `typescript` | TypeScript frontend tooling and the Vite build path |
|
|
558
|
+
| `prisma` | Prisma schema, migrations, and the generated Python ORM |
|
|
559
|
+
| `mcp` | A FastMCP server mounted into the same app (`/mcp`) |
|
|
560
|
+
| `websocket` | App-owned FastAPI `@app.websocket(...)` endpoints and socket helpers |
|
|
561
|
+
|
|
562
|
+
#### WebSockets
|
|
563
|
+
|
|
564
|
+
Use RPC for ordinary reads, writes, uploads, and SSE streams. Reach for WebSockets only for **long-lived bidirectional** channels: chat, collaboration, presence, multiplayer state.
|
|
565
|
+
|
|
566
|
+
Endpoints are app-owned in `main.py`; reusable session/auth/broadcast helpers live in `src/lib/websocket/`. In the browser, use PulsePoint for state and lifecycle but the **native** `WebSocket` for the transport — keep the socket in `pp.ref(...)` and close it in a cleanup effect.
|
|
567
|
+
|
|
568
|
+
**Sockets authorize themselves.** The HTTP middleware stack early-returns on `scope["type"] == "websocket"`, so `AuthMiddleware` does not protect them. Every endpoint must run its own origin + auth guard, and authenticated and guest traffic belong in separate broadcast pools.
|
|
569
|
+
|
|
570
|
+
#### MCP
|
|
571
|
+
|
|
572
|
+
When `mcp: true`, a FastMCP server is mounted into the same app so one deploy serves both web and MCP. The endpoint is mounted **outside** the routing tree, so `AuthMiddleware` does not cover it — `MCP_AUTH_TOKEN` is its credential. With no token it stays open in development and returns **503 in production**.
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Security defaults
|
|
577
|
+
|
|
578
|
+
Caspian ships fail-closed defaults. Things worth knowing before you change them:
|
|
579
|
+
|
|
580
|
+
- **`APP_ENV` resolves fail-closed.** Only an explicit development value (`dev`, `development`, `local`, `staging`, `test`, `testing`) enables relaxations. Unset or misspelled counts as **production**.
|
|
581
|
+
- **Server-interpolated values never carry live PulsePoint syntax.** The Jinja environment encodes `{` / `}` as entities on every non-`Markup` value, so stored user data can never execute as a template expression. `Markup` is the trust boundary — `| safe`, `get_attributes(...)`, `merge_classes(...)`, and the `json` filter legitimately keep their braces.
|
|
582
|
+
- **Authenticated renders are never cached.** The page cache keys on the URI alone, so a request-eligibility check gates both the read and the write; a route's `Cache(...)` cannot override it.
|
|
583
|
+
- **RPC payload keys are filtered against the function signature.**
|
|
584
|
+
- **`/uploads` serves user content in attachment mode**; only real image types render inline. First-party `/css`, `/js`, and `/assets` stay inline.
|
|
585
|
+
- **CSRF protection, strict Origin validation, HttpOnly cookies, security headers, and page rate limiting** are on by default.
|
|
586
|
+
|
|
587
|
+
Security-relevant environment variables: `MCP_AUTH_TOKEN`, `RATE_LIMIT_PAGES` (default `200/minute`), `CONTENT_SECURITY_POLICY` (replaces the default policy wholesale), `MAX_WEBSOCKET_CONNECTIONS`, `MAX_WEBSOCKET_MESSAGES_PER_WINDOW`, `WEBSOCKET_RATE_WINDOW_SECONDS`, and `WEBSOCKET_ALLOWED_ORIGINS` (**required in production** — the same-origin fallback is derived from the client-supplied `Host` header and is development-only).
|
|
588
|
+
|
|
342
589
|
---
|
|
343
590
|
|
|
344
591
|
## Project structure
|
|
345
592
|
|
|
346
593
|
```
|
|
347
594
|
my-app/
|
|
348
|
-
├── main.py
|
|
349
|
-
├── caspian.config.json
|
|
595
|
+
├── main.py # FastAPI entry point, middleware stack, WebSocket + MCP mounts
|
|
596
|
+
├── caspian.config.json # Feature flags — the single source of truth
|
|
597
|
+
├── pyproject.toml # Python deps and tooling config
|
|
598
|
+
├── package.json # CLI/tooling scripts
|
|
350
599
|
├── prisma/
|
|
351
600
|
│ ├── schema.prisma
|
|
352
601
|
│ └── seed.ts
|
|
353
602
|
├── src/
|
|
354
|
-
│ ├── app/
|
|
355
|
-
│ │ ├── layout.html
|
|
356
|
-
│ │ ├──
|
|
357
|
-
│ │
|
|
358
|
-
│ │
|
|
359
|
-
│ │
|
|
360
|
-
│ ├──
|
|
361
|
-
│ │
|
|
362
|
-
│ │
|
|
363
|
-
│
|
|
603
|
+
│ ├── app/ # File-system routes
|
|
604
|
+
│ │ ├── layout.html # Root layout
|
|
605
|
+
│ │ ├── layout.py
|
|
606
|
+
│ │ ├── index.html # Home page
|
|
607
|
+
│ │ ├── index.py
|
|
608
|
+
│ │ ├── globals.css
|
|
609
|
+
│ │ ├── error.html
|
|
610
|
+
│ │ ├── not-found.html
|
|
611
|
+
│ │ └── users/[id]/
|
|
612
|
+
│ │ ├── index.html # /users/:id
|
|
613
|
+
│ │ └── index.py
|
|
614
|
+
│ ├── components/ # Reusable UI (@component)
|
|
615
|
+
│ └── lib/ # Non-UI code
|
|
364
616
|
│ ├── auth/auth_config.py
|
|
365
|
-
│
|
|
366
|
-
│
|
|
367
|
-
|
|
368
|
-
|
|
617
|
+
│ ├── prisma/ # Generated Python ORM — do not edit
|
|
618
|
+
│ ├── websocket/ # Socket helpers (when websocket: true)
|
|
619
|
+
│ └── mcp/ # FastMCP server (when mcp: true)
|
|
620
|
+
├── public/ # Static assets, incl. the PulsePoint runtime and uploads
|
|
621
|
+
└── settings/ # Dev stack config and generated indexes
|
|
369
622
|
```
|
|
370
623
|
|
|
371
|
-
###
|
|
624
|
+
### Placement rules
|
|
372
625
|
|
|
373
|
-
- **
|
|
374
|
-
- **
|
|
375
|
-
- **
|
|
376
|
-
-
|
|
377
|
-
- **Single-root rule:** Every template must have exactly one top-level parent node with any owned `<script>` inside it
|
|
626
|
+
- **Route-owned** logic (first-render query, route `@rpc()` actions, redirects, route validation) stays in that route's `index.py`. Move it to `src/lib/**` only when it is genuinely shared.
|
|
627
|
+
- **Reusable UI** goes in `src/components/`; **helpers, services, adapters** go in `src/lib/`.
|
|
628
|
+
- **Compose pages from components.** A route's `index.html` should read as a short assembly of `x-*` chunks (topbar, sidebar, header, sections, forms, footer), not a wall of markup. Plan the breakdown before writing the route.
|
|
629
|
+
- **Generated, never hand-edited:** `src/lib/prisma/**`, `settings/prisma-schema.json`, `settings/files-list.json`, `settings/component-map.json`, `public/css/styles.css`, `__pycache__/`.
|
|
378
630
|
|
|
379
631
|
---
|
|
380
632
|
|
|
381
|
-
##
|
|
633
|
+
## CLI reference
|
|
382
634
|
|
|
383
|
-
|
|
635
|
+
### Create
|
|
384
636
|
|
|
385
637
|
```bash
|
|
386
|
-
npx
|
|
638
|
+
npx create-caspian-app my-app
|
|
387
639
|
```
|
|
388
640
|
|
|
389
|
-
|
|
641
|
+
| Flag | Description |
|
|
642
|
+
| ---------------------------- | ------------------------------------------------- |
|
|
643
|
+
| `-y` | Non-interactive; skip all prompts |
|
|
644
|
+
| `--backend-only` | API/service project, no frontend assets |
|
|
645
|
+
| `--tailwindcss` | Enable Tailwind CSS |
|
|
646
|
+
| `--typescript` | Enable TypeScript frontend tooling |
|
|
647
|
+
| `--prisma` | Enable Prisma ORM |
|
|
648
|
+
| `--mcp` | Enable MCP server scaffolding |
|
|
649
|
+
| `--starter-kit=<kit>` | `basic`, `fullstack`, `api`, `realtime`, `custom` |
|
|
650
|
+
| `--starter-kit-source=<url>` | Git repository for `--starter-kit=custom` |
|
|
651
|
+
| `--list-starter-kits` | Print the built-in starter catalog |
|
|
652
|
+
|
|
653
|
+
In `-y` mode every feature defaults to `false`. Starter kit presets can still be overridden by explicit flags: `npx create-caspian-app my-app --starter-kit=fullstack --typescript`.
|
|
654
|
+
|
|
655
|
+
`websocket` has no create flag — enable it in `caspian.config.json` after scaffold, then run the update command.
|
|
656
|
+
|
|
657
|
+
### Update an existing project
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
npx casp update project
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Also accepts a positional tag or version, or the named forms `--tag beta`, `--tag=beta`, `--version 1.2.3`, `--version=1.2.3`, plus `-y`. Supplying more than one version source is an error. On Windows the updater resolves `npx.cmd`, and the creator reuses an existing `.venv` rather than recreating it.
|
|
664
|
+
|
|
665
|
+
Use `excludeFiles` in `caspian.config.json` to protect files you have customized (a common entry is `./src/lib/auth/auth_config.py`) from being overwritten on update. Excluded files are yours to merge going forward.
|
|
666
|
+
|
|
667
|
+
### Project scripts
|
|
668
|
+
|
|
669
|
+
| Command | What it does |
|
|
670
|
+
| ---------------------- | ------------------------------------------------------------------ |
|
|
671
|
+
| `npm run dev` | Full local stack: BrowserSync proxy, Tailwind watch, asset watch |
|
|
672
|
+
| `npm run build` | Build Tailwind and regenerate the route/component index |
|
|
673
|
+
| `npm run static` | Export every static route to `static/` (SSG) |
|
|
674
|
+
| `npm run static:serve` | Preview the exported folder on an auto-selected free loopback port |
|
|
675
|
+
|
|
676
|
+
These are opt-in workflows. Don't run them as a validation step just because source files changed.
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## Static export (SSG)
|
|
681
|
+
|
|
682
|
+
`npm run static` boots the app and writes `static/<route>/index.html` plus copied public assets — the equivalent of Next.js `output: export`. It always runs `npm run build` first so the export walks a fresh route index.
|
|
683
|
+
|
|
684
|
+
Policy is **warn & skip**: dynamic routes are pre-rendered only when their `index.py` exports `static_paths` (the `getStaticPaths` equivalent); auth-gated, non-200, and non-HTML routes are reported and skipped.
|
|
685
|
+
|
|
686
|
+
`npm run static:serve` serves only `static/`, binds loopback `127.0.0.1` (network exposure is opt-in via `HOST=0.0.0.0`), and walks upward from port 8000 until it finds a free one — **read the port it prints**.
|
|
687
|
+
|
|
688
|
+
`pp.rpc()`, auth, WebSockets, streaming, and per-request server data are all inert in a static export.
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## Ecosystem
|
|
693
|
+
|
|
694
|
+
### ppicons — 1,500+ Lucide-based icons as Python components
|
|
695
|
+
|
|
696
|
+
```bash
|
|
697
|
+
npx ppicons add Rocket
|
|
698
|
+
```
|
|
390
699
|
|
|
391
700
|
```html
|
|
392
|
-
<!-- @import { Rocket } from "../lib/ppicons" -->
|
|
701
|
+
<!-- @import { Rocket, ChevronDown } from "../lib/ppicons" -->
|
|
393
702
|
<x-rocket class="w-6 h-6 text-primary" />
|
|
394
703
|
```
|
|
395
704
|
|
|
396
|
-
|
|
705
|
+
### maddex — shadcn-style UI component kit for Caspian
|
|
706
|
+
|
|
707
|
+
```bash
|
|
708
|
+
npx maddex add button card dialog
|
|
709
|
+
```
|
|
397
710
|
|
|
398
|
-
|
|
711
|
+
```html
|
|
712
|
+
<!-- @import { Button } from "../lib/maddex/Button.py" -->
|
|
713
|
+
<x-button variant="outline">Continue</x-button>
|
|
714
|
+
```
|
|
399
715
|
|
|
400
|
-
|
|
716
|
+
---
|
|
401
717
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
- **
|
|
405
|
-
- **
|
|
718
|
+
## Recommended VS Code setup
|
|
719
|
+
|
|
720
|
+
- **[Caspian Official Framework Support](https://marketplace.visualstudio.com/items?itemName=JeffersonAbrahamOmier.caspian)** — component snippets and autocomplete (the key piece)
|
|
721
|
+
- **[Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)** — ships Pylance, which is Pyright under the hood
|
|
722
|
+
- **[Prisma](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma)** — schema formatting and highlighting
|
|
723
|
+
- **[Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)** — class completion and sorting
|
|
406
724
|
|
|
407
725
|
---
|
|
408
726
|
|
|
409
727
|
## Learn more
|
|
410
728
|
|
|
729
|
+
The full documentation ships inside every Caspian project at `node_modules/caspian-utils/dist/docs/` — start with `index.md`, which routes you to the right feature guide.
|
|
730
|
+
|
|
411
731
|
- Documentation: [caspian.tsnc.tech/docs](https://caspian.tsnc.tech/docs)
|
|
412
|
-
- PulsePoint
|
|
413
|
-
-
|
|
732
|
+
- PulsePoint: [pulsepoint.tsnc.tech](https://pulsepoint.tsnc.tech)
|
|
733
|
+
- Components: [maddex.tsnc.tech/docs](https://maddex.tsnc.tech)
|
|
734
|
+
- Icons: [ppicons.tsnc.tech](https://ppicons.tsnc.tech)
|
|
414
735
|
|
|
415
736
|
---
|
|
416
737
|
|