@thoughtpivot/flight 1.0.8 → 2.0.1
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 +24 -0
- package/README.md +169 -137
- package/dist/flight.js +128 -15
- package/dist/spa-pipeline.js +122 -0
- package/package.json +12 -2
- package/readme-assets/thoughtpivot-logo.svg +17 -0
- package/src/flight.ts +171 -35
- package/.circleci/config.yml +0 -83
- package/.editorconfig +0 -5
- package/.github/dependabot.yml +0 -11
- package/.nvmrc +0 -1
- package/.prettierrc +0 -11
- package/.vscode/extensions.json +0 -3
- package/.vscode/settings.json +0 -10
- package/eslint.config.mjs +0 -32
- package/tsconfig.json +0 -10
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **Production middleware order** when `FLIGHT_MODE` / `mode` is `production` **and** built-assets mode is on (`--disable_vite` or `FLIGHT_DISABLE_VITE` of `true` / `1` / `yes`): Flight now uses the **production SPA pipeline** by default. Static files from `FLIGHT_DIST_PATH` (default `../dist`) and the SPA `index.html` fallback run **immediately after** the Koa `router` (your `**/*.backend.ts` routes), **before** `koa-compress`, Redis-backed rate limiting, and optional response caching.
|
|
8
|
+
- **`koa-cash` is off by default** in that SPA pipeline (to avoid caching HTML or mixed `vary` surprises). Enable with **`FLIGHT_HTTP_CACHE=true`** (or `1` / `yes`) if you want the previous Redis-backed cache layer in that configuration.
|
|
9
|
+
- **Legacy stack** (previous order: `compress` → `ratelimit` → `koa-cash` → `koa-static` after the router) remains when **either** Vite is **not** disabled in production **or** you set **`FLIGHT_DISABLE_SPA_PIPELINE=true`** (or `1` / `yes`).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Yargs CLI parity** for **`--mode`**, **`--port`**, **`--app_home`**, **`--app_key`**, **`--app_secret`**, **`--payload_limit`**, and **`--disable_vite`** (plus kebab-case aliases), so documented flags populate `argv` reliably instead of being dropped by the parser.
|
|
14
|
+
- **`FLIGHT_TRUST_PROXY`**: when `true` / `1` / `yes`, sets `app.proxy = true` so `ctx.ip` and rate-limit identity honor `X-Forwarded-For` behind a reverse proxy or load balancer.
|
|
15
|
+
- **`FLIGHT_STATIC_PREFIXES`**: comma-separated URL path prefixes (default `/assets,/fonts`) used to build the **rate-limit skip** list for `GET`/`HEAD` (hashed assets should not burn the API limiter).
|
|
16
|
+
- **`FLIGHT_RATE_LIMIT_EXCLUDE_PREFIXES`**: extra comma-separated prefixes merged into that skip list.
|
|
17
|
+
- **`FLIGHT_SPA_INDEX`**: path to the SPA shell inside the dist root (default `index.html`).
|
|
18
|
+
- **`FLIGHT_SPA_DENY_PREFIXES`**: extra comma-separated path prefixes that never receive the SPA HTML fallback (always merged with `/api` and `/health`).
|
|
19
|
+
- **`npm test`**: regression tests for the SPA pipeline (assets, deep links, API prefix, file-like paths).
|
|
20
|
+
- **`files` field** in `package.json` so publishes include only `dist/flight.js`, `dist/spa-pipeline.js`, and docs assets.
|
|
21
|
+
|
|
22
|
+
### Notes
|
|
23
|
+
|
|
24
|
+
- `FLIGHT_DISABLE_VITE` accepts **`true`**, **`1`**, or **`yes`** (case-sensitive values as implemented for the string checks).
|
package/README.md
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="readme-assets/thoughtpivot-logo.svg" alt="ThoughtPivot" width="280" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# Flight
|
|
2
6
|
|
|
3
|
-
Flight is a
|
|
7
|
+
**Flight** is a Node.js application server for teams who want something **fast**, **boring in the good way**, and **ready for serious traffic**. You bring your own hosting—there is no lock-in to a proprietary edge or a single vendor’s deployment story. It fits **twelve-factor** style workflows: configuration via environment variables, horizontal scaling, and state kept where it belongs (for Flight, that includes **Redis** for sessions and cache-friendly layers).
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
Think **platform-agnostic**: not framework-as-a-platform, but a clear runtime you can run wherever Node runs—VMs, Kubernetes, bare metal, your cloud of choice. Flight is aimed at **hyperscale-friendly** designs (cluster workers out of the box), **ephemeral** processes, and **component-shaped** backends so routes stay colocated with the features they serve. **Vue** and **Vite** are first-class today; **React** support is on the roadmap.
|
|
6
10
|
|
|
7
|
-
Flight
|
|
11
|
+
Flight is **open source** from **[ThoughtPivot](https://github.com/thoughtpivot)**.
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## Highlights
|
|
10
14
|
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **Production
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **Hot Module Replacement**: Fast development with instant updates
|
|
19
|
-
- **CORS Enabled**: Ready for modern web applications
|
|
20
|
-
- **Security Features**: Rate limiting, secure session handling, and more
|
|
15
|
+
- **Performance-focused**: Cluster mode, compression, Redis-backed caching hooks, rate limiting in production
|
|
16
|
+
- **Developer velocity**: Vite-powered dev server with HMR for **Vue** (React roadmap)
|
|
17
|
+
- **Composable backends**: Discover `**/*.backend.ts` under your app root and mount Koa routes per component
|
|
18
|
+
- **Production SPA**: Built-in **`dist` + `index.html`** fallback (option B) when running **`production`** with **`disable_vite`**, with an explicit opt-out for API-only processes
|
|
19
|
+
- **Configurable discovery**: `--exclude_paths` / `FLIGHT_EXCLUDE_PATHS` to skip directories when scanning backends
|
|
20
|
+
- **TypeScript-native**: Written for TS projects; sensible defaults, minimal ceremony
|
|
21
|
+
- **Interop-friendly**: Correct handling of `yargs` when launched via **tsx** or similar loaders (no patch-package needed from **v1.0.8** onward)
|
|
21
22
|
|
|
22
23
|
## Installation
|
|
23
24
|
|
|
@@ -27,11 +28,11 @@ npm install @thoughtpivot/flight
|
|
|
27
28
|
yarn add @thoughtpivot/flight
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
Legacy npm scope: the package was previously published as `@spytech/flight`—use `@thoughtpivot/flight` going forward.
|
|
31
32
|
|
|
32
|
-
### Downstream apps (tsx
|
|
33
|
+
### Downstream apps (tsx)
|
|
33
34
|
|
|
34
|
-
From **v1.0.8** onward, Flight normalizes `require('yargs/yargs')`
|
|
35
|
+
From **v1.0.8** onward, Flight normalizes `require('yargs/yargs')` under loaders that expose `{ default: factory }`. If you added **patch-package** only for that issue, upgrade Flight and drop that patch.
|
|
35
36
|
|
|
36
37
|
## Quick Start
|
|
37
38
|
|
|
@@ -49,13 +50,7 @@ npm init -y
|
|
|
49
50
|
npm install @thoughtpivot/flight ioredis
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
3. Ensure Redis is running locally or set environment variables
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
# Default values shown below
|
|
56
|
-
export FLIGHT_REDIS_HOST=localhost
|
|
57
|
-
export FLIGHT_REDIS_PORT=6379
|
|
58
|
-
```
|
|
53
|
+
3. Ensure Redis is running locally or set environment variables (see table below).
|
|
59
54
|
|
|
60
55
|
4. Create a component with a backend route:
|
|
61
56
|
|
|
@@ -77,49 +72,130 @@ router.get('/hello', async (ctx) => {
|
|
|
77
72
|
export default router.routes()
|
|
78
73
|
```
|
|
79
74
|
|
|
80
|
-
5.
|
|
75
|
+
5. Add scripts to **`package.json`** (the `flight` binary comes from **`node_modules/.bin`** after install):
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"scripts": {
|
|
80
|
+
"dev": "flight --mode development",
|
|
81
|
+
"start": "flight --mode production"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use **`FLIGHT_DISABLE_VITE`**, **`FLIGHT_DIST_PATH`**, and related env vars in **`.env`** for production built-assets mode (see the configuration table below).
|
|
81
87
|
|
|
82
|
-
|
|
88
|
+
6. Start the server:
|
|
89
|
+
|
|
90
|
+
**Development (Vite on 3001, API on 3000):**
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx flight --mode development
|
|
94
|
+
# or: npm run dev
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Production:**
|
|
83
98
|
|
|
84
99
|
```bash
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
# Backend API available on port 3000
|
|
100
|
+
npx flight --mode production
|
|
101
|
+
# or: npm run start
|
|
88
102
|
```
|
|
89
103
|
|
|
90
|
-
|
|
104
|
+
**`--app_home`** defaults to **`.`** (the current working directory). Pass **`--app_home path/to/app`** only when your app root is not the directory you run the command from.
|
|
105
|
+
|
|
106
|
+
**Try without adding a dependency** (downloads the package for this invocation):
|
|
91
107
|
|
|
92
108
|
```bash
|
|
93
|
-
|
|
94
|
-
# Builds and serves application on port 3000
|
|
109
|
+
npx --yes --package @thoughtpivot/flight flight --mode development
|
|
95
110
|
```
|
|
96
111
|
|
|
97
|
-
|
|
112
|
+
**Global install** (optional): `npm install -g @thoughtpivot/flight`, then run **`flight`** from your PATH the same way as **`npx flight`**.
|
|
98
113
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
|
|
114
|
+
## Development vs production
|
|
115
|
+
|
|
116
|
+
Flight picks **`mode`** as: **`FLIGHT_MODE`** (if set and non-empty), else **`--mode`** from argv, else **`production`**. Everything below assumes Redis is reachable unless you only use routes that avoid session/ratelimit/cache.
|
|
117
|
+
|
|
118
|
+
| Topic | Development | Production |
|
|
119
|
+
| ---------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| **Vite** | Child process: `npx vite --port 3001 --host 0.0.0.0` (HMR). | If **`disable_vite` is false**: `exec('npx vite build')` is triggered once at worker startup. If **`disable_vite` is true**: no Vite build. |
|
|
121
|
+
| **Listen port** | API + sessions on **`--port`** / `FLIGHT_PORT` (default **3000**). UI dev server on **3001**. | Same **`--port`** / `FLIGHT_PORT` for the worker. |
|
|
122
|
+
| **Static / SPA** | The Vite dev server serves UI; Flight does **not** mount the production static/SPA stack. | See **Production SPA pipeline** below: when **`production`** + **`disable_vite`**, Flight uses **Option A** by default unless opted out. |
|
|
123
|
+
| **Typical use** | Local full-stack with HMR. | CI-built `dist` served by Flight (or behind a load balancer). |
|
|
124
|
+
|
|
125
|
+
**Edge cases**
|
|
126
|
+
|
|
127
|
+
- **`production` + `disable_vite` false**: Vite build still runs; **legacy** middleware order applies (`compress` → `ratelimit` → `koa-cash` → static), because built-assets-only mode is off.
|
|
128
|
+
- **`production` + `disable_vite` true + `FLIGHT_DISABLE_SPA_PIPELINE`**: **Legacy** stack only—no SPA HTML fallback for unknown GETs; tail `koa-static` only after compress/ratelimit/cache.
|
|
129
|
+
- **`development`**: unchanged—Redis session store and production-only static/SPA branches do not apply the SPA pipeline section (the dev Vite server owns UI URLs).
|
|
130
|
+
|
|
131
|
+
## Configuration: CLI, `.env`, and environment variables
|
|
132
|
+
|
|
133
|
+
Flight loads a **`.env`** file from the **current working directory** at startup (via **`dotenv`**). Use **`FLIGHT_*`** in real deployments.
|
|
134
|
+
|
|
135
|
+
**Precedence**
|
|
136
|
+
|
|
137
|
+
- **`mode`**: **`FLIGHT_MODE`** wins over **`--mode`** when the variable is set (non-empty).
|
|
138
|
+
- **`disable_vite`**: explicit CLI wins; if omitted, **`FLIGHT_DISABLE_VITE`** (`true` / `1` / `yes` → on); otherwise **`false`**.
|
|
139
|
+
- Other settings: argv value if your launcher passes it, else the **`FLIGHT_*`** column, then default.
|
|
140
|
+
|
|
141
|
+
**Registered CLI options:** Flight registers **`--exclude_paths`**, **`--mode`**, **`--port`**, **`--app_home`**, **`--app_key`**, **`--app_secret`**, **`--payload_limit`**, and **`--disable_vite`** (with kebab-case aliases where listed in `--help`). yargs is not in **strict** mode, so additional flags may still appear on argv if you extend the entrypoint.
|
|
142
|
+
|
|
143
|
+
| Setting (CLI / argv) | Environment variable | Default | Use case |
|
|
144
|
+
| ------------------------------------ | ------------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
145
|
+
| `--exclude_paths`, `--exclude-paths` | `FLIGHT_EXCLUDE_PATHS` | _(empty)_ | Skip subtrees when globbing `**/*.backend.ts` (monorepo vendor trees, fixtures). |
|
|
146
|
+
| `--app_home` | `FLIGHT_APP_HOME` | `.` | Repository root; `process.chdir` target before discovery and static paths. |
|
|
147
|
+
| `--app_key` | `FLIGHT_APP_KEY` | `flightApp` | Session cookie name. |
|
|
148
|
+
| `--app_secret` | `FLIGHT_APP_SECRET` | _(see code)_ | Cookie signing secret(s); comma-separated for key rotation. |
|
|
149
|
+
| _(argv)_ | `FLIGHT_SESSION_DURATION_MS` | `86400000` | Session `maxAge` in ms. |
|
|
150
|
+
| `--port` | `FLIGHT_PORT` | `3000` | HTTP listen port (all modes). |
|
|
151
|
+
| `--payload_limit` | `FLIGHT_PAYLOAD_LIMIT` | `1mb` | `koa-bodyparser` JSON limit. |
|
|
152
|
+
| `--disable_vite` | `FLIGHT_DISABLE_VITE` | `false` | **`true` / `1` / `yes`**: skip `vite build` and treat the process as **built-assets** mode (triggers SPA pipeline rules in production). |
|
|
153
|
+
| `--mode` | `FLIGHT_MODE` | `production` | `development` spawns Vite; `production` enables production middleware. |
|
|
154
|
+
| _(n/a)_ | `FLIGHT_DIST_PATH` | `../dist` | Filesystem root for `koa-static` (relative to cwd **after** chdir to `app_home`). |
|
|
155
|
+
| _(n/a)_ | `FLIGHT_DISABLE_SPA_PIPELINE` | _(unset)_ | **`1` / `true` / `yes`**: force **legacy** production stack (no in-router SPA fallback) while keeping `production` + `disable_vite`. Use for **API-only** workers or until duplicate app middleware is removed. |
|
|
156
|
+
| _(n/a)_ | `FLIGHT_STATIC_PREFIXES` | `/assets,/fonts` | Baseline URL prefixes for **rate-limit skipping** on `GET`/`HEAD` (static hashed chunks). |
|
|
157
|
+
| _(n/a)_ | `FLIGHT_RATE_LIMIT_EXCLUDE_PREFIXES` | _(empty)_ | Extra prefixes merged with `FLIGHT_STATIC_PREFIXES` for the same skip list. |
|
|
158
|
+
| _(n/a)_ | `FLIGHT_SPA_INDEX` | `index.html` | SPA shell file inside the dist root for HTML fallback. |
|
|
159
|
+
| _(n/a)_ | `FLIGHT_SPA_DENY_PREFIXES` | _(empty)_ | Extra prefixes that never get SPA fallback (merged with `/api`, `/health`). |
|
|
160
|
+
| _(n/a)_ | `FLIGHT_TRUST_PROXY` | `false` | **`1` / `true` / `yes`**: set **`app.proxy = true`** so `ctx.ip` / rate-limit id use **`X-Forwarded-For`** behind an L7 LB. |
|
|
161
|
+
| _(n/a)_ | `FLIGHT_HTTP_CACHE` | `false` | **`1` / `true` / `yes`**: enable **`koa-cash`** in the **SPA pipeline** branch only (off by default there). Legacy branch always attaches `koa-cash`. |
|
|
162
|
+
| _(n/a)_ | `FLIGHT_REDIS_HOST` | `localhost` | Redis for sessions, rate limit, and cache adapter. |
|
|
163
|
+
| _(n/a)_ | `FLIGHT_REDIS_PORT` | `6379` | Redis port. |
|
|
164
|
+
| _(n/a)_ | `FLIGHT_MAX_WORKERS` | CPU count | Cap cluster worker count on small nodes. |
|
|
165
|
+
|
|
166
|
+
Example `.env` fragment:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
FLIGHT_MODE=development
|
|
170
|
+
FLIGHT_APP_HOME=.
|
|
171
|
+
FLIGHT_REDIS_HOST=127.0.0.1
|
|
172
|
+
FLIGHT_REDIS_PORT=6379
|
|
173
|
+
FLIGHT_PORT=3000
|
|
174
|
+
FLIGHT_MAX_WORKERS=4
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Production behind a proxy:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
FLIGHT_MODE=production
|
|
181
|
+
FLIGHT_DISABLE_VITE=true
|
|
182
|
+
FLIGHT_TRUST_PROXY=1
|
|
183
|
+
```
|
|
103
184
|
|
|
104
185
|
## Project Structure
|
|
105
186
|
|
|
106
187
|
```
|
|
107
188
|
my-app/
|
|
108
|
-
├── components/
|
|
109
|
-
│
|
|
110
|
-
│
|
|
111
|
-
│
|
|
112
|
-
├── assets/
|
|
113
|
-
├── dist/
|
|
189
|
+
├── components/
|
|
190
|
+
│ └── Hello/
|
|
191
|
+
│ ├── Hello.vue # Vue UI (example)
|
|
192
|
+
│ └── Hello.backend.ts # Koa routes for this component
|
|
193
|
+
├── assets/
|
|
194
|
+
├── dist/ # Production build output
|
|
114
195
|
└── package.json
|
|
115
196
|
```
|
|
116
197
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
- `Index.vue`: Contains the Vue component template, script, and styles
|
|
120
|
-
- `Index.backend.ts`: Contains the backend routes and logic for the component
|
|
121
|
-
|
|
122
|
-
Example component files:
|
|
198
|
+
Example Vue + backend snippets:
|
|
123
199
|
|
|
124
200
|
`components/hello/Index.vue`:
|
|
125
201
|
|
|
@@ -151,107 +227,67 @@ router.get('/hello', async (ctx) => {
|
|
|
151
227
|
export default router.routes()
|
|
152
228
|
```
|
|
153
229
|
|
|
154
|
-
##
|
|
230
|
+
## Development mode
|
|
155
231
|
|
|
156
|
-
|
|
232
|
+
- **Vue** + **Vite** with HMR on port **3001**
|
|
233
|
+
- Backend worker on **`--port`** (default **3000**)
|
|
234
|
+
- Request logging via **`koa-logger`**
|
|
157
235
|
|
|
158
|
-
|
|
159
|
-
FLIGHT_MODE=development
|
|
160
|
-
FLIGHT_REDIS_HOST=localhost
|
|
161
|
-
FLIGHT_REDIS_PORT=6379
|
|
162
|
-
FLIGHT_MAX_WORKERS=4
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## Development Mode
|
|
166
|
-
|
|
167
|
-
In development mode, Flight provides:
|
|
168
|
-
|
|
169
|
-
- Hot Module Replacement (HMR)
|
|
170
|
-
- Fast refresh for React components
|
|
171
|
-
- Detailed error messages
|
|
172
|
-
- Development server on port 3001
|
|
173
|
-
|
|
174
|
-
## Production Mode
|
|
236
|
+
## Production mode
|
|
175
237
|
|
|
176
|
-
|
|
238
|
+
- Optional **`npx vite build`** before serving static assets when **`disable_vite`** is **false**
|
|
239
|
+
- Cluster workers capped by **`FLIGHT_MAX_WORKERS`**
|
|
240
|
+
- Compression + Redis-backed rate limiting in all production configurations
|
|
241
|
+
- Static dist + SPA behavior depends on **built-assets mode**—see next section
|
|
177
242
|
|
|
178
|
-
|
|
179
|
-
- Rate limiting
|
|
180
|
-
- Response compression
|
|
181
|
-
- Redis caching
|
|
182
|
-
- Cluster mode for load balancing
|
|
183
|
-
- Production server on port 3000
|
|
243
|
+
## Production SPA pipeline (v2)
|
|
184
244
|
|
|
185
|
-
|
|
245
|
+
**When it is on (option B):** `mode === 'production'` **and** **`disable_vite === true`** **and** **`FLIGHT_DISABLE_SPA_PIPELINE` is not truthy.**
|
|
186
246
|
|
|
187
|
-
|
|
247
|
+
**What it does:** After your **`router`** (all `**/*.backend.ts` routes), Flight serves **`FLIGHT_DIST_PATH`** with **`koa-static`**, then applies an **`index.html`** fallback for typical browser navigations (see tests in `src/spa-pipeline.test.ts` for the behavioral contract). Only then runs **`koa-compress`**, Redis **`koa-ratelimit`** (with **`GET`/`HEAD` skipped** under `FLIGHT_STATIC_PREFIXES` ∪ `FLIGHT_RATE_LIMIT_EXCLUDE_PREFIXES`), and optionally **`koa-cash`** when **`FLIGHT_HTTP_CACHE`** is enabled.
|
|
188
248
|
|
|
189
|
-
|
|
249
|
+
**Path ownership**
|
|
190
250
|
|
|
191
|
-
- **
|
|
192
|
-
- **
|
|
193
|
-
- **
|
|
251
|
+
- **API / BFF:** Implement on your `Router` in `*.backend.ts` under prefixes you own (conventionally **`/api/...`**). Those paths are excluded from the SPA fallback, along with **`/health`** and any **`FLIGHT_SPA_DENY_PREFIXES`**.
|
|
252
|
+
- **Static:** Any file present under the dist root is served by **`koa-static`** first.
|
|
253
|
+
- **SPA:** Remaining **`GET`/`HEAD`** requests that look like document navigations and whose last path segment has **no dot** (so `file.ext` URLs are not rewritten) may receive **`FLIGHT_SPA_INDEX`**.
|
|
194
254
|
|
|
195
|
-
|
|
255
|
+
**Load balancers and `X-Forwarded-For`**
|
|
196
256
|
|
|
197
|
-
1. **
|
|
198
|
-
|
|
199
|
-
- Flight: Streamlined with `.backend.ts` files and modern frontend frameworks
|
|
200
|
-
2. **Development Experience**
|
|
201
|
-
- Avian: Webpack-based bundling with slower rebuild times
|
|
202
|
-
- Flight: Vite-powered development with instant HMR and no bundle step in development
|
|
257
|
+
- Set **`FLIGHT_TRUST_PROXY=1`** when Flight sits behind a trusted reverse proxy so **`ctx.ip`** and the rate limiter’s **`id`** reflect the client. Configure your proxy to append **one** well-formed **`X-Forwarded-For`** chain.
|
|
258
|
+
- Sessions use cookies (`sameSite: true` in code today); sticky sessions or shared Redis are operational choices outside Flight’s defaults.
|
|
203
259
|
|
|
204
|
-
|
|
205
|
-
- Avian: Express-session with Redis store
|
|
206
|
-
- Flight: Koa-session with Redis store, improved security defaults
|
|
260
|
+
### Middleware order (mermaid)
|
|
207
261
|
|
|
208
|
-
|
|
209
|
-
- Built-in rate limiting
|
|
210
|
-
- Redis-based caching
|
|
211
|
-
- Automatic compression in production
|
|
212
|
-
- Cluster mode for CPU utilization
|
|
262
|
+
**SPA pipeline (default when production + `disable_vite`):**
|
|
213
263
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
2. **New Features**
|
|
227
|
-
- Native ESM support
|
|
228
|
-
- Built-in CORS support
|
|
229
|
-
- Improved Redis integration
|
|
230
|
-
- Better security defaults
|
|
231
|
-
- Simpler API for backend routes
|
|
232
|
-
- Modern frontend tooling support
|
|
264
|
+
```mermaid
|
|
265
|
+
flowchart TD
|
|
266
|
+
A[cors + bodyparser + session] --> B[router + *.backend.ts]
|
|
267
|
+
B --> C[koa-static dist]
|
|
268
|
+
C --> D[SPA index.html fallback]
|
|
269
|
+
D --> E[koa-compress]
|
|
270
|
+
E --> F[koa-ratelimit with prefix skips]
|
|
271
|
+
F --> G{koa-cash}
|
|
272
|
+
G -->|FLIGHT_HTTP_CACHE on| H[Redis-backed cache]
|
|
273
|
+
G -->|default| I[no cache middleware]
|
|
274
|
+
```
|
|
233
275
|
|
|
234
|
-
|
|
235
|
-
- Reduced configuration complexity
|
|
236
|
-
- More intuitive component organization
|
|
237
|
-
- Better separation of concerns
|
|
238
|
-
- Modern middleware approach
|
|
276
|
+
**Legacy production stack** (`disable_vite` false, or **`FLIGHT_DISABLE_SPA_PIPELINE`** set): `router` → **`compress`** → **`ratelimit`** → **`koa-cash`** → **`koa-static`**.
|
|
239
277
|
|
|
240
|
-
###
|
|
278
|
+
### Migrating from Flight 1.x
|
|
241
279
|
|
|
242
|
-
If you
|
|
280
|
+
1. If you rely on the **old** order (everything after `router` saw compress/ratelimit/cache **before** static), set **`FLIGHT_DISABLE_SPA_PIPELINE=1`** until you validate the new behavior.
|
|
281
|
+
2. If your app already shipped **custom SPA / history fallback** middleware in `*.backend.ts`, remove the duplicate once you confirm Flight’s fallback matches your routes (especially non-`/api` prefixes—use **`FLIGHT_SPA_DENY_PREFIXES`**).
|
|
282
|
+
3. Expect **`koa-cash` off** in the SPA pipeline unless you opt in with **`FLIGHT_HTTP_CACHE`**.
|
|
243
283
|
|
|
244
|
-
|
|
245
|
-
2. Move from Express middleware to Koa middleware
|
|
246
|
-
3. Update your frontend bundling to use Vite
|
|
247
|
-
4. Adapt to the new session management system
|
|
248
|
-
5. Update your static file serving configuration
|
|
284
|
+
See **[CHANGELOG.md](CHANGELOG.md)** for the full **2.0.0** notes.
|
|
249
285
|
|
|
250
286
|
## Requirements
|
|
251
287
|
|
|
252
|
-
- Node.js 16.x or higher
|
|
253
|
-
- Redis
|
|
254
|
-
- TypeScript
|
|
288
|
+
- Node.js **16.x** or higher
|
|
289
|
+
- **Redis** (sessions / rate limit / cache integrations)
|
|
290
|
+
- **TypeScript** in your app if you author `.backend.ts` modules as TS
|
|
255
291
|
|
|
256
292
|
## License
|
|
257
293
|
|
|
@@ -259,14 +295,10 @@ MIT
|
|
|
259
295
|
|
|
260
296
|
## Contributing
|
|
261
297
|
|
|
262
|
-
|
|
298
|
+
Issues and pull requests are welcome. Flight improves fastest with real workloads—if you hit an edge case, open an issue with a minimal repro.
|
|
263
299
|
|
|
264
300
|
## Acknowledgments
|
|
265
301
|
|
|
266
|
-
Flight succeeds
|
|
267
|
-
|
|
268
|
-
We’re grateful to FlyPaper Technologies for stewarding Avian and advancing component-driven architecture.
|
|
269
|
-
|
|
270
|
-
## ThoughtPivot
|
|
302
|
+
**Flight** succeeds **[Avian](https://github.com/ispyhumanfly/avian)**—the component-oriented Node server that helped prove this programming model. **Dan Stevenson** created Avian and carried it into **FlyPaper Technologies**, where **Nick Fredericks**, Dan, and the FlyPaper team sharpened Avian’s component boundaries and pushed its operational story. Dan continued to maintain Avian while **Flight** took shape to embrace newer tooling and a cleaner baseline for the next decade.
|
|
271
303
|
|
|
272
|
-
Today Flight is
|
|
304
|
+
Today **Flight** is maintained by the **ThoughtPivot** engineering team and **contributors like you**—the same spirit of openness and iteration, with a hard focus on speed, clarity, and deployment flexibility.
|
package/dist/flight.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
require("dotenv/config");
|
|
7
8
|
const child_process_1 = require("child_process");
|
|
8
9
|
const koa_1 = __importDefault(require("koa"));
|
|
9
10
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
@@ -21,9 +22,88 @@ const path_1 = __importDefault(require("path"));
|
|
|
21
22
|
const koa_ratelimit_1 = __importDefault(require("koa-ratelimit"));
|
|
22
23
|
const koa_static_1 = __importDefault(require("koa-static"));
|
|
23
24
|
const koa_session_1 = __importDefault(require("koa-session"));
|
|
25
|
+
const spa_pipeline_js_1 = require("./spa-pipeline.js");
|
|
26
|
+
function normalizeExcludePaths(value) {
|
|
27
|
+
if (value == null || value === '')
|
|
28
|
+
return [];
|
|
29
|
+
const parts = Array.isArray(value) ? value : [value];
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const p of parts) {
|
|
32
|
+
const s = String(p).trim();
|
|
33
|
+
if (!s)
|
|
34
|
+
continue;
|
|
35
|
+
out.push(...s
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((x) => x.trim())
|
|
38
|
+
.filter(Boolean));
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function dedupeStrings(items) {
|
|
43
|
+
return [...new Set(items)];
|
|
44
|
+
}
|
|
45
|
+
/** Build fast-glob ignore globs for trees rooted under `appRootAbs`. */
|
|
46
|
+
function backendDiscoveryIgnorePatterns(appRootAbs, excludeRelativeDirs) {
|
|
47
|
+
const patterns = [];
|
|
48
|
+
for (const raw of excludeRelativeDirs) {
|
|
49
|
+
const trimmed = raw.trim();
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
continue;
|
|
52
|
+
const resolved = path_1.default.resolve(appRootAbs, trimmed);
|
|
53
|
+
const rel = path_1.default.relative(appRootAbs, resolved);
|
|
54
|
+
const relPosix = rel.replace(/\\/g, '/');
|
|
55
|
+
if (!relPosix || relPosix.startsWith('..') || path_1.default.isAbsolute(rel)) {
|
|
56
|
+
console.warn(`Flight: exclude_paths entry skipped (outside app_home): ${trimmed}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
patterns.push(`${relPosix}/**`);
|
|
60
|
+
}
|
|
61
|
+
return patterns;
|
|
62
|
+
}
|
|
24
63
|
const _yargs = require('yargs/yargs');
|
|
25
|
-
const
|
|
26
|
-
const argv =
|
|
64
|
+
const yargsEntry = typeof _yargs === 'function' ? _yargs : _yargs.default;
|
|
65
|
+
const argv = yargsEntry(process.argv.slice(2))
|
|
66
|
+
.option('exclude_paths', {
|
|
67
|
+
alias: 'exclude-paths',
|
|
68
|
+
type: 'array',
|
|
69
|
+
string: true,
|
|
70
|
+
default: [],
|
|
71
|
+
describe: 'Directories under app_home to skip when discovering **/*.backend.ts (repeat flag or comma-separated)'
|
|
72
|
+
})
|
|
73
|
+
.option('mode', {
|
|
74
|
+
type: 'string',
|
|
75
|
+
describe: 'development (Vite HMR) or production'
|
|
76
|
+
})
|
|
77
|
+
.option('port', {
|
|
78
|
+
type: 'number',
|
|
79
|
+
describe: 'HTTP listen port'
|
|
80
|
+
})
|
|
81
|
+
.option('app_home', {
|
|
82
|
+
alias: 'app-home',
|
|
83
|
+
type: 'string',
|
|
84
|
+
describe: 'Application root directory'
|
|
85
|
+
})
|
|
86
|
+
.option('app_key', {
|
|
87
|
+
alias: 'app-key',
|
|
88
|
+
type: 'string',
|
|
89
|
+
describe: 'Session cookie name'
|
|
90
|
+
})
|
|
91
|
+
.option('app_secret', {
|
|
92
|
+
alias: 'app-secret',
|
|
93
|
+
type: 'string',
|
|
94
|
+
describe: 'Session signing secret(s), comma-separated for rotation'
|
|
95
|
+
})
|
|
96
|
+
.option('payload_limit', {
|
|
97
|
+
alias: 'payload-limit',
|
|
98
|
+
type: 'string',
|
|
99
|
+
describe: 'koa-bodyparser JSON body limit'
|
|
100
|
+
})
|
|
101
|
+
.option('disable_vite', {
|
|
102
|
+
alias: 'disable-vite',
|
|
103
|
+
type: 'boolean',
|
|
104
|
+
describe: 'Skip vite build in production; enables built-assets / SPA pipeline when mode is production'
|
|
105
|
+
})
|
|
106
|
+
.parseSync();
|
|
27
107
|
// Set default session duration (24 hours in milliseconds)
|
|
28
108
|
const DEFAULT_SESSION_DURATION = 86400000; // 24 hours in milliseconds
|
|
29
109
|
// Get session duration from environment variable or command line argument
|
|
@@ -34,13 +114,13 @@ if (isNaN(argv.session_duration) || argv.session_duration < 0) {
|
|
|
34
114
|
argv.session_duration = DEFAULT_SESSION_DURATION;
|
|
35
115
|
}
|
|
36
116
|
if (!argv.app_home) {
|
|
37
|
-
argv.app_home = '.';
|
|
117
|
+
argv.app_home = process.env.FLIGHT_APP_HOME || '.';
|
|
38
118
|
}
|
|
39
119
|
if (!argv.app_key) {
|
|
40
|
-
argv.app_key = 'flightApp';
|
|
120
|
+
argv.app_key = process.env.FLIGHT_APP_KEY || 'flightApp';
|
|
41
121
|
}
|
|
42
122
|
if (!argv.app_secret) {
|
|
43
|
-
argv.app_secret = 'the best secret key in the world';
|
|
123
|
+
argv.app_secret = process.env.FLIGHT_APP_SECRET || 'the best secret key in the world';
|
|
44
124
|
}
|
|
45
125
|
// Set default port values
|
|
46
126
|
if (!argv.port) {
|
|
@@ -59,14 +139,22 @@ if (isNaN(argv.port) || argv.port < 1 || argv.port > 65535) {
|
|
|
59
139
|
}
|
|
60
140
|
// Set default value for disable_vite flag
|
|
61
141
|
if (argv.disable_vite === undefined) {
|
|
62
|
-
|
|
63
|
-
argv.disable_vite =
|
|
142
|
+
const dv = process.env.FLIGHT_DISABLE_VITE;
|
|
143
|
+
argv.disable_vite = dv === 'true' || dv === '1' || dv === 'yes';
|
|
64
144
|
}
|
|
65
145
|
// Ensure the value is a boolean
|
|
66
146
|
argv.disable_vite = Boolean(argv.disable_vite);
|
|
67
147
|
const appHomePath = path_1.default.resolve(argv.app_home);
|
|
148
|
+
const excludePathsConfigured = dedupeStrings([
|
|
149
|
+
...normalizeExcludePaths(argv.exclude_paths),
|
|
150
|
+
...normalizeExcludePaths(process.env.FLIGHT_EXCLUDE_PATHS)
|
|
151
|
+
]);
|
|
152
|
+
const backendDiscoveryIgnores = backendDiscoveryIgnorePatterns(appHomePath, excludePathsConfigured);
|
|
68
153
|
process.chdir(appHomePath);
|
|
69
154
|
console.log(appHomePath);
|
|
155
|
+
if (backendDiscoveryIgnores.length > 0) {
|
|
156
|
+
console.log('Flight: excluding **/*.backend.ts discovery under:', backendDiscoveryIgnores.map((g) => g.replace(/\*\*$/, '')).join(', '));
|
|
157
|
+
}
|
|
70
158
|
const mode = process.env.FLIGHT_MODE || argv.mode || 'production';
|
|
71
159
|
/** Prefix without breaking printf-style logs (koa-logger uses `%s` as the first argument). */
|
|
72
160
|
const flightModeLabel = `Flight (${mode}):`;
|
|
@@ -99,6 +187,7 @@ if (cluster_1.default.isPrimary) {
|
|
|
99
187
|
}
|
|
100
188
|
else {
|
|
101
189
|
const app = new koa_1.default();
|
|
190
|
+
(0, spa_pipeline_js_1.applyTrustProxy)(app);
|
|
102
191
|
app.use((0, koa_logger_1.default)());
|
|
103
192
|
app.keys = argv.app_secret.split(',');
|
|
104
193
|
const SESSION_CONFIG = {
|
|
@@ -115,7 +204,9 @@ else {
|
|
|
115
204
|
app.use((0, cors_1.default)()).use((0, koa_bodyparser_1.default)({
|
|
116
205
|
jsonLimit: argv.payload_limit
|
|
117
206
|
}));
|
|
118
|
-
const backEndFiles = fast_glob_1.default.sync('**/*.backend.ts'
|
|
207
|
+
const backEndFiles = fast_glob_1.default.sync('**/*.backend.ts', {
|
|
208
|
+
ignore: backendDiscoveryIgnores
|
|
209
|
+
});
|
|
119
210
|
backEndFiles.forEach((file) => {
|
|
120
211
|
const serverRoutes = require(path_1.default.resolve(file));
|
|
121
212
|
console.log('Found component backend file: ' + path_1.default.resolve(file));
|
|
@@ -136,8 +227,15 @@ else {
|
|
|
136
227
|
console.error(`stderr: ${stderr}`);
|
|
137
228
|
});
|
|
138
229
|
}
|
|
139
|
-
|
|
140
|
-
|
|
230
|
+
const distRoot = (0, spa_pipeline_js_1.resolveDistRoot)(process.cwd());
|
|
231
|
+
const useSpaPipeline = (0, spa_pipeline_js_1.productionSpaPipelineActive)(mode, Boolean(argv.disable_vite));
|
|
232
|
+
const staticPrefixes = (0, spa_pipeline_js_1.parseCommaPrefixes)(process.env.FLIGHT_STATIC_PREFIXES, '/assets,/fonts');
|
|
233
|
+
const rateLimitSkipPrefixes = dedupeStrings([
|
|
234
|
+
...staticPrefixes,
|
|
235
|
+
...(0, spa_pipeline_js_1.parseCommaPrefixes)(process.env.FLIGHT_RATE_LIMIT_EXCLUDE_PREFIXES, '')
|
|
236
|
+
]);
|
|
237
|
+
const spaDenyExtra = (0, spa_pipeline_js_1.parseCommaPrefixes)(process.env.FLIGHT_SPA_DENY_PREFIXES, '');
|
|
238
|
+
const productionRatelimit = (0, koa_ratelimit_1.default)({
|
|
141
239
|
driver: 'redis',
|
|
142
240
|
db: redis,
|
|
143
241
|
duration: 60000,
|
|
@@ -150,12 +248,27 @@ else {
|
|
|
150
248
|
},
|
|
151
249
|
max: 1200,
|
|
152
250
|
disableHeader: false
|
|
153
|
-
})
|
|
154
|
-
|
|
251
|
+
});
|
|
252
|
+
const productionKoaCash = (0, koa_cash_1.default)({
|
|
155
253
|
get: (key) => redis.get(key),
|
|
156
254
|
set: (key, value) => redis.set(key, value, 'EX', 30)
|
|
157
|
-
})
|
|
158
|
-
|
|
255
|
+
});
|
|
256
|
+
if (useSpaPipeline) {
|
|
257
|
+
console.log('Flight: production SPA pipeline (static + index.html fallback before compress / rate limit); opt out with FLIGHT_DISABLE_SPA_PIPELINE=1');
|
|
258
|
+
app.use((0, koa_static_1.default)(distRoot));
|
|
259
|
+
app.use((0, spa_pipeline_js_1.spaIndexHtmlFallback)(distRoot, (0, spa_pipeline_js_1.spaIndexRelative)(), spaDenyExtra));
|
|
260
|
+
app.use((0, koa_compress_1.default)());
|
|
261
|
+
app.use((0, spa_pipeline_js_1.ratelimitWithPrefixSkips)(redis, rateLimitSkipPrefixes));
|
|
262
|
+
if ((0, spa_pipeline_js_1.httpCacheEnabledInSpaPipeline)()) {
|
|
263
|
+
app.use(productionKoaCash);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
app.use((0, koa_compress_1.default)());
|
|
268
|
+
app.use(productionRatelimit);
|
|
269
|
+
app.use(productionKoaCash);
|
|
270
|
+
app.use((0, koa_static_1.default)(distRoot));
|
|
271
|
+
}
|
|
159
272
|
if (!argv.disable_vite) {
|
|
160
273
|
console.log(`App served out of dist/ and available on port ${argv.port}`);
|
|
161
274
|
}
|