@orieljs/http 0.1.0-alpha.6 → 0.1.0-alpha.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.
- package/NOTICE +4 -0
- package/README.md +33 -0
- package/TRADEMARKS.md +33 -0
- package/dist/index.d.ts +48 -12
- package/dist/index.js +324 -16
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/NOTICE
CHANGED
|
@@ -10,3 +10,7 @@ The Oriel name and logo are not covered by that license; see TRADEMARKS.md
|
|
|
10
10
|
|
|
11
11
|
@orieljs/support (and @orieljs/framework, which bundles it) includes transliteration data from
|
|
12
12
|
voku/portable-ascii (https://github.com/voku/portable-ascii), Copyright (C) 2019 Lars Moelleken, MIT License.
|
|
13
|
+
|
|
14
|
+
@orieljs/notifications (and @orieljs/framework, which bundles it) includes the HTML and CSS of
|
|
15
|
+
Laravel's default notification mail (laravel/framework, https://github.com/laravel/framework),
|
|
16
|
+
Copyright (c) Taylor Otwell, MIT License.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @orieljs/http
|
|
2
|
+
|
|
3
|
+
The HTTP layer of Oriel: the request, responses and redirects, the middleware kernel, the error handler and its error pages, and the server for Node and Bun. It is Oriel's counterpart of Laravel's HTTP layer. Http is part of `@orieljs/framework`, which every Oriel app already installs, and is re-exported from it. In an app, import it from `@orieljs/framework`; `@orieljs/http` is also published on its own.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @orieljs/framework
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts title="app/Http/Controllers/SearchController.ts"
|
|
10
|
+
import { request, response } from '@orieljs/framework'
|
|
11
|
+
import { Post } from '../../Models/Post'
|
|
12
|
+
import { Controller } from './Controller'
|
|
13
|
+
|
|
14
|
+
export class SearchController extends Controller {
|
|
15
|
+
async index() {
|
|
16
|
+
const term = request().input('q', '')
|
|
17
|
+
const posts = await Post.whereLike('title', `%${term}%`).latest().get()
|
|
18
|
+
return response().json({ term, posts }, 200, { 'Cache-Control': 'no-store' })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`request()` is the current `IncomingRequest`, with `input()`, `string()`, `boolean()`, `integer()`, `only()`, `file()` and `header()`; `await request().user()` is the signed-in user, or `undefined` for a guest. `response()`, `redirect()` and `back()` build responses, and `abort()`, `abortIf()` and `abortUnless()` stop a request with an HTTP error. `await serve(app)` runs the app on Node or Bun.
|
|
24
|
+
|
|
25
|
+
Documentation: https://orieljs.vercel.app/docs/requests
|
|
26
|
+
|
|
27
|
+
Status: early alpha. APIs will change.
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
Oriel is open-source software licensed under the Apache License 2.0 (Apache-2.0; see `LICENSE` and `NOTICE`). The Oriel name and logo are trademarks; see `TRADEMARKS.md`.
|
|
32
|
+
|
|
33
|
+
Website: https://orieljs.vercel.app
|
package/TRADEMARKS.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Oriel trademark policy
|
|
2
|
+
|
|
3
|
+
The Oriel code is open source under the Apache License 2.0. The **Oriel name and logo** are not: the license covers
|
|
4
|
+
the code only (section 6 of the license says so), and the name and logo stay with the project. This page explains
|
|
5
|
+
what you can and can't do with them. The goal is simple: people should always be able to tell what is the real Oriel
|
|
6
|
+
and what isn't.
|
|
7
|
+
|
|
8
|
+
## You can, without asking
|
|
9
|
+
|
|
10
|
+
- Use, change, ship and sell the code, as the license allows.
|
|
11
|
+
- Say that your app or package is "built with Oriel", "for Oriel" or "compatible with Oriel".
|
|
12
|
+
- Name a package `oriel-<something>` or `<something>-for-oriel` when it works with Oriel, as long as it is clear that
|
|
13
|
+
it is not official (for example, don't publish it under the `@orieljs` npm scope).
|
|
14
|
+
- Use the name and logo to write about Oriel, teach it, or link to it, in articles, talks, videos and courses.
|
|
15
|
+
- Use the unmodified logo to point to the project, as it is (no recolouring, cropping or
|
|
16
|
+
distortion).
|
|
17
|
+
|
|
18
|
+
## You can't, without written permission
|
|
19
|
+
|
|
20
|
+
- Call a fork, a modified copy or a competing product "Oriel", or anything likely to be confused with it.
|
|
21
|
+
- Use the name or logo as the name, logo or domain of your own product, company or service.
|
|
22
|
+
- Suggest that the Oriel project made, endorses or supports your work when it doesn't.
|
|
23
|
+
- Sell merchandise with the name or logo.
|
|
24
|
+
|
|
25
|
+
## Forks
|
|
26
|
+
|
|
27
|
+
Forks are welcome. Please give yours a different name and logo, keep the `LICENSE` and `NOTICE` files, and say that
|
|
28
|
+
it is based on Oriel.
|
|
29
|
+
|
|
30
|
+
## Questions
|
|
31
|
+
|
|
32
|
+
Open an issue at https://github.com/orieljs/framework or email meeeet.dev@gmail.com. If you're unsure whether a use
|
|
33
|
+
is fine, ask; the answer is usually yes.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { HttpRequest, CookieJar, Session, Container, RedirectResponse, HeadersInit, Redirector, UploadedFile, Rules, OrielError, RouteName, ResponseFactory, BodyInit, UrlGenerator, HttpKernel, Application, MiddlewareClass, Middleware, Next, MiddlewareConfigurator, Router, ConfigRepository, ExceptionHandler, ExceptionsConfigurator, ConsoleKernel, Route } from '@orieljs/contracts';
|
|
1
|
+
import { HttpRequest, CookieJar, Session, Container, RedirectResponse, HeadersInit, MessageBag, Redirector, UploadedFile, Rules, InferValidatedType, OrielError, RouteName, RouteArguments, ResponseFactory, BodyInit, UrlGenerator, HttpKernel, Application, MiddlewareClass, Middleware, Next, MiddlewareConfigurator, Router, ConfigRepository, ExceptionHandler, ExceptionsConfigurator, ConsoleKernel, Route } from '@orieljs/contracts';
|
|
2
2
|
import { FileType, Stringable, ServiceProvider } from '@orieljs/support';
|
|
3
3
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
|
+
import { SpawnOptions, ChildProcess } from 'node:child_process';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Whether a JSON text nests objects and arrays deeper than `maxDepth`, found by scanning (no parsing, no recursion):
|
|
@@ -18,9 +19,7 @@ declare function currentRequest(): HttpRequest | undefined;
|
|
|
18
19
|
declare function optionalSession(): Session | undefined;
|
|
19
20
|
declare function optionalCookieJar(): CookieJar | undefined;
|
|
20
21
|
|
|
21
|
-
type ErrorsInput = Record<string, string | string[]> |
|
|
22
|
-
toObject(): Record<string, string[]>;
|
|
23
|
-
};
|
|
22
|
+
type ErrorsInput = Record<string, string | string[]> | MessageBag;
|
|
24
23
|
/**
|
|
25
24
|
* Laravel's RedirectResponse. with()/withErrors()/withInput() flash to the request's Session immediately, like
|
|
26
25
|
* Laravel; without a bound Session (an API-only app) they are skipped silently.
|
|
@@ -182,10 +181,17 @@ declare class IncomingRequest implements HttpRequest {
|
|
|
182
181
|
/** One route parameter (after model binding), or all of them. */
|
|
183
182
|
route<T = unknown>(key?: string): T;
|
|
184
183
|
session(): Session;
|
|
185
|
-
|
|
184
|
+
/**
|
|
185
|
+
* The signed-in user, through the resolver Auth sets, else the current scope's guard (the default, or `guard`).
|
|
186
|
+
* The guard keeps the user it found, so it is looked up once per request. Undefined for a guest or without auth.
|
|
187
|
+
*/
|
|
188
|
+
user<T = unknown>(guard?: string): Promise<T | undefined>;
|
|
186
189
|
setUserResolver(resolver: (guard?: string) => unknown): void;
|
|
187
|
-
/**
|
|
188
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Validates all() through the ValidationFactory; throws its Validation error on failure. The data is typed from the
|
|
192
|
+
* rules: `validate({ title: 'required|string' })` is `{ title: string }`.
|
|
193
|
+
*/
|
|
194
|
+
validate<const R extends Rules>(rules: R, messages?: Record<string, string>, attributes?: Record<string, string>): Promise<InferValidatedType<R>>;
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
/** Helpers for request input: PHP-style bracket keys, safe cloning (no prototype pollution) and blank checks. */
|
|
@@ -245,9 +251,9 @@ declare function request<T = unknown>(key: string, defaultValue?: T): T;
|
|
|
245
251
|
/** response() → the ResponseFactory; response(content, status, headers) → a Response. */
|
|
246
252
|
declare function response(): ResponseFactory;
|
|
247
253
|
declare function response(content: BodyInit | null, status?: number, headers?: HeadersInit): Response;
|
|
248
|
-
/** The Redirector with its route() typed by the generated route names. */
|
|
254
|
+
/** The Redirector with its route() typed by the generated route names and their parameters. */
|
|
249
255
|
type TypedRedirector = Omit<Redirector, 'route'> & {
|
|
250
|
-
route(name:
|
|
256
|
+
route<N extends RouteName>(name: N, ...args: RouteArguments<N, [status?: number]>): RedirectResponse;
|
|
251
257
|
};
|
|
252
258
|
/** redirect() → the Redirector; redirect('/path', status) → a RedirectResponse. */
|
|
253
259
|
declare function redirect(): TypedRedirector;
|
|
@@ -257,8 +263,11 @@ declare function back(fallback?: string, status?: number): RedirectResponse;
|
|
|
257
263
|
/** url() → the UrlGenerator; url('/path', query, absolute) → a URL. */
|
|
258
264
|
declare function url(): UrlGenerator;
|
|
259
265
|
declare function url(path: string, query?: Record<string, unknown>, absolute?: boolean): string;
|
|
260
|
-
/**
|
|
261
|
-
|
|
266
|
+
/**
|
|
267
|
+
* The URL of a named route: route('posts.show', post), route('posts.show', { post, page: 2 }). Once `oriel
|
|
268
|
+
* route:types` has run, the name and the route's parameters are checked.
|
|
269
|
+
*/
|
|
270
|
+
declare function route<N extends RouteName>(name: N, ...args: RouteArguments<N>): string;
|
|
262
271
|
/** abort(404), abort(403, 'Nope', headers), or abort(response) to send a ready response. */
|
|
263
272
|
declare function abort(status: number | Response, message?: string, headers?: Record<string, string>): never;
|
|
264
273
|
declare function abortIf(condition: unknown, status: number | Response, message?: string, headers?: Record<string, string>): void;
|
|
@@ -384,7 +393,10 @@ declare class ExceptionsConfiguration implements ExceptionsConfigurator {
|
|
|
384
393
|
/** Runs the withExceptions() callbacks against a handler (the provider does this when the handler is built). */
|
|
385
394
|
declare function applyExceptionCallbacks(app: Application, handler: ErrorHandler): void;
|
|
386
395
|
|
|
387
|
-
/**
|
|
396
|
+
/**
|
|
397
|
+
* The text an error page shows for a status: Laravel's own wording where it has a view ("Server Error", "Page
|
|
398
|
+
* Expired"), else Symfony's `Response::$statusTexts`, which Laravel shows for the others.
|
|
399
|
+
*/
|
|
388
400
|
declare function statusText(status: number): string;
|
|
389
401
|
declare function escapeHtml(value: string): string;
|
|
390
402
|
/** Laravel's minimal error page: "404 | Not Found". */
|
|
@@ -473,6 +485,28 @@ declare function serve(app: Application, options?: ServeOptions): Promise<Runnin
|
|
|
473
485
|
*/
|
|
474
486
|
declare function servePublicFile(publicPath: string, request: Request): Promise<Response | undefined>;
|
|
475
487
|
|
|
488
|
+
interface FileWatcher {
|
|
489
|
+
close(): void;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** node:child_process's spawn(), replaceable in tests. */
|
|
493
|
+
type SpawnServer = (command: string, args: string[], options: SpawnOptions) => ChildProcess;
|
|
494
|
+
interface WatchedServerOptions {
|
|
495
|
+
/** The server's command line: the runtime, its flags, the CLI script, then `serve --host=… --port=…`. */
|
|
496
|
+
command: readonly string[];
|
|
497
|
+
cwd: string;
|
|
498
|
+
env: NodeJS.ProcessEnv;
|
|
499
|
+
/** Starts watching: calls back with the changed files (relative to the base path) once they settle. */
|
|
500
|
+
watch(onChange: (files: string[]) => void): FileWatcher;
|
|
501
|
+
info(message: string): void;
|
|
502
|
+
error(message: string): void;
|
|
503
|
+
/** How long a server may take to stop before it is killed, in ms. Default 15000 (serve gives requests 10 s). */
|
|
504
|
+
stopTimeout?: number;
|
|
505
|
+
/** The signals that stop it. Default SIGINT, SIGTERM and SIGHUP. */
|
|
506
|
+
signals?: readonly NodeJS.Signals[];
|
|
507
|
+
spawn?: SpawnServer;
|
|
508
|
+
}
|
|
509
|
+
|
|
476
510
|
interface RouteListRow {
|
|
477
511
|
/** 'GET|HEAD' */
|
|
478
512
|
readonly method: string;
|
|
@@ -512,6 +546,8 @@ interface HttpCommandOptions {
|
|
|
512
546
|
serve?: (app: Application, options: ServeOptions) => Promise<RunningServer>;
|
|
513
547
|
/** The terminal width for route:list. Default: process.stdout.columns, else 120. */
|
|
514
548
|
terminalWidth?: () => number;
|
|
549
|
+
/** Replaces `serve --watch`'s restarting server (tests). Resolves the command's exit code. */
|
|
550
|
+
watch?: (options: WatchedServerOptions) => Promise<number>;
|
|
515
551
|
}
|
|
516
552
|
/** Registers `serve`, `route:list` and `install:api` as closure commands (PC1). */
|
|
517
553
|
declare function registerHttpCommands(kernel: ConsoleKernel, app: Application, options?: HttpCommandOptions): void;
|
package/dist/index.js
CHANGED
|
@@ -352,6 +352,7 @@ var FormUploadedFile = class _FormUploadedFile {
|
|
|
352
352
|
|
|
353
353
|
// IncomingRequest.ts
|
|
354
354
|
import {
|
|
355
|
+
AuthFactory,
|
|
355
356
|
CurrentRoute,
|
|
356
357
|
Session as Session2,
|
|
357
358
|
ValidationFactory
|
|
@@ -768,13 +769,23 @@ var IncomingRequest = class {
|
|
|
768
769
|
session() {
|
|
769
770
|
return inject3(Session2);
|
|
770
771
|
}
|
|
771
|
-
|
|
772
|
-
|
|
772
|
+
/**
|
|
773
|
+
* The signed-in user, through the resolver Auth sets, else the current scope's guard (the default, or `guard`).
|
|
774
|
+
* The guard keeps the user it found, so it is looked up once per request. Undefined for a guest or without auth.
|
|
775
|
+
*/
|
|
776
|
+
async user(guard) {
|
|
777
|
+
if (this.#userResolver !== void 0) return await this.#userResolver(guard);
|
|
778
|
+
const container = currentContainer2();
|
|
779
|
+
if (container === void 0 || !container.bound(AuthFactory)) return void 0;
|
|
780
|
+
return await container.make(AuthFactory).guard(guard).user();
|
|
773
781
|
}
|
|
774
782
|
setUserResolver(resolver) {
|
|
775
783
|
this.#userResolver = resolver;
|
|
776
784
|
}
|
|
777
|
-
/**
|
|
785
|
+
/**
|
|
786
|
+
* Validates all() through the ValidationFactory; throws its Validation error on failure. The data is typed from the
|
|
787
|
+
* rules: `validate({ title: 'required|string' })` is `{ title: string }`.
|
|
788
|
+
*/
|
|
778
789
|
validate(rules, messages, attributes) {
|
|
779
790
|
return inject3(ValidationFactory).make(this.all(), rules, messages, attributes).validated();
|
|
780
791
|
}
|
|
@@ -818,7 +829,8 @@ function url(path, query, absolute) {
|
|
|
818
829
|
const generator = inject4(UrlGenerator2);
|
|
819
830
|
return path === void 0 ? generator : generator.to(path, query, absolute);
|
|
820
831
|
}
|
|
821
|
-
function route(name,
|
|
832
|
+
function route(name, ...args) {
|
|
833
|
+
const [parameters, absolute] = args;
|
|
822
834
|
return inject4(UrlGenerator2).route(name, parameters, absolute);
|
|
823
835
|
}
|
|
824
836
|
function abort(status, message = "", headers = {}) {
|
|
@@ -1183,18 +1195,41 @@ var STATUS_TEXTS = {
|
|
|
1183
1195
|
403: "Forbidden",
|
|
1184
1196
|
404: "Not Found",
|
|
1185
1197
|
405: "Method Not Allowed",
|
|
1198
|
+
406: "Not Acceptable",
|
|
1199
|
+
407: "Proxy Authentication Required",
|
|
1186
1200
|
408: "Request Timeout",
|
|
1187
1201
|
409: "Conflict",
|
|
1188
1202
|
410: "Gone",
|
|
1203
|
+
411: "Length Required",
|
|
1204
|
+
412: "Precondition Failed",
|
|
1189
1205
|
413: "Content Too Large",
|
|
1206
|
+
414: "URI Too Long",
|
|
1190
1207
|
415: "Unsupported Media Type",
|
|
1208
|
+
416: "Range Not Satisfiable",
|
|
1209
|
+
417: "Expectation Failed",
|
|
1210
|
+
418: "I'm a teapot",
|
|
1191
1211
|
419: "Page Expired",
|
|
1212
|
+
421: "Misdirected Request",
|
|
1192
1213
|
422: "Unprocessable Content",
|
|
1214
|
+
423: "Locked",
|
|
1215
|
+
424: "Failed Dependency",
|
|
1216
|
+
425: "Too Early",
|
|
1217
|
+
426: "Upgrade Required",
|
|
1218
|
+
428: "Precondition Required",
|
|
1193
1219
|
429: "Too Many Requests",
|
|
1220
|
+
431: "Request Header Fields Too Large",
|
|
1221
|
+
451: "Unavailable For Legal Reasons",
|
|
1194
1222
|
500: "Server Error",
|
|
1223
|
+
501: "Not Implemented",
|
|
1195
1224
|
502: "Bad Gateway",
|
|
1196
1225
|
503: "Service Unavailable",
|
|
1197
|
-
504: "Gateway Timeout"
|
|
1226
|
+
504: "Gateway Timeout",
|
|
1227
|
+
505: "HTTP Version Not Supported",
|
|
1228
|
+
506: "Variant Also Negotiates",
|
|
1229
|
+
507: "Insufficient Storage",
|
|
1230
|
+
508: "Loop Detected",
|
|
1231
|
+
510: "Not Extended",
|
|
1232
|
+
511: "Network Authentication Required"
|
|
1198
1233
|
};
|
|
1199
1234
|
function statusText(status) {
|
|
1200
1235
|
return STATUS_TEXTS[status] ?? (status >= 500 ? "Server Error" : "Error");
|
|
@@ -1370,8 +1405,7 @@ var ErrorHandler = class {
|
|
|
1370
1405
|
else if (this.shouldReturnJson(http, error)) response2 = this.#renderJson(error);
|
|
1371
1406
|
else response2 = await this.#renderHtml(http, error);
|
|
1372
1407
|
const headers = error?.headers;
|
|
1373
|
-
|
|
1374
|
-
return response2;
|
|
1408
|
+
return headers === void 0 ? response2 : withHeaders(response2, headers);
|
|
1375
1409
|
}
|
|
1376
1410
|
/** An error with its own render(request) or toResponse(request) (Laravel's renderable exceptions / Responsable). */
|
|
1377
1411
|
async #renderCustom(request2, error) {
|
|
@@ -1491,6 +1525,11 @@ var ErrorHandler = class {
|
|
|
1491
1525
|
return currentContainer4() ?? this.app;
|
|
1492
1526
|
}
|
|
1493
1527
|
};
|
|
1528
|
+
function withHeaders(response2, extra) {
|
|
1529
|
+
const headers = new Headers(response2.headers);
|
|
1530
|
+
for (const [name, value] of Object.entries(extra)) headers.set(name, value);
|
|
1531
|
+
return new Response(response2.body, { status: response2.status, statusText: response2.statusText, headers });
|
|
1532
|
+
}
|
|
1494
1533
|
|
|
1495
1534
|
// ExceptionsConfiguration.ts
|
|
1496
1535
|
import { ExceptionCallbacks } from "@orieljs/contracts";
|
|
@@ -1579,9 +1618,9 @@ async function handleSafely(app, request2, publicPath) {
|
|
|
1579
1618
|
return plainServerError();
|
|
1580
1619
|
}
|
|
1581
1620
|
}
|
|
1582
|
-
function exitOnSignals(close, closed) {
|
|
1621
|
+
function exitOnSignals(close, closed, app) {
|
|
1583
1622
|
const onSignal = () => {
|
|
1584
|
-
void close().then(() => process.exit(0));
|
|
1623
|
+
void close().then(() => app.shutdown()).then(() => process.exit(0));
|
|
1585
1624
|
};
|
|
1586
1625
|
process.once("SIGINT", onSignal);
|
|
1587
1626
|
process.once("SIGTERM", onSignal);
|
|
@@ -1625,7 +1664,7 @@ async function serveBun(app, options) {
|
|
|
1625
1664
|
clearTimeout(timer);
|
|
1626
1665
|
markClosed();
|
|
1627
1666
|
})();
|
|
1628
|
-
if (options.signals) exitOnSignals(close, closed);
|
|
1667
|
+
if (options.signals) exitOnSignals(close, closed, app);
|
|
1629
1668
|
return {
|
|
1630
1669
|
runtime: "bun",
|
|
1631
1670
|
hostname: options.hostname,
|
|
@@ -1763,7 +1802,7 @@ async function serveNode(app, options) {
|
|
|
1763
1802
|
clearInterval(idle);
|
|
1764
1803
|
clearTimeout(force);
|
|
1765
1804
|
})();
|
|
1766
|
-
if (options.signals) exitOnSignals(close, closed);
|
|
1805
|
+
if (options.signals) exitOnSignals(close, closed, app);
|
|
1767
1806
|
return {
|
|
1768
1807
|
runtime: "node",
|
|
1769
1808
|
hostname: options.hostname,
|
|
@@ -1795,12 +1834,255 @@ async function serve(app, options = {}) {
|
|
|
1795
1834
|
}
|
|
1796
1835
|
|
|
1797
1836
|
// commands.ts
|
|
1798
|
-
import { existsSync } from "fs";
|
|
1837
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1799
1838
|
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
1800
1839
|
import { dirname } from "path";
|
|
1801
1840
|
import {
|
|
1802
1841
|
Router as Router3
|
|
1803
1842
|
} from "@orieljs/contracts";
|
|
1843
|
+
|
|
1844
|
+
// watch/environment.ts
|
|
1845
|
+
import { existsSync, readFileSync } from "fs";
|
|
1846
|
+
import { join } from "path";
|
|
1847
|
+
import * as util from "util";
|
|
1848
|
+
var launchEnvironment = Object.freeze({ ...process.env });
|
|
1849
|
+
var BUN_ENV_FILES = [
|
|
1850
|
+
".env",
|
|
1851
|
+
".env.development",
|
|
1852
|
+
".env.production",
|
|
1853
|
+
".env.test",
|
|
1854
|
+
".env.local",
|
|
1855
|
+
".env.development.local",
|
|
1856
|
+
".env.production.local",
|
|
1857
|
+
".env.test.local"
|
|
1858
|
+
];
|
|
1859
|
+
var expand = (value, env) => value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => env[braced ?? bare] ?? "");
|
|
1860
|
+
function serverEnvironment(options = {}) {
|
|
1861
|
+
const launched = options.launched ?? launchEnvironment;
|
|
1862
|
+
const env = { ...launched };
|
|
1863
|
+
if (!(options.bun ?? process.versions.bun !== void 0)) return env;
|
|
1864
|
+
const parseEnv2 = "parseEnv" in options ? options.parseEnv : util.parseEnv;
|
|
1865
|
+
if (typeof parseEnv2 !== "function") return env;
|
|
1866
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1867
|
+
for (const name of BUN_ENV_FILES) {
|
|
1868
|
+
const file = join(cwd, name);
|
|
1869
|
+
if (!existsSync(file)) continue;
|
|
1870
|
+
let values;
|
|
1871
|
+
try {
|
|
1872
|
+
values = parseEnv2(readFileSync(file, "utf8"));
|
|
1873
|
+
} catch {
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
for (const [key, raw] of Object.entries(values)) {
|
|
1877
|
+
const value = launched[key];
|
|
1878
|
+
if (raw !== void 0 && value !== void 0 && (value === raw || value === expand(raw, launched))) delete env[key];
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return env;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// watch/files.ts
|
|
1885
|
+
import { existsSync as existsSync2, statSync, watch as watchPath } from "fs";
|
|
1886
|
+
import { join as join2 } from "path";
|
|
1887
|
+
var WATCHED_FOLDERS = ["app", "bootstrap", "config", "lang", "resources/lang", "resources/views", "routes"];
|
|
1888
|
+
var WATCHED_ROOT_FILES = ["package.json", "tsconfig.json"];
|
|
1889
|
+
var IGNORED_FOLDERS = ["bootstrap/ssr", "bootstrap/cache"];
|
|
1890
|
+
var CODE_FILE = /\.(?:[cm]?[jt]sx?|json)$/;
|
|
1891
|
+
var DECLARATION_FILE = /\.d\.[cm]?ts$/;
|
|
1892
|
+
var EDITOR_FILE = /(?:\.sw[a-px]|~|___jb_(?:tmp|old)___)$/;
|
|
1893
|
+
var isEnvironmentFile = (name) => (name === ".env" || name.startsWith(".env.")) && name !== ".env.example" && !EDITOR_FILE.test(name);
|
|
1894
|
+
function isWatchedPath(path) {
|
|
1895
|
+
const segments = path.split("/");
|
|
1896
|
+
if (segments.length === 1) return isEnvironmentFile(path) || WATCHED_ROOT_FILES.includes(path);
|
|
1897
|
+
if (!WATCHED_FOLDERS.some((folder) => path.startsWith(`${folder}/`))) return false;
|
|
1898
|
+
if (IGNORED_FOLDERS.some((folder) => path === folder || path.startsWith(`${folder}/`))) return false;
|
|
1899
|
+
if (segments.some((segment) => segment.startsWith(".") || segment === "node_modules")) return false;
|
|
1900
|
+
const name = segments.at(-1);
|
|
1901
|
+
if (DECLARATION_FILE.test(name)) return false;
|
|
1902
|
+
return CODE_FILE.test(name) || !name.includes(".");
|
|
1903
|
+
}
|
|
1904
|
+
var toSlashes = (file) => file.split("\\").join("/");
|
|
1905
|
+
function watchFiles(basePath, onChange, options = {}) {
|
|
1906
|
+
const watch = options.watch ?? ((path, watchOptions, listener) => watchPath(path, watchOptions, listener));
|
|
1907
|
+
const debounce = options.debounce ?? 200;
|
|
1908
|
+
const onError = options.onError ?? (() => {
|
|
1909
|
+
});
|
|
1910
|
+
const changed = /* @__PURE__ */ new Set();
|
|
1911
|
+
let timer;
|
|
1912
|
+
const settle = options.settle ?? 1e3;
|
|
1913
|
+
const startedAt = Date.now();
|
|
1914
|
+
const reportedLate = (path) => {
|
|
1915
|
+
if (Date.now() - startedAt >= settle) return false;
|
|
1916
|
+
try {
|
|
1917
|
+
const modified = statSync(join2(basePath, path), { throwIfNoEntry: false })?.mtimeMs;
|
|
1918
|
+
return modified === void 0 || modified < startedAt + 1;
|
|
1919
|
+
} catch {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
const record = (path) => {
|
|
1924
|
+
if (!isWatchedPath(path) || reportedLate(path)) return;
|
|
1925
|
+
changed.add(path);
|
|
1926
|
+
clearTimeout(timer);
|
|
1927
|
+
timer = setTimeout(() => {
|
|
1928
|
+
const files = [...changed];
|
|
1929
|
+
changed.clear();
|
|
1930
|
+
onChange(files);
|
|
1931
|
+
}, debounce);
|
|
1932
|
+
};
|
|
1933
|
+
const handles = [];
|
|
1934
|
+
const start = (folder, recursive, toPath) => {
|
|
1935
|
+
try {
|
|
1936
|
+
const handle = watch(folder === "." ? basePath : join2(basePath, folder), { recursive }, (_event, file) => {
|
|
1937
|
+
const path = toPath(file);
|
|
1938
|
+
if (path !== void 0) record(path);
|
|
1939
|
+
});
|
|
1940
|
+
handle.on("error", (error) => onError(folder, error));
|
|
1941
|
+
handles.push(handle);
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
onError(folder, error);
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
start(".", false, (file) => file === null ? void 0 : toSlashes(file));
|
|
1947
|
+
for (const folder of WATCHED_FOLDERS) {
|
|
1948
|
+
if (existsSync2(join2(basePath, folder))) start(folder, true, (file) => `${folder}/${file === null ? "" : toSlashes(file)}`);
|
|
1949
|
+
}
|
|
1950
|
+
return {
|
|
1951
|
+
close() {
|
|
1952
|
+
clearTimeout(timer);
|
|
1953
|
+
changed.clear();
|
|
1954
|
+
for (const handle of handles) handle.close();
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// watch/port.ts
|
|
1960
|
+
import { createServer as createServer2 } from "net";
|
|
1961
|
+
function claimPort(hostname, port) {
|
|
1962
|
+
return new Promise((resolve2, reject) => {
|
|
1963
|
+
const server = createServer2();
|
|
1964
|
+
server.once("error", reject);
|
|
1965
|
+
try {
|
|
1966
|
+
server.listen({ port, host: hostname, exclusive: true }, () => {
|
|
1967
|
+
const { port: claimed } = server.address();
|
|
1968
|
+
server.close(() => resolve2(claimed));
|
|
1969
|
+
});
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
reject(error);
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
var REASONS = {
|
|
1976
|
+
EADDRINUSE: "Address already in use",
|
|
1977
|
+
EACCES: "Permission denied",
|
|
1978
|
+
EADDRNOTAVAIL: "Can't assign requested address"
|
|
1979
|
+
};
|
|
1980
|
+
function listenFailure(hostname, port, error) {
|
|
1981
|
+
const { code, message } = error;
|
|
1982
|
+
return `Failed to listen on ${displayHost(hostname)}:${port} (reason: ${code !== void 0 && REASONS[code] || message})`;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// watch/WatchedServer.ts
|
|
1986
|
+
import { spawn as spawnProcess } from "child_process";
|
|
1987
|
+
var WatchedServer = class {
|
|
1988
|
+
constructor(options) {
|
|
1989
|
+
this.options = options;
|
|
1990
|
+
}
|
|
1991
|
+
options;
|
|
1992
|
+
running;
|
|
1993
|
+
restarting = false;
|
|
1994
|
+
stopping = false;
|
|
1995
|
+
finish = () => {
|
|
1996
|
+
};
|
|
1997
|
+
/** Starts the server and keeps it current until stopped. Resolves 0 once the last server has exited. */
|
|
1998
|
+
run() {
|
|
1999
|
+
const done = new Promise((resolve2) => this.finish = resolve2);
|
|
2000
|
+
const stopListening = this.listenForSignals();
|
|
2001
|
+
const watcher = this.options.watch((files) => void this.changed(files));
|
|
2002
|
+
this.start();
|
|
2003
|
+
return done.finally(() => {
|
|
2004
|
+
watcher.close();
|
|
2005
|
+
stopListening();
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
/** Stops the server and ends run(); called again, kills the server at once. */
|
|
2009
|
+
stop() {
|
|
2010
|
+
if (this.stopping) {
|
|
2011
|
+
this.running?.child.kill("SIGKILL");
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
this.stopping = true;
|
|
2015
|
+
void this.stopServer().then(() => this.finish(0));
|
|
2016
|
+
}
|
|
2017
|
+
async changed(files) {
|
|
2018
|
+
if (this.stopping || this.restarting || files.length === 0) return;
|
|
2019
|
+
this.restarting = true;
|
|
2020
|
+
try {
|
|
2021
|
+
this.options.info(
|
|
2022
|
+
files.some(isEnvironmentFile) ? "Environment modified. Restarting server..." : `Application change detected in [${files[0]}]. Restarting server...`
|
|
2023
|
+
);
|
|
2024
|
+
await this.stopServer();
|
|
2025
|
+
if (!this.stopping) this.start();
|
|
2026
|
+
} finally {
|
|
2027
|
+
this.restarting = false;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
start() {
|
|
2031
|
+
const [command, ...args] = this.options.command;
|
|
2032
|
+
const child = (this.options.spawn ?? spawnProcess)(command, args, {
|
|
2033
|
+
cwd: this.options.cwd,
|
|
2034
|
+
env: this.options.env,
|
|
2035
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
2036
|
+
});
|
|
2037
|
+
const running = { child, stopping: false, killed: false, exited: Promise.resolve() };
|
|
2038
|
+
running.exited = new Promise((resolve2) => {
|
|
2039
|
+
let settled = false;
|
|
2040
|
+
const settle = (report) => {
|
|
2041
|
+
if (settled) return;
|
|
2042
|
+
settled = true;
|
|
2043
|
+
if (this.running === running) this.running = void 0;
|
|
2044
|
+
if (!running.stopping && !this.stopping) this.options.error(`${report}. Waiting for changes before restarting...`);
|
|
2045
|
+
resolve2();
|
|
2046
|
+
};
|
|
2047
|
+
child.once("exit", (code, signal) => settle(signal !== null ? `The server stopped (signal ${signal})` : `The server stopped (exit code ${code})`));
|
|
2048
|
+
child.once("error", (error) => {
|
|
2049
|
+
if (child.pid === void 0) settle(`The server could not start (${error.message})`);
|
|
2050
|
+
});
|
|
2051
|
+
});
|
|
2052
|
+
this.running = running;
|
|
2053
|
+
}
|
|
2054
|
+
/** Sends SIGTERM once and waits for the exit, killing the server after stopTimeout. */
|
|
2055
|
+
async stopServer() {
|
|
2056
|
+
const running = this.running;
|
|
2057
|
+
if (running === void 0) return;
|
|
2058
|
+
if (!running.stopping) {
|
|
2059
|
+
running.stopping = true;
|
|
2060
|
+
running.child.kill("SIGTERM");
|
|
2061
|
+
}
|
|
2062
|
+
const timeout = this.options.stopTimeout ?? 15e3;
|
|
2063
|
+
const timer = setTimeout(() => {
|
|
2064
|
+
if (running.killed) return;
|
|
2065
|
+
running.killed = true;
|
|
2066
|
+
this.options.error(`The server did not stop within ${timeout / 1e3} seconds and was killed.`);
|
|
2067
|
+
running.child.kill("SIGKILL");
|
|
2068
|
+
}, timeout);
|
|
2069
|
+
try {
|
|
2070
|
+
await running.exited;
|
|
2071
|
+
} finally {
|
|
2072
|
+
clearTimeout(timer);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
listenForSignals() {
|
|
2076
|
+
const signals = this.options.signals ?? ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
2077
|
+
const handler = () => this.stop();
|
|
2078
|
+
for (const signal of signals) process.on(signal, handler);
|
|
2079
|
+
return () => {
|
|
2080
|
+
for (const signal of signals) process.off(signal, handler);
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
|
|
2085
|
+
// commands.ts
|
|
1804
2086
|
function io(command) {
|
|
1805
2087
|
return {
|
|
1806
2088
|
option: (name) => {
|
|
@@ -1874,11 +2156,36 @@ ${web[1]}api: 'routes/api.ts',`);
|
|
|
1874
2156
|
${create[0]}`);
|
|
1875
2157
|
return void 0;
|
|
1876
2158
|
}
|
|
2159
|
+
async function serveWatching(app, out, port, run) {
|
|
2160
|
+
const hostname = out.option("host") ?? "127.0.0.1";
|
|
2161
|
+
const requested = port !== void 0 ? Number(port) : Number(process.env.PORT ?? process.env.SERVER_PORT ?? 8e3);
|
|
2162
|
+
let claimed;
|
|
2163
|
+
try {
|
|
2164
|
+
claimed = await claimPort(hostname, requested);
|
|
2165
|
+
} catch (error) {
|
|
2166
|
+
out.error(listenFailure(hostname, requested, error));
|
|
2167
|
+
return 1;
|
|
2168
|
+
}
|
|
2169
|
+
const script = process.argv[1];
|
|
2170
|
+
if (script === void 0) {
|
|
2171
|
+
out.error("serve --watch can't tell which script started this process, so it can't start the server.");
|
|
2172
|
+
return 1;
|
|
2173
|
+
}
|
|
2174
|
+
return run({
|
|
2175
|
+
command: [process.execPath, ...process.execArgv, script, "serve", `--host=${hostname}`, `--port=${claimed}`],
|
|
2176
|
+
cwd: process.cwd(),
|
|
2177
|
+
env: serverEnvironment(),
|
|
2178
|
+
watch: (onChange) => watchFiles(app.basePath(), onChange, { onError: (folder, error) => out.warn(`Unable to watch [${folder}] for changes: ${error.message}`) }),
|
|
2179
|
+
info: (message) => out.info(message),
|
|
2180
|
+
error: (message) => out.error(message)
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
1877
2183
|
function registerHttpCommands(kernel, app, options = {}) {
|
|
1878
2184
|
const serveApp = options.serve ?? serve;
|
|
1879
2185
|
const terminalWidth = options.terminalWidth ?? (() => process.stdout.columns ?? 120);
|
|
2186
|
+
const runWatching = options.watch ?? ((watchOptions) => new WatchedServer(watchOptions).run());
|
|
1880
2187
|
kernel.command(
|
|
1881
|
-
"serve {--host=127.0.0.1 : The host address to serve the application on} {--port= : The port to serve the application on}",
|
|
2188
|
+
"serve {--host=127.0.0.1 : The host address to serve the application on} {--port= : The port to serve the application on} {--watch : Automatically restart the server when the application is modified}",
|
|
1882
2189
|
async function() {
|
|
1883
2190
|
const out = io(this);
|
|
1884
2191
|
const port = out.option("port");
|
|
@@ -1886,6 +2193,7 @@ function registerHttpCommands(kernel, app, options = {}) {
|
|
|
1886
2193
|
out.error("The --port option must be a number.");
|
|
1887
2194
|
return 1;
|
|
1888
2195
|
}
|
|
2196
|
+
if (out.flag("watch")) return serveWatching(app, out, port, runWatching);
|
|
1889
2197
|
const server = await serveApp(app, {
|
|
1890
2198
|
hostname: out.option("host") ?? "127.0.0.1",
|
|
1891
2199
|
...port !== void 0 && { port: Number(port) },
|
|
@@ -1933,7 +2241,7 @@ function registerHttpCommands(kernel, app, options = {}) {
|
|
|
1933
2241
|
kernel.command("install:api {--force : Overwrite any existing API routes file}", async function() {
|
|
1934
2242
|
const out = io(this);
|
|
1935
2243
|
const routesFile = app.basePath("routes/api.ts");
|
|
1936
|
-
if (
|
|
2244
|
+
if (existsSync3(routesFile) && !out.flag("force")) {
|
|
1937
2245
|
out.error("API routes file already exists.");
|
|
1938
2246
|
} else {
|
|
1939
2247
|
await mkdir(dirname(routesFile), { recursive: true });
|
|
@@ -1941,7 +2249,7 @@ function registerHttpCommands(kernel, app, options = {}) {
|
|
|
1941
2249
|
out.info("Published API routes file.");
|
|
1942
2250
|
}
|
|
1943
2251
|
const bootstrapFile = app.bootstrapPath("app.ts");
|
|
1944
|
-
const source =
|
|
2252
|
+
const source = existsSync3(bootstrapFile) ? await readFile2(bootstrapFile, "utf8") : void 0;
|
|
1945
2253
|
const updated = source === void 0 ? void 0 : addApiRouting(source);
|
|
1946
2254
|
if (updated === void 0) {
|
|
1947
2255
|
out.warn(
|
|
@@ -1950,7 +2258,7 @@ function registerHttpCommands(kernel, app, options = {}) {
|
|
|
1950
2258
|
} else if (updated !== source) {
|
|
1951
2259
|
await writeFile(bootstrapFile, updated);
|
|
1952
2260
|
}
|
|
1953
|
-
out.info("API scaffolding installed.
|
|
2261
|
+
out.info("API scaffolding installed.");
|
|
1954
2262
|
return 0;
|
|
1955
2263
|
}).purpose("Create an API routes file and prepare the application for API authentication");
|
|
1956
2264
|
}
|