create-caspian-app 1.3.6 → 1.3.7

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.
@@ -115,8 +115,7 @@ def _serve(host: str, start_port: int, tries: int) -> int:
115
115
  last_error = exc
116
116
  if offset == 0:
117
117
  print(
118
- f"{YELLOW} Port {start_port} is busy; searching for a free "
119
- f"port...{RESET}"
118
+ f"{YELLOW} Port {start_port} is busy; searching for a free port...{RESET}"
120
119
  )
121
120
  continue
122
121
  # A different error (bad host, permissions, etc.) is not recoverable
@@ -3,163 +3,97 @@ from casp.component_decorator import html
3
3
 
4
4
  def page():
5
5
  return html(r"""
6
- <div
7
- class="min-h-screen bg-background text-foreground selection:bg-primary/30 flex flex-col items-center justify-between py-8 md:py-12 px-6 md:px-24 overflow-x-hidden relative"
8
- >
9
- <div class="fixed inset-0 bg-grid -z-20 opacity-[0.15]"></div>
10
- <div class="fixed inset-0 glow-conic -z-10 translate-y-[-20%]"></div>
11
-
12
- <div
13
- class="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex"
14
- >
15
- <p
16
- class="fixed left-0 top-0 flex w-full justify-center border-b border-border bg-background/80 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-muted/30 lg:p-4 lg:dark:bg-zinc-800/30"
17
- >
18
- Get started by editing&nbsp;
19
- <code class="font-bold text-primary">src/app/index.html</code>
6
+ <main class="flex min-h-screen flex-col justify-between gap-16 bg-background px-6 py-8 text-foreground md:px-12 md:py-12">
7
+ <header class="flex items-center justify-between gap-4">
8
+ <p class="border border-border px-4 py-2 font-mono text-xs text-muted-foreground">
9
+ Get started by editing
10
+ <code class="text-foreground">src/app/index.py</code>
20
11
  </p>
21
12
 
22
- <div
23
- class="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-linear-to-t from-background via-background lg:static lg:h-auto lg:w-auto lg:bg-none"
24
- >
25
- <a
26
- href="https://github.com/TheSteelNinjaCode/create-caspian-app"
27
- target="_blank"
28
- rel="noreferrer"
29
- class="pointer-events-auto flex place-items-center gap-2 p-8 lg:p-0 hover:text-primary transition-colors"
30
- >
31
- <span class="font-brand text-xs font-bold tracking-widest"
32
- >BUILT WITH CASPIAN</span
33
- >
13
+ <a class="flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.18em] text-muted-foreground transition-colors hover:text-foreground"
14
+ href="https://github.com/TheSteelNinjaCode/create-caspian-app"
15
+ target="_blank"
16
+ rel="noreferrer">
17
+ <span class="hidden sm:inline">Built with Caspian</span>
34
18
 
35
- <svg
36
- xmlns="http://www.w3.org/2000/svg"
37
- viewBox="0 0 24 24"
38
- class="h-5 w-5"
39
- fill="currentColor"
40
- aria-hidden="true"
41
- >
42
- <path
43
- d="M12 2C6.477 2 2 6.486 2 12.02c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.866-.013-1.7-2.782.605-3.369-1.343-3.369-1.343-.454-1.157-1.109-1.466-1.109-1.466-.907-.62.069-.608.069-.608 1.003.071 1.531 1.033 1.531 1.033.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.952 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.27.098-2.646 0 0 .84-.27 2.75 1.026A9.56 9.56 0 0 1 12 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.748-1.026 2.748-1.026.546 1.376.202 2.393.1 2.646.64.7 1.028 1.595 1.028 2.688 0 3.849-2.339 4.695-4.566 4.944.359.31.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.579.688.481A10.02 10.02 0 0 0 22 12.02C22 6.486 17.523 2 12 2z"
44
- />
45
- </svg>
46
- </a>
47
- </div>
48
- </div>
19
+ <svg class="size-5"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ viewBox="0 0 24 24"
22
+ fill="none"
23
+ stroke="currentColor"
24
+ stroke-width="2"
25
+ stroke-linecap="round"
26
+ stroke-linejoin="round"
27
+ aria-hidden="true">
28
+ <path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
29
+ <path d="M9 18c-4.51 2-5-2-7-2" />
30
+ </svg>
31
+ </a>
32
+ </header>
49
33
 
50
- <div
51
- class="relative flex flex-col items-center justify-center gap-6 z-10 before:absolute before:h-75 before:w-120 before:-translate-x-1/2 before:rounded-full before:bg-linear-to-br before:from-transparent before:to-primary before:opacity-10 before:blur-2xl before:content-[''] before:max-w-full after:absolute after:-z-20 after:h-45 after:w-60 after:translate-x-1/3 after:bg-linear-to-t after:from-cyan-300 after:via-primary after:opacity-20 after:blur-2xl after:content-[''] after:max-w-full"
52
- >
53
- <div
54
- class="relative flex flex-col items-center justify-center gap-6 z-10 pt-24 lg:pt-0 before:absolute before:h-75 before:w-120 before:-translate-x-1/2 before:rounded-full before:bg-linear-to-br before:from-transparent before:to-primary before:opacity-10 before:blur-2xl before:content-[''] before:max-w-full after:absolute after:-z-20 after:h-45 after:w-60 after:translate-x-1/3 after:bg-linear-to-t after:from-cyan-300 after:via-primary after:opacity-20 after:blur-2xl after:content-[''] after:max-w-full"
55
- >
56
- <div
57
- class="relative w-24 h-24 md:w-32 md:h-32 flex items-center justify-center mb-4"
58
- >
59
- <img
60
- src="/favicon.ico"
61
- alt="caspian logo"
62
- class="w-full h-full object-contain"
63
- />
64
- </div>
34
+ <div class="flex flex-col items-center text-center">
35
+ <img class="size-20 object-contain md:size-24"
36
+ src="/favicon.ico"
37
+ alt="Caspian" />
65
38
 
66
- <h1
67
- class="text-4xl md:text-6xl font-bold font-brand tracking-tight text-center"
68
- >
69
- CASPIAN
70
- </h1>
71
- </div>
72
- <p
73
- class="text-muted-foreground text-center max-w-[85vw] md:max-w-125 text-sm md:text-base"
74
- >
75
- The Native Python Framework with
76
- <span class="text-[#053b69] font-medium whitespace-nowrap"
77
- >PulsePoint</span
78
- >
79
- Reactivity.
39
+ <h1 class="mt-8 text-5xl font-bold tracking-[-0.04em] md:text-7xl">CASPIAN</h1>
40
+
41
+ <p class="mt-5 max-w-md text-pretty text-muted-foreground">
42
+ The native Python framework, with
43
+ <span class="text-foreground">PulsePoint</span> reactivity.
80
44
  </p>
81
45
  </div>
82
46
 
83
- <div
84
- class="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 grid-cols-1 md:grid-cols-2 lg:grid-cols-4 lg:text-left gap-5 md:gap-6 mt-16"
85
- >
86
- <a
87
- href="https://caspian.tsnc.tech/docs"
88
- class="group rounded-lg border border-transparent px-5 py-4 transition-all hover:border-border hover:bg-muted/20 hover:backdrop-blur-sm"
89
- target="_blank"
90
- >
91
- <h2
92
- class="mb-3 text-xl md:text-2xl font-semibold font-brand group-hover:text-primary transition-colors"
93
- >
47
+ <nav class="grid gap-px border border-border bg-border sm:grid-cols-2 lg:grid-cols-4">
48
+ <a class="group bg-background p-6 transition-colors hover:bg-muted/50"
49
+ href="https://caspian.tsnc.tech/docs"
50
+ target="_blank"
51
+ rel="noreferrer">
52
+ <h2 class="flex items-center gap-2 font-semibold">
94
53
  Docs
95
- <span
96
- class="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none"
97
- >-></span
98
- >
54
+ <span class="transition-transform group-hover:translate-x-1 motion-reduce:transform-none">&rarr;</span>
99
55
  </h2>
100
- <p class="m-0 text-sm text-muted-foreground">
101
- Find in-depth information about Caspian, Prisma, and PulsePoint
102
- features.
56
+ <p class="mt-2 text-sm leading-relaxed text-muted-foreground">
57
+ Everything about routing, layouts, components and PulsePoint.
103
58
  </p>
104
59
  </a>
105
60
 
106
- <a
107
- href="https://caspian.tsnc.tech/docs/rpc"
108
- class="group rounded-lg border border-transparent px-5 py-4 transition-all hover:border-border hover:bg-muted/20 hover:backdrop-blur-sm"
109
- target="_blank"
110
- >
111
- <h2
112
- class="mb-3 text-xl md:text-2xl font-semibold font-brand group-hover:text-cyan-400 transition-colors"
113
- >
61
+ <a class="group bg-background p-6 transition-colors hover:bg-muted/50"
62
+ href="https://caspian.tsnc.tech/docs/rpc"
63
+ target="_blank"
64
+ rel="noreferrer">
65
+ <h2 class="flex items-center gap-2 font-semibold">
114
66
  RPC
115
- <span
116
- class="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none"
117
- >-></span
118
- >
67
+ <span class="transition-transform group-hover:translate-x-1 motion-reduce:transform-none">&rarr;</span>
119
68
  </h2>
120
- <p class="m-0 text-sm text-muted-foreground">
121
- Learn how to call Python functions from HTML using Native RPC.
69
+ <p class="mt-2 text-sm leading-relaxed text-muted-foreground">
70
+ Call a Python function straight from a click, no endpoint to write.
122
71
  </p>
123
72
  </a>
124
73
 
125
- <a
126
- href="https://caspian.tsnc.tech/docs/database"
127
- class="group rounded-lg border border-transparent px-5 py-4 transition-all hover:border-border hover:bg-muted/20 hover:backdrop-blur-sm"
128
- target="_blank"
129
- >
130
- <h2
131
- class="mb-3 text-xl md:text-2xl font-semibold font-brand group-hover:text-purple-400 transition-colors"
132
- >
74
+ <a class="group bg-background p-6 transition-colors hover:bg-muted/50"
75
+ href="https://caspian.tsnc.tech/docs/database"
76
+ target="_blank"
77
+ rel="noreferrer">
78
+ <h2 class="flex items-center gap-2 font-semibold">
133
79
  Schema
134
- <span
135
- class="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none"
136
- >-></span
137
- >
80
+ <span class="transition-transform group-hover:translate-x-1 motion-reduce:transform-none">&rarr;</span>
138
81
  </h2>
139
- <p class="m-0 text-sm text-muted-foreground">
140
- Edit <code>schema.prisma</code> to define your data models and generate
141
- type-safe clients.
82
+ <p class="mt-2 text-sm leading-relaxed text-muted-foreground">
83
+ Define your models in <code class="font-mono text-xs">schema.prisma</code> and query them with types.
142
84
  </p>
143
85
  </a>
144
86
 
145
- <a
146
- href="https://caspian.tsnc.tech/docs/deploy"
147
- class="group rounded-lg border border-transparent px-5 py-4 transition-all hover:border-border hover:bg-muted/20 hover:backdrop-blur-sm"
148
- target="_blank"
149
- >
150
- <h2
151
- class="mb-3 text-xl md:text-2xl font-semibold font-brand group-hover:text-green-400 transition-colors"
152
- >
87
+ <a class="group bg-background p-6 transition-colors hover:bg-muted/50"
88
+ href="https://caspian.tsnc.tech/docs/deploy"
89
+ target="_blank"
90
+ rel="noreferrer">
91
+ <h2 class="flex items-center gap-2 font-semibold">
153
92
  Deploy
154
- <span
155
- class="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none"
156
- >-></span
157
- >
93
+ <span class="transition-transform group-hover:translate-x-1 motion-reduce:transform-none">&rarr;</span>
158
94
  </h2>
159
- <p class="m-0 text-sm text-muted-foreground">
160
- Instantly deploy your native Python app to Vercel, Railway, or Docker.
161
- </p>
95
+ <p class="mt-2 text-sm leading-relaxed text-muted-foreground">Ship your Python app to Vercel, Railway or Docker.</p>
162
96
  </a>
163
- </div>
164
- </div>
97
+ </nav>
98
+ </main>
165
99
  """)
@@ -13,6 +13,21 @@ def layout():
13
13
  <title>{{ metadata.title | default("Caspian App") }}</title>
14
14
  <meta name="description"
15
15
  content="{{ metadata.description | default('Powered by Caspian Framework') }}" />
16
+ {#
17
+ Everything a page put in `Metadata(extra={...})` -- robots, canonical,
18
+ Open Graph, Twitter cards. The page pipeline already merges `extra` into
19
+ this dict; without this loop it collected those values and dropped them,
20
+ so `extra` silently did nothing. Open Graph is keyed by `property`, every
21
+ other vocabulary by `name`.
22
+ #}
23
+ {% for meta_name, meta_content in (metadata | default({})).items()
24
+ if meta_name not in ("title", "description") and meta_content %}
25
+ {% if meta_name.startswith("og:") %}
26
+ <meta property="{{ meta_name }}" content="{{ meta_content }}" />
27
+ {% else %}
28
+ <meta name="{{ meta_name }}" content="{{ meta_content }}" />
29
+ {% endif %}
30
+ {% endfor %}
16
31
  <link rel="icon"
17
32
  href="/favicon.ico"
18
33
  type="image/x-icon"
@@ -96,10 +96,124 @@ failure. Coverage is in `tests/test_browser_log.py`.
96
96
  npm run check:fix
97
97
  ```
98
98
 
99
- That runs `settings/fix.py` (safely fixes lint issues dead imports, redundant
100
- code, etc.) and then re-runs the full gate so you see what's left. Type errors
101
- (pyright) and failing tests (pytest) are never auto-fixed — fix those at the
102
- reported `path:line:col`.
99
+ That runs `settings/fix.py`, which **formats first** (see below), then safely
100
+ fixes lint issues (dead imports, redundant code, etc.), then re-runs the full
101
+ gate so you see what's left. Type errors (pyright) and failing tests (pytest)
102
+ are never auto-fixed — fix those at the reported `path:line:col`.
103
+
104
+ ## Formatting
105
+
106
+ ```bash
107
+ npm run format # write
108
+ npm run format:check # report only; exits 1 if work remains
109
+ ```
110
+
111
+ `settings/format.py` formats two surfaces, **markup first, then Python**:
112
+
113
+ 1. **Markup** — the template inside every `html(r"""...""")`, via **djLint**.
114
+ 2. **Python** — `main.py`, `src/**`, `settings/**`, `tests/**`, via
115
+ `ruff format`, then the `html(` rejoin described below.
116
+
117
+ The order is load-bearing. Reformatting a template changes how many lines its
118
+ string literal spans, which changes how ruff wraps the enclosing `html(...)`
119
+ call — so ruff must run last, or a single pass never converges.
120
+
121
+ ### The `html(r"""` opening
122
+
123
+ The house style keeps the call and its template on one line, with the markup
124
+ starting on the next:
125
+
126
+ ```python
127
+ return html(r"""
128
+ <div>…</div>
129
+ """, name=name)
130
+ ```
131
+
132
+ `ruff format` will not produce that. Any call whose first argument is a
133
+ multiline string *and* which has other arguments gets exploded — in default and
134
+ preview style alike:
135
+
136
+ ```python
137
+ return html(
138
+ r"""
139
+ <div>…</div>
140
+ """,
141
+ name=name,
142
+ )
143
+ ```
144
+
145
+ So `format.py` rejoins the opening after ruff runs. The two steps feed each
146
+ other — rejoining the opening lets ruff also pull the closing `)` up on a
147
+ sole-argument call — so the pair is iterated to a fixed point. That is also why
148
+ `--check` replays the whole pipeline against a temporary mirror of the tree
149
+ instead of calling `ruff format --check`, which would flag every rejoined call
150
+ as unformatted.
151
+
152
+ **Running `ruff format` on its own (or on IDE save) will re-split these.**
153
+ `npm run format` puts them back.
154
+
155
+ ### Why djLint, and why every block is proved
156
+
157
+ The markup layers four dialects in one string — HTML, Jinja `{{ }}`/`{% %}`,
158
+ PulsePoint `{ }`, and JS in `<script>` — and they nest:
159
+
160
+ ```html
161
+ class="... {currentUrl === '{{ item['href'] }}' ? 'a' : 'b'}"
162
+ ```
163
+
164
+ Prettier has no Jinja awareness: it de-indents `{% for %}` blocks to column 0
165
+ and joins `{% endfor %} {% endfor %}` onto one line. djLint is Jinja-aware and
166
+ does not reflow text, so PulsePoint expressions survive.
167
+
168
+ djLint is still a *general* HTML formatter, so it is never trusted. It will
169
+ insert a newline between a block tag and an adjacent inline or `<x-*>` tag,
170
+ which renders as a visible space — a custom element's `display` comes from CSS
171
+ the formatter cannot see. So each block is formatted and then checked against
172
+ `settings/_markup_equivalence.py`, a tokenizer that decides whether the result
173
+ is **guaranteed** to render identically. Only proven blocks are written; the
174
+ rest are skipped and printed with the reason:
175
+
176
+ ```
177
+ skipped — djLint's output could not be proved to render identically:
178
+ src/app/crm/index.py
179
+ 8: would trim whitespace around 'Only Active' inside <button>, whose
180
+ display is not known to be block-level
181
+ ```
182
+
183
+ **A skip is not a failure.** It means the reformat would have changed rendering,
184
+ so the block was left alone. Do not "fix" a skip by loosening the oracle.
185
+
186
+ `<x-*>` tags are registered with djLint (`--custom-html`), so a component tree
187
+ nests instead of sitting flat:
188
+
189
+ ```html
190
+ <x-shell>
191
+ <x-brand />
192
+ <x-panel />
193
+ </x-shell>
194
+ ```
195
+
196
+ That does not weaken the check. The oracle still treats `<x-*>` as *inline*,
197
+ because a custom element's display comes from CSS — so indenting tags that were
198
+ already on separate lines is accepted (whitespace was there either way), while
199
+ separating two tags that were touching is still refused, since that inserts a
200
+ rendered space.
201
+
202
+ SVG elements (`<svg>`, `<path>`, `<g>`, …) count as block-level for a related
203
+ reason: an SVG fragment lays out no text, so indenting the children of an inline
204
+ `<svg>` in a component template cannot change what is drawn. `<text>`,
205
+ `<tspan>`, `<textPath>` and `<foreignObject>` are excluded — they do render
206
+ their content.
207
+
208
+ `<script>`, `<style>`, `<pre>`, and `<textarea>` bodies are masked out before
209
+ djLint runs, so code and preformatted text are preserved byte-for-byte by
210
+ construction rather than by proof — djLint otherwise reads `/>` inside a JS
211
+ regex as a tag delimiter and rewrites `.replace(/>/g, …)`.
212
+
213
+ Coverage is in `tests/test_format.py`. An oracle that wrongly returns *True* is
214
+ the only failure mode that silently corrupts pages, so the tests pin both
215
+ directions: transforms that must be recognised as safe, and transforms that must
216
+ never be.
103
217
 
104
218
  ### How unused-import (F401) removal stays safe
105
219
 
@@ -131,5 +245,6 @@ an `<x-*>` tag, so **the gate fails only on genuinely dead imports**. The
131
245
  - **pyright** — type checker. Config in `[tool.pyright]`: `include = ["main.py", "src", "settings/*.py"]` with `exclude = [".venv", "node_modules", "**/__pycache__"]`, so it checks `main.py`, all of `src` (including the generated `src/lib/prisma/**` ORM), and the top-level `settings/*.py` tooling scripts (mirroring ruff's `include`). Pylance reads the same config, so the IDE and `npm run check` agree.
132
246
  - **ruff** — linter. Config in `[tool.ruff]`; correctness-focused rules.
133
247
  - **pytest** — test runner. Tests live in `tests/`.
248
+ - **djlint** — markup formatter, driven by `settings/format.py`. Invoked with explicit flags (`--profile jinja --indent 2 --max-line-length 120 --preserve-blank-lines`) rather than a `[tool.djlint]` config block, because blocks are formatted in a temp directory outside the project where that config would not be found.
134
249
 
135
250
  Install/refresh them with `uv sync --group dev`.
@@ -82,6 +82,7 @@ def component_fixture_dir():
82
82
  finally:
83
83
  shutil.rmtree(scan_root, ignore_errors=True)
84
84
 
85
+
85
86
  # Keep the app in its non-production code paths during tests (dev security
86
87
  # headers, readable error messages, no HTTPS-only cookies).
87
88
  os.environ.setdefault("APP_ENV", "development")
@@ -16,9 +16,7 @@ import main
16
16
  async def _get(path: str) -> httpx2.Response:
17
17
  # ASGITransport speaks to the app in-process; no server/socket required.
18
18
  transport = httpx2.ASGITransport(app=main.app)
19
- async with httpx2.AsyncClient(
20
- transport=transport, base_url="http://testserver"
21
- ) as client:
19
+ async with httpx2.AsyncClient(transport=transport, base_url="http://testserver") as client:
22
20
  return await client.get(path)
23
21
 
24
22
 
@@ -96,7 +96,7 @@ class TestDeferComponentRoots:
96
96
 
97
97
  def test_component_root_is_wrapped_in_template(self):
98
98
  # The transform operates on a full document body, as in production.
99
- html = "<body><div pp-component=\"abc\"><span>x</span></div></body>"
99
+ html = '<body><div pp-component="abc"><span>x</span></div></body>'
100
100
  out = main.defer_component_roots(html)
101
101
  assert "<template" in out
102
102
  assert 'pp-component="abc"' in out
@@ -106,8 +106,8 @@ class TestDeferComponentRoots:
106
106
  '<body><div pp-component="abc" '
107
107
  'title="&#123;attr&#125;" '
108
108
  'class="probe &#x7b;active&#x7d;">'
109
- '&lbrace;text&rbrace;'
110
- '</div><p>&#123;outside&#125;</p></body>'
109
+ "&lbrace;text&rbrace;"
110
+ "</div><p>&#123;outside&#125;</p></body>"
111
111
  )
112
112
 
113
113
  out = main.defer_component_roots(html)
@@ -118,10 +118,7 @@ class TestDeferComponentRoots:
118
118
  assert "<p>&#123;outside&#125;</p>" in out
119
119
 
120
120
  def test_finalize_html_defers_and_preserves_plain_scripts(self):
121
- html = (
122
- "<body><div pp-component=\"abc\">"
123
- "<script>console.log(1)</script></div></body>"
124
- )
121
+ html = '<body><div pp-component="abc"><script>console.log(1)</script></div></body>'
125
122
  out = main.finalize_html(html)
126
123
  assert "<script>console.log(1)</script>" in out
127
124
  assert "<template" in out
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-caspian-app",
3
- "version": "1.3.6",
3
+ "version": "1.3.7",
4
4
  "description": "Scaffold a new Caspian project (FastAPI-powered reactive Python framework).",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",