@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 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 modern, high-performance web application server built on Node.js, designed for building scalable, component-driven applications. It is the successor to [Avian](https://github.com/ispyhumanfly/avian): originally created by ThoughtPivot, handed to FlyPaper Technologies, LLC, where Avian matured—Flight builds on those advancements with modern tooling and developer experience.
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
- ## Overview
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 takes the best concepts from Avian and enhances them with modern tooling and practices. It provides a robust foundation for building enterprise-grade applications while maintaining simplicity and developer productivity.
11
+ Flight is **open source** from **[ThoughtPivot](https://github.com/thoughtpivot)**.
8
12
 
9
- ### Key Features
13
+ ## Highlights
10
14
 
11
- - **Modern Stack**: Built on Koa.js with TypeScript support
12
- - **Component-Based Architecture**: Organize your application into reusable components
13
- - **Built-in Development Server**: Powered by Vite for lightning-fast development
14
- - **Production-Ready**: Includes rate limiting, compression, and caching out of the box
15
- - **Cluster Mode**: Automatic load balancing across CPU cores
16
- - **Redis Integration**: Built-in support for session management and caching
17
- - **TypeScript First**: Native TypeScript support throughout the framework
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
- Previously published on npm as `@spytech/flight`; switch your dependency to `@thoughtpivot/flight`.
31
+ Legacy npm scope: the package was previously published as `@spytech/flight`—use `@thoughtpivot/flight` going forward.
31
32
 
32
- ### Downstream apps (tsx / patch-package)
33
+ ### Downstream apps (tsx)
33
34
 
34
- From **v1.0.8** onward, Flight normalizes `require('yargs/yargs')` when loaders such as **tsx** expose it as `{ default: factory }` instead of the factory. If you only added **patch-package** for that workaround, upgrade `@thoughtpivot/flight`, delete `patches/@thoughtpivot+flight+*.patch`, and remove any **postinstall** hook that existed solely to apply it.
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. Start the server:
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
- Development mode:
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
- node flight.js --mode development --app_home .
86
- # Starts development server on port 3001 with HMR
87
- # Backend API available on port 3000
100
+ npx flight --mode production
101
+ # or: npm run start
88
102
  ```
89
103
 
90
- Production mode:
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
- node flight.js --mode production --app_home .
94
- # Builds and serves application on port 3000
109
+ npx --yes --package @thoughtpivot/flight flight --mode development
95
110
  ```
96
111
 
97
- Available CLI options:
112
+ **Global install** (optional): `npm install -g @thoughtpivot/flight`, then run **`flight`** from your PATH the same way as **`npx flight`**.
98
113
 
99
- - `--app_home`: Application root directory (default: current directory)
100
- - `--app_key`: Application key for sessions (default: 'flightApp')
101
- - `--app_secret`: Secret key for session encryption (default: 'the best secret key in the world')
102
- - `--mode`: 'development' or 'production' (default: 'production')
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/ # Application components
109
- │ ├── Hello/ # Component directory
110
- │ │ ├── Hello.vue # Vue component view
111
- │ │ └── Hello.backend.ts # Backend routes and logic
112
- ├── assets/ # Static assets
113
- ├── dist/ # Production build output
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
- Each component follows a simple structure:
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
- ## Configuration
230
+ ## Development mode
155
231
 
156
- Flight can be configured through environment variables or command-line arguments:
232
+ - **Vue** + **Vite** with HMR on port **3001**
233
+ - Backend worker on **`--port`** (default **3000**)
234
+ - Request logging via **`koa-logger`**
157
235
 
158
- ```bash
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
- Production mode includes:
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
- - Optimized builds
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
- ## Comparison with Avian
245
+ **When it is on (option B):** `mode === 'production'` **and** **`disable_vite === true`** **and** **`FLIGHT_DISABLE_SPA_PIPELINE` is not truthy.**
186
246
 
187
- Flight is a modern reimagining of the Avian framework, making several architectural improvements while maintaining the core philosophy of component-driven applications. Here are the key differences:
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
- ### Framework Evolution
249
+ **Path ownership**
190
250
 
191
- - **Koa Instead of Express**: Flight uses Koa.js as its foundation instead of Express, providing better async/await support and a more modern middleware architecture
192
- - **Vite Instead of Webpack**: Replaced Webpack bundling with Vite for significantly faster development experience and simpler configuration
193
- - **TypeScript First**: While Avian supported TypeScript, Flight is built with TypeScript from the ground up
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
- ### Architectural Improvements
255
+ **Load balancers and `X-Forwarded-For`**
196
256
 
197
- 1. **Simplified Component Structure**
198
- - Avian: Complex component hierarchy with multiple file types (.client, .server, .view, .config)
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
- 3. **Session Management**
205
- - Avian: Express-session with Redis store
206
- - Flight: Koa-session with Redis store, improved security defaults
260
+ ### Middleware order (mermaid)
207
261
 
208
- 4. **Performance Features**
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
- 5. **Configuration**
215
- - Avian: Complex webpack configuration and multiple build modes
216
- - Flight: Simplified configuration with sensible defaults and Vite's zero-config approach
217
-
218
- ### What's Different
219
-
220
- 1. **Removed Features**
221
- - Removed Webpack-specific configurations
222
- - Removed legacy view engine support (EJS, Twig, Pug)
223
- - Removed Sentry integration (can be added as middleware if needed)
224
- - Removed built-in cron job scheduler (better handled by dedicated services)
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
- 3. **Simplified Architecture**
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
- ### Migration from Avian
278
+ ### Migrating from Flight 1.x
241
279
 
242
- If you're migrating from Avian, the main changes you'll need to make are:
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
- 1. Update your component structure to use `.backend.ts` files
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 server
254
- - TypeScript 4.x or higher
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
- Contributions are welcome! Please feel free to submit a Pull Request.
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 the [Avian](https://github.com/ispyhumanfly/avian) framework. Avian began at ThoughtPivot and was later handed to FlyPaper Technologies, LLC, where it grew into the platform many teams relied on. Flight is born from that lineage—the ideas and hardening FlyPaper contributed to Avian—carried forward as ThoughtPivot’s next-generation server.
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 an official ThoughtPivot technology: developed and maintained by the [ThoughtPivot](https://github.com/thoughtpivot) organization as part of its ecosystem.
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
- #!ts-node
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 yargsFactory = typeof _yargs === 'function' ? _yargs : _yargs.default;
26
- const argv = yargsFactory(process.argv.slice(2)).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
- // Check for environment variable first, then default to false
63
- argv.disable_vite = process.env.FLIGHT_DISABLE_VITE === 'true' ? true : false;
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
- app.use((0, koa_compress_1.default)());
140
- app.use((0, koa_ratelimit_1.default)({
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
- app.use((0, koa_cash_1.default)({
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
- app.use((0, koa_static_1.default)(process.env.FLIGHT_DIST_PATH || '../dist'));
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
  }