@orieljs/framework 0.1.0-alpha.1 → 0.1.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-WI7E7Q46.js → chunk-24673JQF.js} +8 -4
  5. package/dist/chunk-24673JQF.js.map +1 -0
  6. package/dist/{chunk-K6MAA3RM.js → chunk-HTJKT2R2.js} +9 -1
  7. package/dist/chunk-HTJKT2R2.js.map +1 -0
  8. package/dist/chunk-KDE5BLI3.js +8776 -0
  9. package/dist/chunk-KDE5BLI3.js.map +1 -0
  10. package/dist/chunk-MURTCTRH.js +3253 -0
  11. package/dist/chunk-MURTCTRH.js.map +1 -0
  12. package/dist/contracts.d.ts +852 -168
  13. package/dist/contracts.js +9 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +3015 -5160
  16. package/dist/index.js +7628 -2640
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-B8J6xq-7.d.ts → jsx-runtime-6um4Mwfb.d.ts} +99 -149
  19. package/dist/jsx-runtime.d.ts +3 -1
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/testing-CKwfWJGn.d.ts +1586 -0
  22. package/dist/testing.d.ts +3 -1408
  23. package/dist/testing.js +29 -5
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +654 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +371 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/export.md +93 -0
  46. package/docs/facades.md +278 -0
  47. package/docs/hashing.md +118 -0
  48. package/docs/helpers.md +1281 -0
  49. package/docs/http-tests.md +386 -0
  50. package/docs/inertia.md +485 -0
  51. package/docs/installation.md +219 -0
  52. package/docs/lifecycle.md +234 -0
  53. package/docs/live-tests.md +226 -0
  54. package/docs/live.md +910 -0
  55. package/docs/logging.md +255 -0
  56. package/docs/mail.md +619 -0
  57. package/docs/middleware.md +385 -0
  58. package/docs/migrations.md +416 -0
  59. package/docs/mocking.md +264 -0
  60. package/docs/navigation.json +277 -0
  61. package/docs/notifications.md +225 -0
  62. package/docs/pagination.md +247 -0
  63. package/docs/passwords.md +207 -0
  64. package/docs/providers.md +187 -0
  65. package/docs/queries.md +600 -0
  66. package/docs/queues.md +330 -0
  67. package/docs/requests.md +533 -0
  68. package/docs/responses.md +444 -0
  69. package/docs/routing.md +738 -0
  70. package/docs/scheduling.md +277 -0
  71. package/docs/seeding.md +122 -0
  72. package/docs/session.md +409 -0
  73. package/docs/starter-kits.md +399 -0
  74. package/docs/storage.md +192 -0
  75. package/docs/structure.md +264 -0
  76. package/docs/testing.md +165 -0
  77. package/docs/time-travel.md +138 -0
  78. package/docs/validation.md +1792 -0
  79. package/docs/verification.md +152 -0
  80. package/docs/views.md +566 -0
  81. package/docs/vite.md +400 -0
  82. package/package.json +37 -30
  83. package/dist/chunk-AI2RFMBT.js +0 -745
  84. package/dist/chunk-AI2RFMBT.js.map +0 -1
  85. package/dist/chunk-BZPCRKFQ.js +0 -2941
  86. package/dist/chunk-BZPCRKFQ.js.map +0 -1
  87. package/dist/chunk-K6MAA3RM.js.map +0 -1
  88. package/dist/chunk-WI7E7Q46.js.map +0 -1
@@ -0,0 +1,219 @@
1
+ # Installation
2
+
3
+ Oriel is a full-stack TypeScript framework for Node and Bun, in the spirit of Laravel. Create an app with your
4
+ package manager and one of seven starter kits.
5
+
6
+ ## Meet Oriel
7
+
8
+ Oriel gives a TypeScript app what Laravel gives a PHP one: routing, controllers and middleware, models and
9
+ migrations, validation, authentication, queues, mail and the scheduler, under Laravel's names. Your pages are Inertia
10
+ pages in React, Vue, Svelte, Solid or Angular, or Live components rendered on the server, and the same app can serve a
11
+ JSON API.
12
+
13
+ ## Creating an Oriel application
14
+
15
+ ### Requirements
16
+
17
+ You need **Node 22** or later, or **Bun 1.2** or later. Nothing is installed globally: the installer runs through
18
+ your package manager, and each app has its own `oriel` command. A new app keeps its data in SQLite, so you don't need
19
+ a database server to start.
20
+
21
+ ### Creating an application
22
+
23
+ Create an app with your package manager's `create` command:
24
+
25
+ ```sh
26
+ npm create @orieljs@latest blog
27
+ ```
28
+
29
+ The same installer runs with pnpm, Bun and Yarn:
30
+
31
+ ```sh
32
+ pnpm create @orieljs blog
33
+ bun create @orieljs blog
34
+ yarn create @orieljs blog
35
+ ```
36
+
37
+ `npm create oriel-app` is the same installer under another name: both run `create-oriel-app` at the same version.
38
+
39
+ The installer asks for the app's name, unless you gave one, then for a stack (Inertia, Live or an API) and, for Inertia,
40
+ its framework, and last whether to install the dependencies and start a git repository. The name is the last part of the
41
+ folder you create the app in, and it must be a valid npm package name: lower case, with no spaces. When the folder isn't
42
+ empty, the installer asks before it removes the files in it.
43
+
44
+ ### Choosing a starter kit
45
+
46
+ The kits with pages come with registration, login, email verification, password reset, a dashboard, and profile,
47
+ password and appearance settings. The API kit has registration, login, logout and password reset as JSON endpoints,
48
+ with personal access tokens and no pages. Pick one in the prompt, or name it with an option:
49
+
50
+ | Option | The kit |
51
+ | --- | --- |
52
+ | `--inertia=react` | Inertia pages in React (a bare `--inertia` means React) |
53
+ | `--inertia=vue`, `--inertia=svelte`, `--inertia=solid`, `--inertia=angular` | Inertia pages in Vue, Svelte, Solid or Angular |
54
+ | `--live` | Live: server-driven components written in TSX |
55
+ | `--api` | A JSON API with personal access tokens, and no pages |
56
+
57
+ [Starter kits](starter-kits.md) describes what each kit contains.
58
+
59
+ > [!NOTE]
60
+ > With npm, the options go after `--`, so npm passes them on to the installer:
61
+ > `npm create @orieljs@latest blog -- --inertia=vue`. pnpm, Bun and Yarn take them directly.
62
+
63
+ ### Installer options
64
+
65
+ | Option | What it does |
66
+ | --- | --- |
67
+ | `--pm <npm\|pnpm\|yarn\|bun>` | The package manager to use (by default, the one running the installer) |
68
+ | `--install`, `--no-install` | Install the dependencies, set the app key, create the database and build the assets (default: yes) |
69
+ | `--git`, `--no-git` | Start a git repository with a first commit (default: yes) |
70
+ | `-f`, `--force` | Empty a folder that isn't empty, without asking (a `.git` folder stays) |
71
+ | `-y`, `--yes` | Ask nothing, and take the defaults for everything not given: `oriel-app`, React, install, git |
72
+ | `-h`, `--help` | List the options |
73
+ | `-v`, `--version` | Print the installer's version |
74
+
75
+ The installer asks its questions only in a terminal: when its input and output are a terminal, the `CI` variable isn't
76
+ set and you didn't pass `--yes`. In a script or on CI, pass the kit as an option or `--yes`; without either, the
77
+ installer stops with "No stack chosen: pass --inertia[=<framework>], --live or --api (or --yes for React).". A folder
78
+ that isn't empty needs `--force` there.
79
+
80
+ ### What the installer does
81
+
82
+ It copies the kit into the new folder and writes the app's `package.json` for your package manager. It writes `.env`
83
+ from `.env.example`, with `APP_NAME` set to the app's name, and a CI workflow, `.github/workflows/ci.yml`, that
84
+ installs the app, migrates a fresh database, type-checks and tests the app, builds its assets, and checks that it
85
+ serves. The workflow runs on Node 22, Node 24 and Bun, or on Bun alone for an app made with Bun.
86
+
87
+ Unless you pass `--no-install`, it then runs the first commands for you:
88
+
89
+ ```sh
90
+ npm install
91
+ node oriel key:generate
92
+ node oriel migrate --force
93
+ npm run build
94
+ ```
95
+
96
+ `key:generate` writes your app's secret key, `APP_KEY`, into `.env`, and `migrate` creates the SQLite database,
97
+ `database/database.sqlite`, with the tables a new app needs: users, password reset tokens, sessions, cache, cache locks,
98
+ jobs, job batches, failed jobs, personal access tokens and notifications. The API kit has no assets, so it has no
99
+ `build` step.
100
+
101
+ Last, unless you pass `--no-git`, it runs `git init` and makes a first commit, "Initial commit". It skips this when
102
+ git isn't installed or the folder is inside a git repository already. When the commit fails, for example because
103
+ git has no `user.name` and `user.email` yet, the repository stays, with your files added but not committed.
104
+
105
+ ### Package managers
106
+
107
+ The installer works with npm, pnpm, Yarn (Yarn 1, and Yarn 2 or later such as Yarn 4) and Bun. The app it writes
108
+ follows the one you chose:
109
+
110
+ - **pnpm:** `package.json` names pnpm and its version in `packageManager`, and `pnpm-workspace.yaml` lists the
111
+ dependencies allowed to run install scripts (before pnpm 10.26, a `pnpm` field in `package.json` does).
112
+ - **Yarn 2 or later:** `package.json` names Yarn and its version in `packageManager`, and `.yarnrc.yml` sets
113
+ `nodeLinker: node-modules`, since the app's tools don't run under Plug'n'Play. Yarn 1 needs neither.
114
+ - **Bun:** the app runs `bun oriel` wherever another app runs `node oriel`: in its scripts, in the commands the
115
+ installer runs and prints, and in its CI workflow, which runs on Bun only.
116
+
117
+ The commands in the kit's `README.md` are rewritten for your package manager too.
118
+
119
+ ## Running your application
120
+
121
+ When the installer is done, it prints the commands to run while you work:
122
+
123
+ ```sh
124
+ cd blog
125
+ npm run dev
126
+ node oriel serve --watch
127
+ ```
128
+
129
+ Run the last two in two terminals. In one, `npm run dev` starts Vite, which serves your pages' scripts and styles and
130
+ updates them in the browser as you edit them. In the other, `node oriel serve --watch` starts your app's server.
131
+
132
+ Your app is at [http://localhost:8000](http://localhost:8000). `--watch` restarts the server when a file it runs
133
+ changes: a TypeScript, JavaScript or JSON file in `app`, `bootstrap`, `config`, `lang`, `resources/lang`,
134
+ `resources/views` or `routes`, `.env` or another `.env.*` file (not `.env.example`), `package.json` or
135
+ `tsconfig.json`. Your pages' own scripts and styles, in `resources/js` and `resources/css`, are Vite's, and don't
136
+ restart it.
137
+
138
+ > [!NOTE]
139
+ > Vite reloads the page when a view, a route file or a Live component changes, often before the server has
140
+ > restarted, and it doesn't reload the page again once the new server is up. After `serve --watch` prints
141
+ > "Restarting server...", reload the page once more to see your change.
142
+
143
+ `oriel serve` listens on `127.0.0.1`, on port 8000 or the one the `PORT` variable names. `--host` and `--port` choose
144
+ others:
145
+
146
+ ```sh
147
+ node oriel serve --watch --host=0.0.0.0 --port=3000
148
+ ```
149
+
150
+ The API kit has no Vite config and no `dev` script: `node oriel serve --watch` is all it needs. An app made with Bun
151
+ runs `bun run dev` and `bun oriel serve --watch`.
152
+
153
+ If you passed `--no-install`, the printed commands start with the steps the installer skipped: `npm install`,
154
+ `node oriel key:generate`, `node oriel migrate` and `npm run build`.
155
+
156
+ > [!TIP]
157
+ > Every command the framework knows is listed by `node oriel list`, and `node oriel help <command>` explains one.
158
+
159
+ ## Initial configuration
160
+
161
+ Your app's settings are in the `config` folder, one file per part of the framework: `config/app.ts`,
162
+ `config/database.ts`, `config/session.ts` and so on. A new app runs as it is, but you may want to look at
163
+ `config/app.ts`, which holds the app's name, URL and locale.
164
+
165
+ ### Environment based configuration
166
+
167
+ The values that differ between your machine and your servers, such as the database's password or the mail server,
168
+ come from the `.env` file at the root of your app, which the config files read with `env()`. The installer writes
169
+ `.env` for you, and git ignores it: each machine has its own, and its secrets stay out of your repository.
170
+ [Configuration](configuration.md) explains the file and `env()`.
171
+
172
+ ### Databases and migrations
173
+
174
+ A new app uses SQLite, in `database/database.sqlite`, which the installer created and migrated. To use PostgreSQL or
175
+ MySQL instead, install its driver, `pg` or `mysql2`:
176
+
177
+ ```sh
178
+ npm install pg
179
+ ```
180
+
181
+ then set the `DB_*` variables in `.env`:
182
+
183
+ ```dotenv
184
+ DB_CONNECTION=pgsql
185
+ DB_HOST=127.0.0.1
186
+ DB_PORT=5432
187
+ DB_DATABASE=blog
188
+ DB_USERNAME=root
189
+ DB_PASSWORD=
190
+ ```
191
+
192
+ Create the database on your server, then run your migrations:
193
+
194
+ ```sh
195
+ node oriel migrate
196
+ ```
197
+
198
+ For MySQL, `DB_CONNECTION` is `mysql` and the port is usually 3306.
199
+
200
+ ## Editor support
201
+
202
+ The OrielJS extension for VS Code covers the strings Oriel reads at run time, where TypeScript can't help: it completes
203
+ validation rules, `config()` keys, `env()` names, view names, and the actions and properties in Live's `o:` attributes,
204
+ shows their docs when you hover, and warns about one your app doesn't have. It also runs `route:types` and
205
+ `inertia:types` again when your routes, controllers or pages change, and puts the `oriel` commands in the command
206
+ palette. Install it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=orieljs.orieljs),
207
+ or search for OrielJS in the editor's Extensions view. It needs Oriel 0.1.0-alpha.9 or later, and reads your app only
208
+ in a trusted workspace.
209
+
210
+ ## Next steps
211
+
212
+ Read how an app is laid out in [Directory structure](structure.md), and how it reads its settings in
213
+ [Configuration](configuration.md). Your app's routes live in `routes/web.ts`: [Routing](routing.md) is the place to
214
+ start writing your own code. When a request has slow work to do, such as sending mail or resizing images, hand it to a
215
+ worker with [Queues](queues.md). When your app is ready to go live, [Deployment](deployment.md) covers building and
216
+ running it on a server.
217
+
218
+ To learn how an app runs, read [Request lifecycle](lifecycle.md), [Service container](container.md) and
219
+ [Facades](facades.md).
@@ -0,0 +1,234 @@
1
+ # Request lifecycle
2
+
3
+ Oriel builds your app once, when its process starts, and answers every request from it, until the process ends and
4
+ the app shuts down.
5
+
6
+ ## Introduction
7
+
8
+ You can build an app without knowing how Oriel works inside. Knowing the order in which it does things tells you
9
+ where your own code goes: what a service provider can rely on, when your routes exist, and what lasts longer than one
10
+ request.
11
+
12
+ ## Lifecycle overview
13
+
14
+ ### First steps
15
+
16
+ Your app has two entry points. `public/index.ts` starts the web server, as `node --import tsx public/index.ts` or
17
+ `bun public/index.ts`, and `node oriel serve` starts the same server while you work. The `oriel` file runs a command,
18
+ as `node oriel migrate`. Both get the application from `createApplication()` in `bootstrap/app.ts`:
19
+
20
+ ```ts title="public/index.ts"
21
+ // @filename: bootstrap/app.ts
22
+ export declare function createApplication(): Promise<import('@orieljs/framework').Application>
23
+ // ---cut---
24
+ import { serve } from '@orieljs/framework'
25
+ import { createApplication } from '../bootstrap/app'
26
+
27
+ // Production entry: `node --import tsx public/index.ts`, or `bun public/index.ts`.
28
+ await serve(await createApplication())
29
+ ```
30
+
31
+ `Application.configure()` creates the application, which is also the [service container](container.md). The methods
32
+ after it record what you give them, and `create()` puts the app together:
33
+
34
+ ```ts title="bootstrap/app.ts"
35
+ import { dirname } from 'node:path'
36
+ import { Application } from '@orieljs/framework'
37
+ import providers from './providers'
38
+
39
+ /** Builds the application. Each call returns a fresh app (tests need one per test). */
40
+ export function createApplication(): Promise<Application> {
41
+ return Application.configure({ basePath: dirname(import.meta.dirname) })
42
+ .withRouting({ web: 'routes/web.ts', console: 'routes/console.ts', health: '/up' })
43
+ .withProviders(providers)
44
+ .create()
45
+ }
46
+ ```
47
+
48
+ `create()` loads your `.env` file and every file in `config/` ([Configuration](configuration.md) covers both), then
49
+ registers the service providers. Each call makes a new application, so your tests can have a fresh one for every test.
50
+
51
+ ### HTTP / console kernels
52
+
53
+ A request goes to the HTTP kernel, and a command to the console kernel.
54
+
55
+ `serve()` answers a `GET` or `HEAD` request for a file in `public/` itself, and hands every other request to
56
+ `app.handle()`, which passes it to the HTTP kernel and returns the response. The kernel takes a web-standard `Request`
57
+ and returns a `Response`: think of it as a black box that stands for your whole app. It wraps the request in Oriel's
58
+ request object, applies the trusted proxies and the body limits of `config/http.ts` (see
59
+ [Requests](requests.md#body-size-limits)), reads the body, and sends the request through the global middleware to the
60
+ router.
61
+
62
+ `node oriel` gives its arguments to the console kernel. The command runs, the app terminates as it does after a
63
+ request, then it shuts down, and the process exits with the command's exit code.
64
+
65
+ ### Service providers
66
+
67
+ Service providers set up every part of the framework: the database, the queue, validation, routing and the rest.
68
+ `create()` calls each provider's `register()`, in order. The framework has 22, registered first, in this order:
69
+ Pipeline, Encryption, Cookie, Session, Hashing, Log, Event, Cache, Filesystem, Database, Queue, Bus, Mail,
70
+ Notifications, Validation, Auth, View, Inertia, Live, Routing, Http and Console (each one's class is its name followed
71
+ by `ServiceProvider`). Each may rely on the ones before it. Yours come next, in the order `bootstrap/providers.ts`
72
+ lists them.
73
+
74
+ `create()` registers the providers but doesn't boot them. The app boots once, the first time something needs it:
75
+ `serve()` before it starts listening, `node oriel` before it runs its command, your tests before each test, or the
76
+ first request the app handles. Booting runs, in this order:
77
+
78
+ 1. the `booting()` callbacks;
79
+ 2. each provider's `boot()`, in the order they were registered. A provider registered while the app boots is booted in
80
+ the same pass, and one registered after it has booted is booted at once;
81
+ 3. the `booted()` callbacks, in the order they were added: those you give the builder first, then the framework's,
82
+ which add the health route, load the route files of `withRouting()` (the `web` files in the `web` group, then the
83
+ `api` files in the `api` group), apply your `withMiddleware()` callback and, in the console only, find your
84
+ commands; last, those your providers add in `boot()`.
85
+
86
+ Your route files run once every provider has booted, so a route pattern, a binding or a gate that a provider sets up
87
+ in `boot()` is ready for them. The builder takes callbacks of its own:
88
+
89
+ ```ts
90
+ Application.configure({ basePath: '.' })
91
+ .booting((app) => {
92
+ // Before the first provider boots...
93
+ })
94
+ .booted((app) => {
95
+ // Once every provider has booted, before your routes load...
96
+ })
97
+ ```
98
+
99
+ > [!NOTE]
100
+ > Laravel bootstraps the application again for every request: a PHP process handles one request, then forgets it.
101
+ > An Oriel app runs in a long-lived Node or Bun process, so it registers and boots its providers once, and serves
102
+ > every request from the same application. A singleton lasts as long as the process; what belongs to one request lives
103
+ > in that request's [scope](container.md#request-scopes).
104
+
105
+ ### Routing
106
+
107
+ `app.handle()` opens a request scope, and the kernel binds the request in it: everything resolved for this request,
108
+ such as the request, its session and its cookie jar, lives in the scope and is dropped with it. The global middleware
109
+ run first: `TrimStrings`, `ConvertEmptyStringsToNull`, and those you add with
110
+ [`withMiddleware()`](middleware.md#global-middleware).
111
+
112
+ The router then finds the route. A path no route matches is a 404, and a path whose routes don't take the request's
113
+ method is a 405 with an `Allow` header. The router records the route as the current one, then runs the route's
114
+ middleware: those of its groups, outer group first, then its own. The routes in `routes/web.ts` are in the `web` group,
115
+ which gives them their cookies, their session and CSRF protection:
116
+
117
+ | The `web` group | What it does |
118
+ | --- | --- |
119
+ | `EncryptCookies` | Decrypts the request's cookies and encrypts the response's |
120
+ | `AddQueuedCookiesToResponse` | Adds the cookies queued on the cookie jar to the response |
121
+ | `StartSession` | Loads the session, and saves it once the response is ready |
122
+ | `VerifyCsrfToken` | Refuses a form without the CSRF token |
123
+
124
+ The `web` and `api` groups' middleware, `auth` and `guest` run before the route's other middleware, then route model
125
+ binding replaces the parameters that name a model, then the rest run ([Sorting
126
+ middleware](middleware.md#sorting-middleware) has the rule). Last, the action runs: a closure is called with the
127
+ route's parameters, in URI order, and a controller is built by the container for this request. Whatever the action
128
+ returns becomes the response, as [Responses](responses.md) describes.
129
+
130
+ ### Finishing up
131
+
132
+ The response goes back out through the route's middleware, then the global middleware, each of which can examine or
133
+ change it. An error thrown at any stage becomes a response at that stage, rendered by the exception handler (see
134
+ [Error handling](errors.md)), so a middleware further out always receives a response. The response to a `HEAD`
135
+ request loses its body here.
136
+
137
+ The server writes the response, and then the app terminates the request, still inside its scope:
138
+
139
+ 1. `terminate()` runs on each [terminable middleware](middleware.md#terminable-middleware), the global ones first,
140
+ then the route's;
141
+ 2. the `terminating()` callbacks run, in the order they were added. One of them is the framework's: it runs the jobs
142
+ you dispatched with [`dispatchAfterResponse()`](queues.md#running-a-job-now).
143
+
144
+ On Node, this starts once the response is written; on Bun, right after the response is handed to the server.
145
+
146
+ Register a terminating callback in a service provider. It receives no arguments:
147
+
148
+ ```ts title="app/Providers/AppServiceProvider.ts"
149
+ // @filename: app/Services/Metrics.ts
150
+ export declare class Metrics {
151
+ flush(): Promise<void>
152
+ }
153
+ // ---cut---
154
+ import { ServiceProvider } from '@orieljs/framework'
155
+ import { Metrics } from '../Services/Metrics'
156
+
157
+ export class AppServiceProvider extends ServiceProvider {
158
+ override register(): void {
159
+ this.app.singleton(Metrics)
160
+ }
161
+
162
+ override boot(): void {
163
+ this.app.terminating(async () => {
164
+ await this.app.make(Metrics).flush()
165
+ })
166
+ }
167
+ }
168
+ ```
169
+
170
+ > [!WARNING]
171
+ > The terminating callbacks also run after each `node oriel` command, and after the server sends a file from
172
+ > `public/`, which never reaches the app. They then run outside any request, where `request()` throws. A terminating
173
+ > callback that throws stops the ones after it, and its error is printed to the console rather than reported; after a
174
+ > command, `node oriel` then exits with code 1.
175
+
176
+ ## Shutting down
177
+
178
+ A Node or Bun process keeps its database pools, its Redis clients and its log files open from one request to the next.
179
+ When the process ends, `app.shutdown()` closes them: it runs the callbacks registered with `app.onShutdown()`, once.
180
+
181
+ - The server calls it when it receives `SIGINT` or `SIGTERM`. It stops taking new connections, waits for the requests
182
+ in flight (closing the connections that are still open after 10 seconds), shuts the app down, and exits with 0.
183
+ - `node oriel` calls it after its command, also when the command throws.
184
+ - A script that makes the app itself must call it before it exits, or the last log entries are lost (see [Flushing
185
+ the log](logging.md#flushing-the-log)).
186
+
187
+ The callbacks run newest first. Yours were registered after the framework's, so they run before the framework closes
188
+ what they might use. Then the framework closes the queue's Redis connections, the database pools, the cache's Redis
189
+ clients, the log files (writing out what is waiting) and the session's Redis connection, each only if your app opened
190
+ it:
191
+
192
+ ```ts title="app/Providers/AppServiceProvider.ts"
193
+ // @filename: app/Services/Metrics.ts
194
+ export declare class Metrics {
195
+ close(): Promise<void>
196
+ }
197
+ // ---cut---
198
+ import { ServiceProvider } from '@orieljs/framework'
199
+ import { Metrics } from '../Services/Metrics'
200
+
201
+ export class AppServiceProvider extends ServiceProvider {
202
+ override register(): void {
203
+ this.app.singleton(Metrics)
204
+
205
+ this.app.onShutdown(async () => {
206
+ if (this.app.resolved(Metrics)) await this.app.make(Metrics).close()
207
+ })
208
+ }
209
+ }
210
+ ```
211
+
212
+ A callback that throws is printed with `console.error`, and the others still run. If the callbacks haven't finished
213
+ after 10 seconds, the rest are given up on, with "The shutdown callbacks did not finish within 10 seconds; carrying on
214
+ with the exit.", so the process can still exit. `shutdown()` never rejects, and calling it again returns the same
215
+ promise.
216
+
217
+ | | `terminating()` | `onShutdown()` |
218
+ | --- | --- | --- |
219
+ | Runs | After each response, and after each `node oriel` command | Once, when the process ends |
220
+ | Inside | The request's scope, after a request | No request |
221
+ | Order | The order they were added | Newest first |
222
+ | For | Work that waits until the response is sent | Closing pools, clients and files |
223
+
224
+ > [!NOTE]
225
+ > Laravel has no `onShutdown()`: PHP frees what a request opened when the request ends. An Oriel process serves many
226
+ > requests, so the handles it opens stay open until the process ends, and `shutdown()` closes them before it exits.
227
+
228
+ ## Focus on service providers
229
+
230
+ Service providers are the key to how an Oriel app starts: the application is created, the providers are registered and
231
+ booted, and the requests are handed to the booted app. Your own providers live in `app/Providers`. A new app's
232
+ `AppServiceProvider` is empty: it is the place for your app's bindings and its setup at boot. A larger app can split
233
+ them over several providers, each listed in `bootstrap/providers.ts`. [Service providers](providers.md) explains how
234
+ to write them.
@@ -0,0 +1,226 @@
1
+ # Live tests
2
+
3
+ `Live.test()` mounts a Live component in a test, calls its actions and checks its state and HTML. It goes through the
4
+ same steps as the browser's updates, without a browser.
5
+
6
+ ## Creating your first test
7
+
8
+ `make:live --test` writes a test with the component:
9
+
10
+ ```sh
11
+ node oriel make:live Counter --test
12
+ ```
13
+
14
+ The test, in `tests/Feature/Live`, mounts the component and checks its HTML:
15
+
16
+ ```ts title="tests/Feature/Live/Counter.test.ts"
17
+ // @filename: app/Live/Counter.tsx
18
+ import { Component } from '@orieljs/framework'
19
+ export default class Counter extends Component {
20
+ count = 0
21
+ increment() {
22
+ this.count++
23
+ }
24
+ render() {
25
+ return <div>{this.count}</div>
26
+ }
27
+ }
28
+ // ---cut---
29
+ import { Live } from '@orieljs/framework'
30
+ import { expect, test } from 'vitest'
31
+ import Counter from '../../../app/Live/Counter'
32
+ import { TestCase } from '../../TestCase'
33
+
34
+ TestCase.boot()
35
+
36
+ test('counter renders', async () => {
37
+ const component = await Live.test(Counter)
38
+ expect(component.html()).toContain('<div')
39
+ })
40
+ ```
41
+
42
+ `Live.test()` needs the test's application, so boot the test case in the file.
43
+
44
+ ## Testing actions and properties
45
+
46
+ `set()` sets a property, as an `o:model` input does, and `call()` calls an action, as `o:click` does. Both send an
47
+ update, so `await` each one; the assertions that follow are synchronous and chain:
48
+
49
+ ```ts title="tests/Feature/Live/Counter.test.ts"
50
+ // @filename: app/Live/Counter.tsx
51
+ import { Component } from '@orieljs/framework'
52
+ export default class Counter extends Component {
53
+ count = 0
54
+ increment() {
55
+ this.count++
56
+ }
57
+ render() {
58
+ return <div>{this.count}</div>
59
+ }
60
+ }
61
+ // ---cut---
62
+ import { Live } from '@orieljs/framework'
63
+ import { test } from 'vitest'
64
+ import Counter from '../../../app/Live/Counter'
65
+ import { TestCase } from '../../TestCase'
66
+
67
+ TestCase.boot()
68
+
69
+ test('the counter goes up', async () => {
70
+ const component = await Live.test(Counter)
71
+
72
+ await component.set('count', 5)
73
+ await component.call('increment')
74
+
75
+ component.assertSet('count', 6).assertSee('6')
76
+ })
77
+ ```
78
+
79
+ `set()` also takes several properties at once, `set({ title: 'Hello', body: 'World' })`, and `call()` the action's
80
+ arguments after its name: `call('delete', 5)`. `toggle('open')` flips a boolean, `refresh()` renders the component
81
+ again, and `dispatch('post-created', { id: 1 })` sends it an event, which its `@On` methods receive.
82
+
83
+ The second argument of `Live.test()` is the mount parameters, as `<Live>` props would be:
84
+
85
+ ```ts
86
+ // @filename: app/Live/Counter.tsx
87
+ import { Component } from '@orieljs/framework'
88
+ export default class Counter extends Component {
89
+ count = 0
90
+ render() {
91
+ return <div>{this.count}</div>
92
+ }
93
+ }
94
+ // ---cut---
95
+ import Counter from './app/Live/Counter'
96
+
97
+ const component = await Live.test(Counter, { count: 4 })
98
+ ```
99
+
100
+ `get('count')` reads a property, `html()` the last HTML, `instance()` the component itself, and `effects()` what the
101
+ last update asks of the browser, such as a redirect.
102
+
103
+ ### Authenticating
104
+
105
+ `Live.test()` runs in the test's requests, so a user you gave `actingAs()` is signed in for the component:
106
+
107
+ ```ts title="tests/Feature/Live/Password.test.ts"
108
+ // @filename: app/Live/Settings/Password.tsx
109
+ import { Component } from '@orieljs/framework'
110
+ export default class Password extends Component {
111
+ current_password = ''
112
+ password = ''
113
+ password_confirmation = ''
114
+ updatePassword() {}
115
+ render() {
116
+ return <form />
117
+ }
118
+ }
119
+ // ---cut---
120
+ import { Hash, Live } from '@orieljs/framework'
121
+ import { RefreshDatabase } from '@orieljs/framework/testing'
122
+ import { expect, test } from 'vitest'
123
+ import Password from '../../../app/Live/Settings/Password'
124
+ import { User } from '../../../app/Models/User'
125
+ import { TestCase } from '../../TestCase'
126
+
127
+ const t = TestCase.boot({ uses: [RefreshDatabase] })
128
+
129
+ test('password can be updated', async () => {
130
+ const user = await User.factory().create()
131
+
132
+ t.actingAs(user)
133
+ const component = await Live.test(Password)
134
+ await component.set({ current_password: 'password', password: 'new-password', password_confirmation: 'new-password' })
135
+ await component.call('updatePassword')
136
+
137
+ component.assertHasNoErrors().assertSet('password', '').assertDispatched('password-updated')
138
+
139
+ expect(await Hash.check('new-password', (await user.fresh())!.password)).toBe(true)
140
+ })
141
+ ```
142
+
143
+ What the component changes in the session, such as a sign-in, carries on to the test's next request, and the
144
+ other way round.
145
+
146
+ ## Testing validation
147
+
148
+ A failed validation leaves its errors on the component, which `assertHasErrors()` checks:
149
+
150
+ ```ts
151
+ import type { Testable } from '@orieljs/framework/testing'
152
+ declare const component: Testable
153
+ // ---cut---
154
+ await component.set('title', '')
155
+ await component.call('save')
156
+
157
+ component.assertHasErrors(['title'])
158
+ ```
159
+
160
+ With no keys, `assertHasErrors()` passes when there is any error, and `assertHasNoErrors()` when there is none.
161
+ `instance().errors.first('title')` reads a message.
162
+
163
+ ## Testing authorization
164
+
165
+ An action that calls `abort(403)`, or whose policy check fails, throws in the test as it would end the request:
166
+
167
+ ```ts
168
+ import type { Testable } from '@orieljs/framework/testing'
169
+ import { expect } from 'vitest'
170
+ declare const component: Testable
171
+ // ---cut---
172
+ await expect(component.call('delete', 5)).rejects.toMatchObject({ status: 403 })
173
+ ```
174
+
175
+ A page route's `can` middleware runs on the page's own request, so check it with an [HTTP test](http-tests.md) of the
176
+ page.
177
+
178
+ ## Testing redirects and events
179
+
180
+ ```ts
181
+ import type { Testable } from '@orieljs/framework/testing'
182
+ declare const component: Testable
183
+ // ---cut---
184
+ await component.call('save')
185
+
186
+ component.assertRedirect('/posts').assertDispatched('post-created', { title: 'Hello' })
187
+ ```
188
+
189
+ `assertRedirect()` with no URL passes on any redirect. `effects().redirectUsingNavigate` is `true` when the action
190
+ asked for `{ navigate: true }`.
191
+
192
+ ## Testing file uploads
193
+
194
+ In a `WithFileUploads` component, `set()` with a `FakeFile` uploads it as the browser would: through the upload rules,
195
+ to the temporary disk, and into the property as a `TemporaryUploadedFile`:
196
+
197
+ ```ts
198
+ import { FakeFile, type Testable } from '@orieljs/framework/testing'
199
+ declare const component: Testable
200
+ // ---cut---
201
+ await component.set('photo', FakeFile.image('photo.jpg'))
202
+ await component.call('save')
203
+
204
+ component.assertHasNoErrors()
205
+ ```
206
+
207
+ An array of files sets a `multiple` input's property. A file that fails `live.temporary_file_upload.rules` leaves the
208
+ upload's error on the property. Use `Storage.fake()` to check where `store()` puts the file.
209
+
210
+ ## Available assertions
211
+
212
+ | Assertion | Passes when |
213
+ | --- | --- |
214
+ | `assertSet(name, value)`, `assertNotSet(name, value)` | The property has the value, or hasn't |
215
+ | `assertSee(...texts)`, `assertDontSee(...texts)` | The HTML has each text, escaped as the HTML writes it |
216
+ | `assertSeeHtml(...html)` | The HTML has each piece of HTML, as it is |
217
+ | `assertSeeText(...texts)`, `assertDontSeeText(...texts)` | The text, with the tags taken out, has each text |
218
+ | `assertHasErrors(keys?)`, `assertHasNoErrors(keys?)` | The component has errors for the keys, or has none |
219
+ | `assertDispatched(name, params?)`, `assertNotDispatched(name)` | The last update dispatched the event, with those parameters |
220
+ | `assertRedirect(url?)`, `assertNoRedirect()` | The last update redirects, to that URL |
221
+ | `assertReturned(value)` | The last action returned the value |
222
+
223
+ > [!NOTE]
224
+ > Livewire's `assertSeeLivewire()`, `assertViewHas()`, `assertCount()` and `withQueryParams()` aren't in Oriel. To
225
+ > check that a page has a component, check the page's HTML with an HTTP test; `get()` and Vitest's `expect()` check
226
+ > the rest.