@zerotal/arch 1.7.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- package/src/tools/searchDocs.ts +345 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Inertia Rendering Pages
|
|
3
|
+
description: Return a page from a controller, choose a component, and control the response.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rendering pages
|
|
7
|
+
|
|
8
|
+
Every Inertia response goes through one helper: `inertia()`. This section covers how it
|
|
9
|
+
resolves components, what it returns for each request type, controller-less page
|
|
10
|
+
routes, and how redirects behave.
|
|
11
|
+
|
|
12
|
+
## The inertia helper
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
function inertia(component: PageName, props?: RenderProps): Promise<void>;
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Call `inertia()` from any controller action. It reads the active request from
|
|
19
|
+
`RequestContext` (via AsyncLocalStorage), so there is **no context argument** — and
|
|
20
|
+
it sets `ctx.response` as a side effect. It is **async** (it resolves lazy/deferred props), so it
|
|
21
|
+
returns `Promise<void>` — always `return inertia(...)` (or `await` it):
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// app/controllers/PostController.ts
|
|
25
|
+
import type { HttpContext } from "zerotal";
|
|
26
|
+
import { inertia } from "@zerotal/inertia";
|
|
27
|
+
import { Post } from "../models/Post.ts";
|
|
28
|
+
|
|
29
|
+
export class PostController {
|
|
30
|
+
async index(ctx: HttpContext): Promise<void> {
|
|
31
|
+
const posts = await Post.query()
|
|
32
|
+
.withScopes((s) => s.published())
|
|
33
|
+
.with("author")
|
|
34
|
+
.orderBy("published_at", "desc")
|
|
35
|
+
.paginate(10, Number(ctx.query("page", "1")));
|
|
36
|
+
|
|
37
|
+
return inertia("Posts/Index", { posts });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async show(ctx: HttpContext): Promise<void> {
|
|
41
|
+
const post = await Post.query()
|
|
42
|
+
.where("slug", ctx.params["slug"]!)
|
|
43
|
+
.with("author")
|
|
44
|
+
.with("comments")
|
|
45
|
+
.firstOrFail();
|
|
46
|
+
|
|
47
|
+
return inertia("Posts/Show", { post });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`return inertia(...)` is idiomatic: the helper returns `Promise<void>`, so returning it ends the
|
|
53
|
+
action.
|
|
54
|
+
|
|
55
|
+
> **Warning** — `inertia()` takes `(component, props)` — _not_
|
|
56
|
+
> `inertia(ctx, component, props)`. The request is resolved from context
|
|
57
|
+
> automatically.
|
|
58
|
+
|
|
59
|
+
### Controlling which props are sent, and when
|
|
60
|
+
|
|
61
|
+
Props can be more than plain values. Wrap them to make them lazy, optional, deferred, or mergeable —
|
|
62
|
+
the foundation for partial reloads, "load more" lists, and deferred content:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// in a controller
|
|
66
|
+
import { inertia, optional, defer, merge } from "@zerotal/inertia";
|
|
67
|
+
|
|
68
|
+
return inertia("Users/Index", {
|
|
69
|
+
users: () => User.all(), // lazy — only evaluated when sent
|
|
70
|
+
roles: optional(() => Role.all()), // only on a partial reload that asks for it
|
|
71
|
+
stats: defer(() => computeStats()), // loaded after first paint
|
|
72
|
+
feed: merge(() => Post.paginate(15, page)), // appended on "load more"
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A unified `Inertia` facade exposes the full protocol API (`Inertia.render`, `Inertia.optional`,
|
|
77
|
+
`Inertia.defer`, `Inertia.merge`, `Inertia.share`, `Inertia.location`, …). See
|
|
78
|
+
[Data Props](/docs/inertia/props) for the full v3 feature set.
|
|
79
|
+
|
|
80
|
+
## Component resolution
|
|
81
|
+
|
|
82
|
+
The component string maps to a file under your pages directory (`resources/js/pages/`
|
|
83
|
+
by default, configurable via `pagesDir`), with the framework extension appended
|
|
84
|
+
(`.tsx` for React, `.vue` for Vue):
|
|
85
|
+
|
|
86
|
+
| `inertia(...)` call | Component file |
|
|
87
|
+
| ----------------------------- | ----------------------------------------- |
|
|
88
|
+
| `inertia("Dashboard")` | `resources/js/pages/Dashboard.tsx` |
|
|
89
|
+
| `inertia("Posts/Index")` | `resources/js/pages/Posts/Index.tsx` |
|
|
90
|
+
| `inertia("Admin/Users/Edit")` | `resources/js/pages/Admin/Users/Edit.tsx` |
|
|
91
|
+
|
|
92
|
+
Component names are validated — a name containing `..` or a leading `/` is rejected
|
|
93
|
+
to prevent path traversal.
|
|
94
|
+
|
|
95
|
+
### The name is checked at compile time
|
|
96
|
+
|
|
97
|
+
`inertia("Posts/Shwo", …)` does not compile: the name has to be a page in the
|
|
98
|
+
generated registry (`resources/js/pages.generated.ts`, rebuilt by
|
|
99
|
+
`bun zt inertia:build` and by `zt dev`). A renamed or misspelled page was a
|
|
100
|
+
runtime 500 before — the kind that reaches production because the route it lives
|
|
101
|
+
on is the one nobody clicked.
|
|
102
|
+
|
|
103
|
+
For a name that genuinely isn't known until runtime, `inertia.dynamic(name, props)`
|
|
104
|
+
takes any string and skips the check.
|
|
105
|
+
|
|
106
|
+
The props are checked too — see [Typed props](/docs/inertia/props#typed-props).
|
|
107
|
+
|
|
108
|
+
### Props serialization
|
|
109
|
+
|
|
110
|
+
Props are JSON-serialized into the page object. **Pass plain data, not live ORM
|
|
111
|
+
models with unloaded relations** — eager-load what the page needs (`.with("author")`)
|
|
112
|
+
or map to a plain shape. Shared props (`auth.user`) are already reduced to scalars
|
|
113
|
+
for you; see [Shared Props](/docs/inertia/props).
|
|
114
|
+
|
|
115
|
+
## First load vs. navigation
|
|
116
|
+
|
|
117
|
+
`inertia()` branches on the `X-Inertia` request header:
|
|
118
|
+
|
|
119
|
+
| Request | Response |
|
|
120
|
+
| ---------------------------------- | -------------------------------------------------------- |
|
|
121
|
+
| First load (no `X-Inertia` header) | Full HTML document with the page object JSON embedded |
|
|
122
|
+
| XHR navigation (`X-Inertia: true`) | JSON page object only (`Content-Type: application/json`) |
|
|
123
|
+
|
|
124
|
+
Both responses carry `Vary: X-Inertia` so browsers and CDNs cache the HTML and JSON
|
|
125
|
+
variants separately. The page object always includes the current `url` and asset
|
|
126
|
+
`version`.
|
|
127
|
+
|
|
128
|
+
## Controller-less routes
|
|
129
|
+
|
|
130
|
+
For pages that need no controller logic (marketing pages, static dashboards), render
|
|
131
|
+
straight from the route with the `Router.inertia()` macro (added by the package):
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// routes/web.ts
|
|
135
|
+
import { Router } from "zerotal";
|
|
136
|
+
|
|
137
|
+
Router.inertia("/about", "About/Index"); // no props
|
|
138
|
+
Router.inertia("/home", "Home/Index", { greeting: "Hello" }); // static props
|
|
139
|
+
Router.inertia("/admin", "Admin/Dashboard", [AuthMiddleware]); // middleware shorthand
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The third argument is polymorphic: pass a **props object**, or pass a **middleware
|
|
143
|
+
array** directly as a shorthand. To use both, pass props third and middleware fourth:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
// routes/web.ts
|
|
147
|
+
Router.inertia("/admin", "Admin/Dashboard", { title: "Admin" }, [AuthMiddleware]);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Redirects
|
|
151
|
+
|
|
152
|
+
After a non-GET action (a form POST/PUT/DELETE), redirect as usual — return a 302 and
|
|
153
|
+
[`InertiaMiddleware`](/docs/inertia/middleware) upgrades it to a **303** so the
|
|
154
|
+
browser issues a GET on the target instead of replaying the form:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// in a controller
|
|
158
|
+
async store(ctx: HttpContext): Promise<void> {
|
|
159
|
+
const post = await Post.create(await ctx.body());
|
|
160
|
+
ctx.flash("success", "Post created.");
|
|
161
|
+
return ctx.redirect(`/posts/${post.slug}`); // 302 → 303, then renders Posts/Show
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Validation failures redirect back with errors in the session, which surface as the
|
|
166
|
+
`errors` shared prop on the re-rendered page — again, see
|
|
167
|
+
[Shared Props](/docs/inertia/props).
|
|
168
|
+
|
|
169
|
+
### External redirects — Inertia.location
|
|
170
|
+
|
|
171
|
+
To send the browser to an external URL (or force a full-page visit), use `Inertia.location(url)`. On
|
|
172
|
+
an Inertia request it returns a `409` + `X-Inertia-Location` so the client does a `window.location`
|
|
173
|
+
visit; on a normal request it's a plain `302`:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// in a controller
|
|
177
|
+
import { Inertia } from "@zerotal/inertia";
|
|
178
|
+
|
|
179
|
+
return Inertia.location("https://billing.stripe.com/session/abc");
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Redirects to a target with a URL fragment (`/page#section`) are automatically converted to a `409` +
|
|
183
|
+
`X-Inertia-Redirect` on Inertia requests so the fragment is preserved across the visit. See
|
|
184
|
+
[External & fragment redirects](/docs/inertia/props#external-fragment-redirects).
|
|
185
|
+
|
|
186
|
+
## Next steps
|
|
187
|
+
|
|
188
|
+
- [Inertia overview](/docs/inertia) — the guide's front page and the rest of the sections.
|
|
189
|
+
- [Reference](/docs/inertia/references) — the full API surface in one table.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Inertia Server-Side Rendering
|
|
3
|
+
description: Render the first paint on the server, and what changes when you do.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Server-side rendering
|
|
7
|
+
|
|
8
|
+
By default Inertia renders the first page on the client. Server-side rendering (SSR)
|
|
9
|
+
renders the initial HTML on the server instead — better Time-to-First-Byte and
|
|
10
|
+
crawlable content — while subsequent navigations keep using the fast JSON path.
|
|
11
|
+
|
|
12
|
+
Zerotal offers two approaches: the **`/__ssr` endpoint** (standard Inertia SSR) and
|
|
13
|
+
**streaming SSR** via `inertiaStream()`.
|
|
14
|
+
|
|
15
|
+
## Which should I use?
|
|
16
|
+
|
|
17
|
+
- **Endpoint SSR** (`ssr: true`) — the standard Inertia SSR contract. Turn it on
|
|
18
|
+
globally and the Inertia client renders the first page through `POST /__ssr`. Use
|
|
19
|
+
this when you want crawlable, server-rendered HTML across the whole app with no
|
|
20
|
+
per-controller change.
|
|
21
|
+
- **Streaming SSR** (`inertiaStream()`) — swap `inertia()` for `inertiaStream()` in
|
|
22
|
+
the controllers whose initial document you want streamed for the fastest TTFB. Use
|
|
23
|
+
it selectively on heavy landing pages; everything else stays on `inertia()`.
|
|
24
|
+
|
|
25
|
+
## Endpoint SSR
|
|
26
|
+
|
|
27
|
+
Enable the SSR endpoint in config:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// config/inertia.ts
|
|
31
|
+
import { InertiaConfig } from "@zerotal/inertia";
|
|
32
|
+
import { env } from "zerotal";
|
|
33
|
+
|
|
34
|
+
export default InertiaConfig({
|
|
35
|
+
htmlTemplate: "./resources/app.html",
|
|
36
|
+
version: env("ASSET_VERSION", "1"),
|
|
37
|
+
ssr: true, // registers POST /__ssr — requires a server renderer
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
When `ssr: true`, `InertiaProvider` registers `POST /__ssr`, which accepts
|
|
42
|
+
`{ component, props, url }` and returns `{ body, head }` — the same contract as the
|
|
43
|
+
Inertia Node SSR server. On a first-page load the server renders the component into
|
|
44
|
+
the template instead of shipping an empty root `<div>`; subsequent navigations use
|
|
45
|
+
the normal JSON path. Pages render with the framework they're authored in: React
|
|
46
|
+
`.tsx` via `react-dom/server`, or Vue `.vue` via `@inertiajs/vue3` +
|
|
47
|
+
`vue/server-renderer` — install the server renderer for the framework(s) your app uses.
|
|
48
|
+
|
|
49
|
+
## Streaming SSR
|
|
50
|
+
|
|
51
|
+
`inertiaStream()` is a drop-in async alternative to `inertia()` that uses React 18's
|
|
52
|
+
`renderToReadableStream` to improve TTFB. Instead of buffering the whole render, it
|
|
53
|
+
streams the React output between the template's HTML prefix and suffix:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
│ HTML prefix (everything before <!-- @inertia -->) │ → browser starts parsing <head>
|
|
57
|
+
│ React component stream │ → above-the-fold content arrives early
|
|
58
|
+
│ <div> close + page JSON + HTML suffix │
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Swap `inertia()` → `inertiaStream()` and `await` it — nothing else changes:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// app/controllers/PostController.ts
|
|
65
|
+
import { inertiaStream } from "@zerotal/inertia";
|
|
66
|
+
|
|
67
|
+
export class PostController {
|
|
68
|
+
async show(ctx: HttpContext): Promise<void> {
|
|
69
|
+
const post = await Post.findOrFail(ctx.params["id"]);
|
|
70
|
+
return inertiaStream("Posts/Show", { post });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Requirements
|
|
76
|
+
|
|
77
|
+
- `react-dom/server` ≥ 18 (for `renderToReadableStream`)
|
|
78
|
+
- The HTML template must contain `<!-- @inertia -->`
|
|
79
|
+
- The page component must exist under your pages directory (`resources/js/pages/<component>.tsx`)
|
|
80
|
+
|
|
81
|
+
It throws if the template hasn't loaded, or if the component name contains path
|
|
82
|
+
traversal (`..` or a leading `/`).
|
|
83
|
+
|
|
84
|
+
### inertia vs. inertiaStream
|
|
85
|
+
|
|
86
|
+
| | `inertia()` | `inertiaStream()` |
|
|
87
|
+
| -------------- | ---------------------- | ---------------------------------- |
|
|
88
|
+
| Return type | `Promise<void>` | `Promise<void>` |
|
|
89
|
+
| Rendering | Buffered HTML string | Streaming `renderToReadableStream` |
|
|
90
|
+
| Response body | Fully buffered string | Streaming `ReadableStream` |
|
|
91
|
+
| TTFB | After full render | After the prefix is flushed |
|
|
92
|
+
| XHR navigation | JSON (the normal path) | N/A — only the first-page document |
|
|
93
|
+
|
|
94
|
+
For XHR navigations (`X-Inertia: true`), keep using `inertia()` — streaming only
|
|
95
|
+
benefits the initial HTML document load.
|
|
96
|
+
|
|
97
|
+
> **Tip** — Stream the heaviest landing pages and leave everything else on `inertia()`.
|
|
98
|
+
|
|
99
|
+
## Next steps
|
|
100
|
+
|
|
101
|
+
- [Inertia overview](/docs/inertia) — the guide's front page and the rest of the sections.
|
|
102
|
+
- [Reference](/docs/inertia/references) — the full API surface in one table.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Inspirations & Attributions
|
|
3
|
+
description: The frameworks, runtimes, and tools that shaped Zerotal's design — what it borrowed, adapted, and where it diverged.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Inspirations & Attributions
|
|
7
|
+
|
|
8
|
+
Zerotal is built on the shoulders of giants. This page acknowledges the frameworks,
|
|
9
|
+
runtimes, and tools that shaped its design — what we borrowed, what we adapted, and
|
|
10
|
+
where we intentionally diverged.
|
|
11
|
+
|
|
12
|
+
Zerotal is a full-stack TypeScript framework built natively for the [Bun](https://bun.sh)
|
|
13
|
+
runtime, and to get there we drew on the best patterns from across the industry. The
|
|
14
|
+
influences below are grouped by the part of the stack they shaped.
|
|
15
|
+
|
|
16
|
+
## The Runtime & Foundation
|
|
17
|
+
|
|
18
|
+
### Bun
|
|
19
|
+
|
|
20
|
+
Bun is not just a runtime dependency — it is the reason Zerotal exists in the form it
|
|
21
|
+
does. Without Bun's native primitives there would be no coherent single-package story:
|
|
22
|
+
|
|
23
|
+
- **`Bun.SQL`** — the native SQLite/PostgreSQL client is the foundation of the ORM.
|
|
24
|
+
No `pg`, no `better-sqlite3`, no driver-abstraction overhead.
|
|
25
|
+
- **`Bun.serve`** — the HTTP server. No `http.createServer`, no Express under the hood.
|
|
26
|
+
- **`Bun.file` / `Bun.write`** — power the local storage driver and asset pipeline.
|
|
27
|
+
- **`Bun.markdown`** — renders this documentation site straight from the `docs/*.md`
|
|
28
|
+
source files.
|
|
29
|
+
- **`bun:test`** — the testing helpers (`TestApp`, `TestResponse`, factories) are built
|
|
30
|
+
directly on `bun:test`'s `describe` / `it` / `expect` primitives.
|
|
31
|
+
- **Bun plugin API** — `bun-plugin-tailwind` and the JS bundler used by `FlowProvider`
|
|
32
|
+
run inside Bun's native plugin lifecycle.
|
|
33
|
+
|
|
34
|
+
Where we diverged: Zerotal makes no attempt to also run on Node. Committing fully to one
|
|
35
|
+
runtime is what keeps the stack small — one SQL client, one server, one test runner —
|
|
36
|
+
instead of abstracting over several.
|
|
37
|
+
|
|
38
|
+
### Temporal
|
|
39
|
+
|
|
40
|
+
The TC39 `Temporal` proposal directly underpins `Carbon`, Zerotal's date library. All
|
|
41
|
+
date arithmetic uses `Temporal.PlainDate`, `Temporal.ZonedDateTime`, and friends — never
|
|
42
|
+
the legacy `Date` object — and `Carbon` is built on Bun's bundled `Temporal`
|
|
43
|
+
implementation rather than wrapping `Date`.
|
|
44
|
+
|
|
45
|
+
Where we diverged: `Carbon` is a thin ergonomic layer over `Temporal`, not a Moment/Day.js
|
|
46
|
+
clone — it tracks the proposal toward stable rather than inventing its own date model.
|
|
47
|
+
|
|
48
|
+
## Architectural Titans
|
|
49
|
+
|
|
50
|
+
### Laravel
|
|
51
|
+
|
|
52
|
+
Laravel is the gold standard for developer-friendly full-stack frameworks. Its
|
|
53
|
+
influence on Zerotal is pervasive:
|
|
54
|
+
|
|
55
|
+
- **Active Record ORM** — `Model`, attribute decorators (`@column`, `@belongsTo`,
|
|
56
|
+
`@hasMany`, `@hasOne`), and the query builder API mirror Eloquent's ergonomics.
|
|
57
|
+
- **Service providers** — the `onRegister` / `onBooting` / `onBooted` lifecycle
|
|
58
|
+
(through to `onStarting` / `onStarted` / `onStopping` / `onStopped`) maps directly
|
|
59
|
+
to Laravel's provider boot cycle.
|
|
60
|
+
- **Artisan-style CLI** — the `zt` command runner and generator scaffolding
|
|
61
|
+
(`make:model`, `make:controller`, `migrate`, …) take heavy cues from Artisan.
|
|
62
|
+
- **Mail notifications** — `Notify.send(user, new WelcomeNotification())`, with the
|
|
63
|
+
email built fluently inside `toMail()` (`new MailMessage().subject(...).line(...)`),
|
|
64
|
+
is a conscious homage to Laravel's mail/notification API.
|
|
65
|
+
- **Facades** — `Notify`, `Cache`, `Storage`, `Queue`, `Auth`, and `Log` follow the
|
|
66
|
+
static-proxy pattern, resolving a container singleton on each call.
|
|
67
|
+
- **Gate / Policy authorization, password broker, and the `validate()` request flow**
|
|
68
|
+
all follow Laravel's shapes.
|
|
69
|
+
|
|
70
|
+
Where we diverged: Zerotal drops PHP's runtime magic (no dynamic proxies, no
|
|
71
|
+
`__get`/`__set` trickery) in favour of explicit TypeScript decorators and genuine
|
|
72
|
+
static typing throughout.
|
|
73
|
+
|
|
74
|
+
### Rails
|
|
75
|
+
|
|
76
|
+
Before Laravel, Ruby on Rails established the patterns both frameworks inherit: the
|
|
77
|
+
**Active Record** model pattern itself, **convention over configuration**, and
|
|
78
|
+
**versioned, runnable migrations**. Zerotal's `Model.create()` / `Model.find()` surface
|
|
79
|
+
and its migration runner trace their lineage here.
|
|
80
|
+
|
|
81
|
+
Where we diverged: the same convention-over-configuration spirit, expressed through
|
|
82
|
+
explicit TypeScript types and decorators rather than Ruby metaprogramming — conventions
|
|
83
|
+
you can follow by reading types, not by memorising magic.
|
|
84
|
+
|
|
85
|
+
## The TypeScript & Node Ecosystem
|
|
86
|
+
|
|
87
|
+
### NestJS
|
|
88
|
+
|
|
89
|
+
NestJS demonstrated that a decorator-driven IoC container could feel natural in
|
|
90
|
+
TypeScript. Zerotal's dependency-injection story echoes it:
|
|
91
|
+
|
|
92
|
+
- **Container + decorators** — `@inject(...)` marks a class for auto-wiring,
|
|
93
|
+
declaring its constructor dependencies as explicit tokens the container
|
|
94
|
+
resolves in order.
|
|
95
|
+
- **Provider modules** — grouping bindings and boot logic into provider classes
|
|
96
|
+
parallels Nest's module/provider model.
|
|
97
|
+
|
|
98
|
+
Where we diverged: Zerotal uses standard **TC39 decorators** with no `reflect-metadata`
|
|
99
|
+
dependency — dependencies are declared explicitly as `@inject(...)` tokens rather than
|
|
100
|
+
inferred from constructor parameter types.
|
|
101
|
+
|
|
102
|
+
### AdonisJS
|
|
103
|
+
|
|
104
|
+
AdonisJS proved that a Laravel-flavoured framework could thrive in the JavaScript
|
|
105
|
+
ecosystem. Several Zerotal conventions trace back to Adonis:
|
|
106
|
+
|
|
107
|
+
- **`HttpContext` as the single per-request object** — passing one rich context object
|
|
108
|
+
through the entire pipeline (rather than `req`/`res` pairs) was validated by Adonis.
|
|
109
|
+
- **IoC container token conventions** — string-keyed bindings (`'db'`, `'cache'`,
|
|
110
|
+
`'queue'`, `'events'`, `'log'`) alongside class and Symbol tokens follow the pattern
|
|
111
|
+
Adonis popularised.
|
|
112
|
+
- **Lucid-inspired query scopes** — named scopes (`Model.scope(...)`,
|
|
113
|
+
`Model.query().withScopes(...)`) and global scopes parallel Lucid's model scopes.
|
|
114
|
+
|
|
115
|
+
Where we diverged: Zerotal keeps views as typed TypeScript (template literals / JSX)
|
|
116
|
+
rather than adopting a dedicated template engine like Adonis's Edge — one less language
|
|
117
|
+
to learn, and views type-check against the data you pass them.
|
|
118
|
+
|
|
119
|
+
### Koa
|
|
120
|
+
|
|
121
|
+
Zerotal's middleware pipeline is a Koa-style **onion model**, not a Laravel-style
|
|
122
|
+
before/after/terminate kernel:
|
|
123
|
+
|
|
124
|
+
- **`handle(ctx, next)`** — each middleware is a `Pipe` with a single `handle` method
|
|
125
|
+
that receives the `HttpContext` and a `next()` continuation, awaits downstream
|
|
126
|
+
middleware, and can run logic on the way out — exactly Koa's cascading model.
|
|
127
|
+
- **Short-circuiting** — a middleware that returns without calling `next()` stops the
|
|
128
|
+
chain, the same way Koa middleware can decline to continue.
|
|
129
|
+
|
|
130
|
+
Where we diverged: Koa is deliberately minimal — middleware and little else. Zerotal keeps
|
|
131
|
+
the onion model but ships a batteries-included stack on top of it, and the context is a
|
|
132
|
+
richly-typed `HttpContext` rather than a bare object. Post-response work (Laravel's
|
|
133
|
+
`terminate`) is handled via `ctx.afterResponse(...)` callbacks rather than a third phase.
|
|
134
|
+
|
|
135
|
+
## Reactivity & The Frontend
|
|
136
|
+
|
|
137
|
+
### Next.js
|
|
138
|
+
|
|
139
|
+
Next.js normalised several patterns that Zerotal adopted for its front-end story:
|
|
140
|
+
|
|
141
|
+
- **File-based routing** — a `routes/` directory with `GET`, `POST`, etc. named exports
|
|
142
|
+
mirrors the Next.js App Router file convention.
|
|
143
|
+
- **Co-located route handlers** — keeping data-fetching logic next to the route that
|
|
144
|
+
uses it (rather than in a separate controllers directory) is a Next.js-influenced
|
|
145
|
+
option.
|
|
146
|
+
- **`_layout.ts`** — a persistent shell that wraps every page in a section of the app
|
|
147
|
+
comes directly from Next.js layouts.
|
|
148
|
+
|
|
149
|
+
Where we diverged: file-based routing is _one option_, not the whole story — routes can
|
|
150
|
+
equally be registered explicitly with `Router.get(...)` — and everything runs
|
|
151
|
+
server-side on Bun, with no client bundler or React Server Components coupling.
|
|
152
|
+
|
|
153
|
+
### Livewire / Alpine.js
|
|
154
|
+
|
|
155
|
+
Flow — Zerotal's first-party, server-driven reactive component system — draws from the
|
|
156
|
+
Livewire mental model:
|
|
157
|
+
|
|
158
|
+
- **Server-driven reactivity** — component state lives on the server; only diffs are
|
|
159
|
+
sent to the client.
|
|
160
|
+
- **`@locked` / `@expose` decorators** — the distinction between server-private and
|
|
161
|
+
client-accessible properties mirrors Livewire's `#[Locked]` and `wire:model` boundary.
|
|
162
|
+
- **SPA `navigate` transitions** — Livewire's `livewire:navigate` was the direct
|
|
163
|
+
reference point for Flow's fetch-and-swap page navigation.
|
|
164
|
+
|
|
165
|
+
Where we diverged: Flow is pure TypeScript with no PHP/Blade runtime — components are
|
|
166
|
+
typed classes, and the diffing/transport protocol is Zerotal's own.
|
|
167
|
+
|
|
168
|
+
### Inertia.js
|
|
169
|
+
|
|
170
|
+
Zerotal's first-party Inertia adapter (`@zerotal/inertia`) is built on Inertia.js
|
|
171
|
+
itself. The adapter layer follows the same server-side adapter conventions established
|
|
172
|
+
by the official Laravel and Rails adapters.
|
|
173
|
+
|
|
174
|
+
Where we diverged: the adapter ships first-party and is versioned with the framework,
|
|
175
|
+
rather than living as a separately-maintained community package.
|
|
176
|
+
|
|
177
|
+
### shadcn/ui
|
|
178
|
+
|
|
179
|
+
`@zerotal/flow-ui` follows the model shadcn/ui established, and the debt is worth naming
|
|
180
|
+
plainly:
|
|
181
|
+
|
|
182
|
+
- **You own the code** — `bun zt flow:add button` copies the source into your app rather
|
|
183
|
+
than adding a dependency you can only configure from the outside. `flow:add` is the
|
|
184
|
+
same idea as `npx shadcn add`, down to the registry manifest behind it.
|
|
185
|
+
- **Tokens over props** — re-theming happens by overriding CSS variables, not by
|
|
186
|
+
threading a theme object through every component. The default token names
|
|
187
|
+
(`background`, `card`, `primary`, `muted`, `accent`, `destructive`, `border`, `input`,
|
|
188
|
+
`ring`) are shadcn's, so a palette written for one drops into the other.
|
|
189
|
+
- **Styled wrappers over headless primitives** — shadcn styles Radix; flow-ui styles
|
|
190
|
+
Flow's own headless layer, which fills the same role.
|
|
191
|
+
- **The catalogue** — the component list was chosen by working through shadcn's and
|
|
192
|
+
asking which entries a Zerotal application would actually reach for.
|
|
193
|
+
|
|
194
|
+
Where we diverged: components render on the server and return HTML rather than React
|
|
195
|
+
elements, so there is no client component tree; the `Chart` component draws SVG on the
|
|
196
|
+
server instead of wrapping a charting library; and the AI-chat components have no
|
|
197
|
+
equivalent here.
|
|
198
|
+
|
|
199
|
+
## Tooling, Data & Telemetry
|
|
200
|
+
|
|
201
|
+
### Zod
|
|
202
|
+
|
|
203
|
+
The validator's API is shaped by Zod's fluent, inferable schemas:
|
|
204
|
+
|
|
205
|
+
- **Chained rule builders** — `r.string().min(3).max(255)`, `r.number().integer()`,
|
|
206
|
+
`r.array(r.string()).optional()` read like Zod schemas.
|
|
207
|
+
- **Type inference** — `Infer<>` derives the fully-typed result of a schema, so
|
|
208
|
+
validated data needs no manual casts — the same payoff as Zod's `z.infer`.
|
|
209
|
+
|
|
210
|
+
Where we diverged: Zerotal's validator is wired into the HTTP request lifecycle
|
|
211
|
+
(`FormRequest`, the `validate(ctx, …)` helper, automatic 422/redirect handling) rather
|
|
212
|
+
than being a standalone parsing library.
|
|
213
|
+
|
|
214
|
+
### Monolog
|
|
215
|
+
|
|
216
|
+
Zerotal's logger borrows Monolog's channel architecture (by way of Laravel's logging):
|
|
217
|
+
|
|
218
|
+
- **Named channels with per-channel level thresholds** — `console`, `single`, `daily`,
|
|
219
|
+
`stack`, `null`, each filtering below its configured minimum level.
|
|
220
|
+
- **Stack channels** — fan a single entry out to several channels at once.
|
|
221
|
+
- **Daily rotation with retention** — date-stamped files pruned after `days`.
|
|
222
|
+
|
|
223
|
+
Where we diverged: the logger is a lean, five-driver reimplementation — there is no PHP
|
|
224
|
+
handler/processor ecosystem to port, just the channel model that earns its keep.
|
|
225
|
+
|
|
226
|
+
### Laravel Telescope / Pulse
|
|
227
|
+
|
|
228
|
+
Zerotal's devtools (`@zerotal/devtools`) take their cue from Telescope and Pulse:
|
|
229
|
+
|
|
230
|
+
- **Per-request traces** — queries, cache operations, mail, jobs, and logs captured for
|
|
231
|
+
each request and streamed live to an in-browser dashboard.
|
|
232
|
+
- **Slow-query and N+1 detection** — surfaced automatically, the way Telescope flags
|
|
233
|
+
slow queries.
|
|
234
|
+
|
|
235
|
+
Where we diverged: rather than each domain package depending on devtools, everything is
|
|
236
|
+
captured through the synchronous **`FrameworkEvents`** instrumentation bus in
|
|
237
|
+
`@zerotal/core` — devtools (and the logger, and any metrics layer) simply subscribe, so
|
|
238
|
+
the domain packages never import their observers. See [Events](/docs/events).
|
|
239
|
+
|
|
240
|
+
### Prisma / Drizzle
|
|
241
|
+
|
|
242
|
+
The class-based migration format (`export default class extends Migration` with `up()`
|
|
243
|
+
and `down()` methods) and the `migrate` / `migrate:fresh` CLI commands were shaped by
|
|
244
|
+
observing what Prisma and Drizzle got right.
|
|
245
|
+
|
|
246
|
+
Where we diverged: migrations stay as ordinary TypeScript you can read and run — no
|
|
247
|
+
schema DSL, no generated client step — keeping the database layer transparent rather
|
|
248
|
+
than hidden behind a code-generation pipeline.
|
|
249
|
+
|
|
250
|
+
If you see your work reflected here and feel it is misrepresented or missing, please
|
|
251
|
+
open an issue or pull request — attribution matters.
|
|
252
|
+
|
|
253
|
+
## Next steps
|
|
254
|
+
|
|
255
|
+
- [Getting started](/docs/getting-started) — scaffold your first Zerotal app.
|
|
256
|
+
- [Application](/docs/application) — how the framework is wired together.
|
|
257
|
+
- [Contributing](/docs/contributing) — help shape the framework.
|