@thoughtpivot/flight 1.0.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.
@@ -0,0 +1,83 @@
1
+ filter-run-always: &filter-run-always
2
+ filters:
3
+ tags:
4
+ only: /.*/
5
+
6
+ filter-run-on-master-and-version-tag-only: &filter-run-on-master-and-version-tag-only
7
+ filters:
8
+ tags:
9
+ only: /^v.*/
10
+ branches:
11
+ only: main
12
+
13
+ aliases:
14
+ - &step-checkout checkout
15
+ - &step-restore-cache
16
+ restore_cache:
17
+ keys:
18
+ - v1-dependencies-{{ checksum "package.json" }}
19
+ - v1-dependencies-
20
+ - &step-install
21
+ run: npm install
22
+ - &step-save-cache
23
+ save_cache:
24
+ paths:
25
+ - node_modules
26
+ key: v1-dependencies-{{ checksum "package.json" }}
27
+ - &step-build
28
+ run: npm run build
29
+
30
+ build-node-common: &common-build
31
+ working_directory: ~/repo
32
+ steps:
33
+ - *step-checkout
34
+ - *step-restore-cache
35
+ - *step-install
36
+ - *step-save-cache
37
+ - *step-build
38
+ - run:
39
+ name: Testing
40
+ command: npm run test
41
+
42
+ version: 2
43
+ jobs:
44
+ deploy:
45
+ working_directory: ~/repo
46
+ docker:
47
+ - image: cimg/node:20.12
48
+ steps:
49
+ - *step-checkout
50
+ - *step-restore-cache
51
+ - *step-install
52
+ - *step-save-cache
53
+ - *step-build
54
+ - run:
55
+ name: Authenticate with registry
56
+ command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
57
+ - run:
58
+ name: Publish Package
59
+ command: npm publish
60
+
61
+ build-node18:
62
+ <<: *common-build
63
+ docker:
64
+ - image: cimg/node:18.20
65
+
66
+ build-node20:
67
+ <<: *common-build
68
+ docker:
69
+ - image: cimg/node:20.11
70
+
71
+ workflows:
72
+ version: 2
73
+ build_all:
74
+ jobs:
75
+ - build-node18:
76
+ <<: *filter-run-always
77
+ - build-node20:
78
+ <<: *filter-run-always
79
+ - deploy:
80
+ requires:
81
+ - build-node18
82
+ - build-node20
83
+ <<: *filter-run-on-master-and-version-tag-only
package/.editorconfig ADDED
@@ -0,0 +1,5 @@
1
+ [*.{js,jsx,ts,tsx,cs, json, md, yml, yaml, html, css, scss, less, graphql, mdx, vue, svelte, php, py, rb, rs, toml, go, java, sh, fish, bash, zsh, ksh, sql, tex, r, rmd, dart, xml, cfml, cfc, cfm, tcl, liquid, lisp, el, vue, svelte, swift, scala, groovy, kt, kts, clj, cljs, edn, hs, lhs, lua, moon, ml, mli, mll, mly, pascal, pas, pl, pm, t, pod, php, php3, php4, php5, php6, php7, php8, phtml, ps1, psm1, py, py3, pyi, pyx, r, rkt, rpy, rpy2, rb, rs, rli}]
2
+ indent_style = space
3
+ indent_size = 4
4
+ trim_trailing_whitespace = true
5
+ insert_final_newline = true
@@ -0,0 +1,11 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: 'npm'
9
+ directory: '/' # Location of package manifests
10
+ schedule:
11
+ interval: 'weekly'
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 20.11
package/.prettierrc ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "arrowParens": "always",
3
+ "endOfLine": "auto",
4
+ "printWidth": 120,
5
+ "semi": false,
6
+ "singleAttributePerLine": false,
7
+ "singleQuote": true,
8
+ "tabWidth": 4,
9
+ "trailingComma": "none",
10
+ "useTabs": false
11
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["esbenp.prettier-vscode"]
3
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ // exclude .vscode, .github and node_modules from the explorer
3
+ "files.exclude": {
4
+ "**/.vscode": true,
5
+ "**/.github": true,
6
+ "**/node_modules": true,
7
+ "**/dist": true,
8
+ ".circleci": true
9
+ }
10
+ }
package/LICENSE.md ADDED
@@ -0,0 +1,19 @@
1
+ # MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # Flight
2
+
3
+ Flight is a modern, high-performance web application server built on Node.js, designed for building scalable, component-driven applications. It represents the evolution of the [Avian](https://github.com/ispyhumanfly/avian) component application server, incorporating modern best practices and improved developer experience.
4
+
5
+ ## Overview
6
+
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.
8
+
9
+ ### Key Features
10
+
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
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @thoughtpivot/flight
26
+ # or
27
+ yarn add @thoughtpivot/flight
28
+ ```
29
+
30
+ Previously published on npm as `@spytech/flight`; switch your dependency to `@thoughtpivot/flight`.
31
+
32
+ ## Quick Start
33
+
34
+ 1. Create a new project directory and initialize:
35
+
36
+ ```bash
37
+ mkdir my-flight-app
38
+ cd my-flight-app
39
+ npm init -y
40
+ ```
41
+
42
+ 2. Install Flight and its dependencies:
43
+
44
+ ```bash
45
+ npm install @thoughtpivot/flight ioredis
46
+ ```
47
+
48
+ 3. Ensure Redis is running locally or set environment variables:
49
+
50
+ ```bash
51
+ # Default values shown below
52
+ export FLIGHT_REDIS_HOST=localhost
53
+ export FLIGHT_REDIS_PORT=6379
54
+ ```
55
+
56
+ 4. Create a component with a backend route:
57
+
58
+ ```bash
59
+ mkdir -p components/hello
60
+ ```
61
+
62
+ Create `components/hello/hello.backend.ts`:
63
+
64
+ ```typescript
65
+ import Router from '@koa/router'
66
+
67
+ const router = new Router()
68
+
69
+ router.get('/hello', async (ctx) => {
70
+ ctx.body = { message: 'Hello from Flight!' }
71
+ })
72
+
73
+ export default router.routes()
74
+ ```
75
+
76
+ 5. Start the server:
77
+
78
+ Development mode:
79
+
80
+ ```bash
81
+ node flight.js --mode development --app_home .
82
+ # Starts development server on port 3001 with HMR
83
+ # Backend API available on port 3000
84
+ ```
85
+
86
+ Production mode:
87
+
88
+ ```bash
89
+ node flight.js --mode production --app_home .
90
+ # Builds and serves application on port 3000
91
+ ```
92
+
93
+ Available CLI options:
94
+
95
+ - `--app_home`: Application root directory (default: current directory)
96
+ - `--app_key`: Application key for sessions (default: 'flightApp')
97
+ - `--app_secret`: Secret key for session encryption (default: 'the best secret key in the world')
98
+ - `--mode`: 'development' or 'production' (default: 'production')
99
+
100
+ ## Project Structure
101
+
102
+ ```
103
+ my-app/
104
+ ├── components/ # Application components
105
+ │ ├── Hello/ # Component directory
106
+ │ │ ├── Hello.vue # Vue component view
107
+ │ │ └── Hello.backend.ts # Backend routes and logic
108
+ ├── assets/ # Static assets
109
+ ├── dist/ # Production build output
110
+ └── package.json
111
+ ```
112
+
113
+ Each component follows a simple structure:
114
+
115
+ - `Index.vue`: Contains the Vue component template, script, and styles
116
+ - `Index.backend.ts`: Contains the backend routes and logic for the component
117
+
118
+ Example component files:
119
+
120
+ `components/hello/Index.vue`:
121
+
122
+ ```vue
123
+ <template>
124
+ <div>
125
+ <h1>{{ message }}</h1>
126
+ </div>
127
+ </template>
128
+
129
+ <script setup lang="ts">
130
+ import { ref } from 'vue'
131
+
132
+ const message = ref('Hello from Flight!')
133
+ </script>
134
+ ```
135
+
136
+ `components/hello/Index.backend.ts`:
137
+
138
+ ```typescript
139
+ import Router from '@koa/router'
140
+
141
+ const router = new Router()
142
+
143
+ router.get('/hello', async (ctx) => {
144
+ ctx.body = { message: 'Hello from Flight!' }
145
+ })
146
+
147
+ export default router.routes()
148
+ ```
149
+
150
+ ## Configuration
151
+
152
+ Flight can be configured through environment variables or command-line arguments:
153
+
154
+ ```bash
155
+ FLIGHT_MODE=development
156
+ FLIGHT_REDIS_HOST=localhost
157
+ FLIGHT_REDIS_PORT=6379
158
+ FLIGHT_MAX_WORKERS=4
159
+ ```
160
+
161
+ ## Development Mode
162
+
163
+ In development mode, Flight provides:
164
+
165
+ - Hot Module Replacement (HMR)
166
+ - Fast refresh for React components
167
+ - Detailed error messages
168
+ - Development server on port 3001
169
+
170
+ ## Production Mode
171
+
172
+ Production mode includes:
173
+
174
+ - Optimized builds
175
+ - Rate limiting
176
+ - Response compression
177
+ - Redis caching
178
+ - Cluster mode for load balancing
179
+ - Production server on port 3000
180
+
181
+ ## Comparison with Avian
182
+
183
+ 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:
184
+
185
+ ### Framework Evolution
186
+
187
+ - **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
188
+ - **Vite Instead of Webpack**: Replaced Webpack bundling with Vite for significantly faster development experience and simpler configuration
189
+ - **TypeScript First**: While Avian supported TypeScript, Flight is built with TypeScript from the ground up
190
+
191
+ ### Architectural Improvements
192
+
193
+ 1. **Simplified Component Structure**
194
+ - Avian: Complex component hierarchy with multiple file types (.client, .server, .view, .config)
195
+ - Flight: Streamlined with `.backend.ts` files and modern frontend frameworks
196
+ 2. **Development Experience**
197
+ - Avian: Webpack-based bundling with slower rebuild times
198
+ - Flight: Vite-powered development with instant HMR and no bundle step in development
199
+
200
+ 3. **Session Management**
201
+ - Avian: Express-session with Redis store
202
+ - Flight: Koa-session with Redis store, improved security defaults
203
+
204
+ 4. **Performance Features**
205
+ - Built-in rate limiting
206
+ - Redis-based caching
207
+ - Automatic compression in production
208
+ - Cluster mode for CPU utilization
209
+
210
+ 5. **Configuration**
211
+ - Avian: Complex webpack configuration and multiple build modes
212
+ - Flight: Simplified configuration with sensible defaults and Vite's zero-config approach
213
+
214
+ ### What's Different
215
+
216
+ 1. **Removed Features**
217
+ - Removed Webpack-specific configurations
218
+ - Removed legacy view engine support (EJS, Twig, Pug)
219
+ - Removed Sentry integration (can be added as middleware if needed)
220
+ - Removed built-in cron job scheduler (better handled by dedicated services)
221
+
222
+ 2. **New Features**
223
+ - Native ESM support
224
+ - Built-in CORS support
225
+ - Improved Redis integration
226
+ - Better security defaults
227
+ - Simpler API for backend routes
228
+ - Modern frontend tooling support
229
+
230
+ 3. **Simplified Architecture**
231
+ - Reduced configuration complexity
232
+ - More intuitive component organization
233
+ - Better separation of concerns
234
+ - Modern middleware approach
235
+
236
+ ### Migration from Avian
237
+
238
+ If you're migrating from Avian, the main changes you'll need to make are:
239
+
240
+ 1. Update your component structure to use `.backend.ts` files
241
+ 2. Move from Express middleware to Koa middleware
242
+ 3. Update your frontend bundling to use Vite
243
+ 4. Adapt to the new session management system
244
+ 5. Update your static file serving configuration
245
+
246
+ ## Requirements
247
+
248
+ - Node.js 16.x or higher
249
+ - Redis server
250
+ - TypeScript 4.x or higher
251
+
252
+ ## License
253
+
254
+ MIT
255
+
256
+ ## Contributing
257
+
258
+ Contributions are welcome! Please feel free to submit a Pull Request.
259
+
260
+ ## Acknowledgments
261
+
262
+ Flight is built upon the solid foundation laid by the [Avian](https://github.com/ispyhumanfly/avian) framework, created by FlyPaper Technologies, LLC. We're grateful for their pioneering work in component-driven architecture.
263
+
264
+ ## ThoughtPivot
265
+
266
+ Flight is a [ThoughtPivot](https://github.com/thoughtpivot) technology: developed and maintained as part of the ThoughtPivot organization and ecosystem.
package/dist/flight.js ADDED
@@ -0,0 +1,187 @@
1
+ #!ts-node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const child_process_1 = require("child_process");
8
+ const koa_1 = __importDefault(require("koa"));
9
+ const ioredis_1 = __importDefault(require("ioredis"));
10
+ const koa_redis_1 = __importDefault(require("koa-redis"));
11
+ const router_1 = __importDefault(require("@koa/router"));
12
+ const koa_bodyparser_1 = __importDefault(require("koa-bodyparser"));
13
+ const cluster_1 = __importDefault(require("cluster"));
14
+ const koa_compress_1 = __importDefault(require("koa-compress"));
15
+ const cors_1 = __importDefault(require("@koa/cors"));
16
+ const fast_glob_1 = __importDefault(require("fast-glob"));
17
+ const koa_cash_1 = __importDefault(require("koa-cash"));
18
+ const koa_logger_1 = __importDefault(require("koa-logger"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const koa_ratelimit_1 = __importDefault(require("koa-ratelimit"));
22
+ const koa_static_1 = __importDefault(require("koa-static"));
23
+ const koa_session_1 = __importDefault(require("koa-session"));
24
+ const argv = require('yargs/yargs')(process.argv.slice(2)).argv;
25
+ // Set default session duration (24 hours in milliseconds)
26
+ const DEFAULT_SESSION_DURATION = 86400000; // 24 hours in milliseconds
27
+ // Get session duration from environment variable or command line argument
28
+ argv.session_duration = Number(process.env.FLIGHT_SESSION_DURATION_MS) || argv.session_duration;
29
+ // Validate session duration
30
+ if (isNaN(argv.session_duration) || argv.session_duration < 0) {
31
+ console.error('Invalid session duration specified. Using default of 24 hours (86400000ms).');
32
+ argv.session_duration = DEFAULT_SESSION_DURATION;
33
+ }
34
+ if (!argv.app_home) {
35
+ argv.app_home = '.';
36
+ }
37
+ if (!argv.app_key) {
38
+ argv.app_key = 'flightApp';
39
+ }
40
+ if (!argv.app_secret) {
41
+ argv.app_secret = 'the best secret key in the world';
42
+ }
43
+ // Set default port values
44
+ if (!argv.port) {
45
+ // Check for environment variable first, then use default
46
+ argv.port = process.env.FLIGHT_PORT || 3000;
47
+ }
48
+ if (!argv.payload_limit) {
49
+ argv.payload_limit = process.env.FLIGHT_PAYLOAD_LIMIT || '1mb';
50
+ }
51
+ // Convert port to number
52
+ argv.port = Number(argv.port);
53
+ // Validate port is a valid number
54
+ if (isNaN(argv.port) || argv.port < 1 || argv.port > 65535) {
55
+ console.error('Invalid port specified. Using default port 3000.');
56
+ argv.port = 3000;
57
+ }
58
+ // Set default value for disable_vite flag
59
+ if (argv.disable_vite === undefined) {
60
+ // Check for environment variable first, then default to false
61
+ argv.disable_vite = process.env.FLIGHT_DISABLE_VITE === 'true' ? true : false;
62
+ }
63
+ // Ensure the value is a boolean
64
+ argv.disable_vite = Boolean(argv.disable_vite);
65
+ const appHomePath = path_1.default.resolve(argv.app_home);
66
+ process.chdir(appHomePath);
67
+ console.log(appHomePath);
68
+ const mode = process.env.FLIGHT_MODE || argv.mode || 'production';
69
+ /** Prefix without breaking printf-style logs (koa-logger uses `%s` as the first argument). */
70
+ const flightModeLabel = `Flight (${mode}):`;
71
+ const origConsoleLog = console.log.bind(console);
72
+ console.log = (first, ...rest) => {
73
+ if (typeof first === 'string' && /%[sdjifoO%]/.test(first)) {
74
+ origConsoleLog(`${flightModeLabel} ${first}`, ...rest);
75
+ }
76
+ else if (first !== undefined) {
77
+ origConsoleLog(flightModeLabel, first, ...rest);
78
+ }
79
+ else {
80
+ origConsoleLog(flightModeLabel);
81
+ }
82
+ };
83
+ const redis = new ioredis_1.default({
84
+ host: process.env.FLIGHT_REDIS_HOST || 'localhost',
85
+ port: Number(process.env.FLIGHT_REDIS_PORT) || 6379
86
+ });
87
+ if (cluster_1.default.isPrimary) {
88
+ const numCPUs = os_1.default.cpus().length;
89
+ const maxWorkers = Number(process.env.FLIGHT_MAX_WORKERS) || numCPUs;
90
+ const workersCount = Math.min(maxWorkers, numCPUs);
91
+ for (let i = 0; i < workersCount; i++) {
92
+ cluster_1.default.fork();
93
+ }
94
+ cluster_1.default.on('exit', () => {
95
+ cluster_1.default.fork();
96
+ });
97
+ }
98
+ else {
99
+ const app = new koa_1.default();
100
+ app.use((0, koa_logger_1.default)());
101
+ app.keys = argv.app_secret.split(',');
102
+ const SESSION_CONFIG = {
103
+ key: argv.app_key,
104
+ maxAge: argv.session_duration,
105
+ sameSite: true,
106
+ path: '/',
107
+ store: (0, koa_redis_1.default)({
108
+ client: redis
109
+ })
110
+ };
111
+ app.use((0, koa_session_1.default)(SESSION_CONFIG, app));
112
+ const router = new router_1.default();
113
+ app.use((0, cors_1.default)()).use((0, koa_bodyparser_1.default)({
114
+ jsonLimit: argv.payload_limit
115
+ }));
116
+ const backEndFiles = fast_glob_1.default.sync('**/*.backend.ts');
117
+ backEndFiles.forEach((file) => {
118
+ const serverRoutes = require(path_1.default.resolve(file));
119
+ console.log('Found component backend file: ' + path_1.default.resolve(file));
120
+ if (serverRoutes && serverRoutes.default) {
121
+ router.use(serverRoutes.default);
122
+ }
123
+ });
124
+ app.use(router.routes()).use(router.allowedMethods());
125
+ if (mode === 'production') {
126
+ console.log('Starting flight in production mode');
127
+ if (!argv.disable_vite) {
128
+ (0, child_process_1.exec)('npx vite build', (error, stdout, stderr) => {
129
+ if (error) {
130
+ console.error(`exec error: ${error}`);
131
+ return;
132
+ }
133
+ console.log(`stdout: ${stdout}`);
134
+ console.error(`stderr: ${stderr}`);
135
+ });
136
+ }
137
+ app.use((0, koa_compress_1.default)());
138
+ app.use((0, koa_ratelimit_1.default)({
139
+ driver: 'redis',
140
+ db: redis,
141
+ duration: 60000,
142
+ errorMessage: 'Sometimes You Just Have to Slow Down.',
143
+ id: (ctx) => ctx.get('x-forwarded-for') || ctx.ip,
144
+ headers: {
145
+ remaining: 'Rate-Limit-Remaining',
146
+ reset: 'Rate-Limit-Reset',
147
+ total: 'Rate-Limit-Total'
148
+ },
149
+ max: 1200,
150
+ disableHeader: false
151
+ }));
152
+ app.use((0, koa_cash_1.default)({
153
+ get: (key) => redis.get(key),
154
+ set: (key, value) => redis.set(key, value, 'EX', 30)
155
+ }));
156
+ app.use((0, koa_static_1.default)(process.env.FLIGHT_DIST_PATH || '../dist'));
157
+ if (!argv.disable_vite) {
158
+ console.log(`App served out of dist/ and available on port ${argv.port}`);
159
+ }
160
+ else {
161
+ console.log(`App served out of ${appHomePath} and available on port ${argv.port}`);
162
+ }
163
+ }
164
+ app.listen(argv.port, () => {
165
+ console.log(`Server worker ${process.pid} started, All backend services are running on port ${argv.port}`);
166
+ });
167
+ if (mode === 'development') {
168
+ console.log('Starting flight in development mode');
169
+ const viteProcess = (0, child_process_1.spawn)('npx', ['vite', '--port', '3001', '--host', '0.0.0.0'], {
170
+ stdio: 'inherit',
171
+ shell: true
172
+ });
173
+ viteProcess.on('error', (error) => {
174
+ console.error('Failed to start vite server:', error);
175
+ });
176
+ viteProcess.on('exit', (code) => {
177
+ if (code !== 0) {
178
+ console.error(`Vite server exited with code ${code}`);
179
+ }
180
+ });
181
+ process.on('SIGINT', () => {
182
+ viteProcess.kill('SIGINT');
183
+ process.exit(0);
184
+ });
185
+ console.log(`Vite development server with hot module reload ${process.pid} started on 3001`);
186
+ }
187
+ }
@@ -0,0 +1,32 @@
1
+ import jsdoc from 'eslint-plugin-jsdoc'
2
+ import tsParser from '@typescript-eslint/parser'
3
+
4
+ export default [
5
+ {
6
+ files: ['**/*.js'],
7
+ plugins: {
8
+ jsdoc: jsdoc
9
+ },
10
+ rules: {
11
+ 'jsdoc/require-description': 'error',
12
+ 'jsdoc/check-values': 'error'
13
+ }
14
+ },
15
+ {
16
+ files: ['**/*.ts'],
17
+ languageOptions: {
18
+ parser: tsParser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ sourceType: 'module'
22
+ }
23
+ },
24
+ plugins: {
25
+ jsdoc: jsdoc
26
+ },
27
+ rules: {
28
+ 'jsdoc/require-description': 'error',
29
+ 'jsdoc/check-values': 'error'
30
+ }
31
+ }
32
+ ]
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@thoughtpivot/flight",
3
+ "version": "1.0.7",
4
+ "description": "A web server designed for running full stack, component based applications at enterprise scale.",
5
+ "main": "src/flight.ts",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "flight": "dist/flight.js"
11
+ },
12
+ "scripts": {
13
+ "lint": "eslint . --fix && prettier -w .",
14
+ "build": "rm -rf dist; node_modules/.bin/tsc --project .",
15
+ "test": "echo \"Error: no test specified\" && exit 0"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/thoughtpivot/flight.git"
20
+ },
21
+ "author": "",
22
+ "license": "MIT",
23
+ "bugs": {
24
+ "url": "https://github.com/thoughtpivot/flight/issues"
25
+ },
26
+ "homepage": "https://github.com/thoughtpivot/flight#readme",
27
+ "dependencies": {
28
+ "@koa/cors": "^5.0.0",
29
+ "@koa/router": "^13.1.0",
30
+ "@vitejs/plugin-vue": "^5.2.1",
31
+ "@vue/language-plugin-pug": "^2.2.0",
32
+ "core-js": "^3.40.0",
33
+ "dotenv": "^17.4.2",
34
+ "esbuild": "^0.28.0",
35
+ "fast-glob": "^3.3.3",
36
+ "ioredis": "^5.4.2",
37
+ "koa": "^2.15.3",
38
+ "koa-bodyparser": "^4.4.1",
39
+ "koa-cash": "^5.0.0",
40
+ "koa-compress": "^5.1.1",
41
+ "koa-connect-history-api-fallback": "^0.3.1",
42
+ "koa-helmet": "^8.0.1",
43
+ "koa-logger": "^3.2.1",
44
+ "koa-ratelimit": "^5.1.0",
45
+ "koa-redis": "^4.0.1",
46
+ "koa-send": "^5.0.1",
47
+ "koa-session": "^6.4.0",
48
+ "koa-static": "^5.0.0",
49
+ "unplugin-fonts": "^2.0.0",
50
+ "vite": "^6.1.7",
51
+ "vite-plugin-vuetify": "^2.0.4",
52
+ "yargs": "^18.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/koa": "^2.15.0",
56
+ "@types/koa__cors": "^5.0.0",
57
+ "@types/koa__router": "^12.0.4",
58
+ "@types/koa-bodyparser": "^4.3.12",
59
+ "@types/koa-compress": "^4.0.6",
60
+ "@types/koa-logger": "^3.1.5",
61
+ "@types/koa-ratelimit": "^5.0.5",
62
+ "@types/koa-redis": "^4.0.5",
63
+ "@types/koa-redis-cache": "^3.0.4",
64
+ "@types/koa-session": "^6.4.5",
65
+ "@types/koa-static": "^4.0.4",
66
+ "@types/node": "^22.19.17",
67
+ "@typescript-eslint/parser": "^8.59.1",
68
+ "eslint": "^9.39.4",
69
+ "eslint-plugin-jsdoc": "^50.8.0",
70
+ "eslint-plugin-vue": "^9.33.0",
71
+ "prettier": "^3.8.3",
72
+ "ts-node": "^10.9.2",
73
+ "typescript": "^5.9.3"
74
+ }
75
+ }
package/src/flight.ts ADDED
@@ -0,0 +1,226 @@
1
+ #!ts-node
2
+
3
+ import { exec, spawn } from 'child_process'
4
+
5
+ import Koa from 'koa'
6
+ import Redis from 'ioredis'
7
+ import RedisStore from 'koa-redis'
8
+ import Router from '@koa/router'
9
+ import bodyParser from 'koa-bodyparser'
10
+ import cluster from 'cluster'
11
+ import compress from 'koa-compress'
12
+ import cors from '@koa/cors'
13
+ import fg from 'fast-glob'
14
+ import koaCash from 'koa-cash'
15
+ import logger from 'koa-logger'
16
+ import os from 'os'
17
+ import path from 'path'
18
+ import ratelimit from 'koa-ratelimit'
19
+ import serve from 'koa-static'
20
+ import session from 'koa-session'
21
+
22
+ const argv = require('yargs/yargs')(process.argv.slice(2)).argv
23
+
24
+ // Set default session duration (24 hours in milliseconds)
25
+ const DEFAULT_SESSION_DURATION = 86400000 // 24 hours in milliseconds
26
+
27
+ // Get session duration from environment variable or command line argument
28
+ argv.session_duration = Number(process.env.FLIGHT_SESSION_DURATION_MS) || argv.session_duration
29
+
30
+ // Validate session duration
31
+ if (isNaN(argv.session_duration) || argv.session_duration < 0) {
32
+ console.error('Invalid session duration specified. Using default of 24 hours (86400000ms).')
33
+ argv.session_duration = DEFAULT_SESSION_DURATION
34
+ }
35
+
36
+ if (!argv.app_home) {
37
+ argv.app_home = '.'
38
+ }
39
+
40
+ if (!argv.app_key) {
41
+ argv.app_key = 'flightApp'
42
+ }
43
+
44
+ if (!argv.app_secret) {
45
+ argv.app_secret = 'the best secret key in the world'
46
+ }
47
+
48
+ // Set default port values
49
+ if (!argv.port) {
50
+ // Check for environment variable first, then use default
51
+ argv.port = process.env.FLIGHT_PORT || 3000
52
+ }
53
+
54
+ if (!argv.payload_limit) {
55
+ argv.payload_limit = process.env.FLIGHT_PAYLOAD_LIMIT || '1mb'
56
+ }
57
+
58
+ // Convert port to number
59
+ argv.port = Number(argv.port)
60
+
61
+ // Validate port is a valid number
62
+ if (isNaN(argv.port) || argv.port < 1 || argv.port > 65535) {
63
+ console.error('Invalid port specified. Using default port 3000.')
64
+ argv.port = 3000
65
+ }
66
+
67
+ // Set default value for disable_vite flag
68
+ if (argv.disable_vite === undefined) {
69
+ // Check for environment variable first, then default to false
70
+ argv.disable_vite = process.env.FLIGHT_DISABLE_VITE === 'true' ? true : false
71
+ }
72
+
73
+ // Ensure the value is a boolean
74
+ argv.disable_vite = Boolean(argv.disable_vite)
75
+
76
+ const appHomePath = path.resolve(argv.app_home)
77
+ process.chdir(appHomePath)
78
+
79
+ console.log(appHomePath)
80
+
81
+ const mode = process.env.FLIGHT_MODE || argv.mode || 'production'
82
+
83
+ /** Prefix without breaking printf-style logs (koa-logger uses `%s` as the first argument). */
84
+ const flightModeLabel = `Flight (${mode}):`
85
+ const origConsoleLog = console.log.bind(console)
86
+ console.log = (first?: unknown, ...rest: unknown[]): void => {
87
+ if (typeof first === 'string' && /%[sdjifoO%]/.test(first)) {
88
+ origConsoleLog(`${flightModeLabel} ${first}`, ...rest)
89
+ } else if (first !== undefined) {
90
+ origConsoleLog(flightModeLabel, first, ...rest)
91
+ } else {
92
+ origConsoleLog(flightModeLabel)
93
+ }
94
+ }
95
+
96
+ const redis = new Redis({
97
+ host: process.env.FLIGHT_REDIS_HOST || 'localhost',
98
+ port: Number(process.env.FLIGHT_REDIS_PORT) || 6379
99
+ })
100
+
101
+ if (cluster.isPrimary) {
102
+ const numCPUs = os.cpus().length
103
+ const maxWorkers = Number(process.env.FLIGHT_MAX_WORKERS) || numCPUs
104
+ const workersCount = Math.min(maxWorkers, numCPUs)
105
+
106
+ for (let i = 0; i < workersCount; i++) {
107
+ cluster.fork()
108
+ }
109
+
110
+ cluster.on('exit', () => {
111
+ cluster.fork()
112
+ })
113
+ } else {
114
+ const app = new Koa()
115
+ app.use(logger())
116
+
117
+ app.keys = argv.app_secret.split(',')
118
+
119
+ const SESSION_CONFIG = {
120
+ key: argv.app_key,
121
+ maxAge: argv.session_duration,
122
+ sameSite: true,
123
+ path: '/',
124
+ store: RedisStore({
125
+ client: redis
126
+ })
127
+ }
128
+
129
+ app.use(session(SESSION_CONFIG, app))
130
+
131
+ const router = new Router()
132
+
133
+ app.use(cors()).use(
134
+ bodyParser({
135
+ jsonLimit: argv.payload_limit
136
+ })
137
+ )
138
+
139
+ const backEndFiles = fg.sync('**/*.backend.ts')
140
+ backEndFiles.forEach((file) => {
141
+ const serverRoutes = require(path.resolve(file))
142
+
143
+ console.log('Found component backend file: ' + path.resolve(file))
144
+
145
+ if (serverRoutes && serverRoutes.default) {
146
+ router.use(serverRoutes.default)
147
+ }
148
+ })
149
+
150
+ app.use(router.routes()).use(router.allowedMethods())
151
+
152
+ if (mode === 'production') {
153
+ console.log('Starting flight in production mode')
154
+
155
+ if (!argv.disable_vite) {
156
+ exec('npx vite build', (error, stdout, stderr) => {
157
+ if (error) {
158
+ console.error(`exec error: ${error}`)
159
+ return
160
+ }
161
+ console.log(`stdout: ${stdout}`)
162
+ console.error(`stderr: ${stderr}`)
163
+ })
164
+ }
165
+
166
+ app.use(compress())
167
+ app.use(
168
+ ratelimit({
169
+ driver: 'redis',
170
+ db: redis,
171
+ duration: 60000,
172
+ errorMessage: 'Sometimes You Just Have to Slow Down.',
173
+ id: (ctx) => ctx.get('x-forwarded-for') || ctx.ip,
174
+ headers: {
175
+ remaining: 'Rate-Limit-Remaining',
176
+ reset: 'Rate-Limit-Reset',
177
+ total: 'Rate-Limit-Total'
178
+ },
179
+ max: 1200,
180
+ disableHeader: false
181
+ })
182
+ )
183
+ app.use(
184
+ koaCash({
185
+ get: (key) => redis.get(key),
186
+ set: (key, value) => redis.set(key, value, 'EX', 30)
187
+ })
188
+ )
189
+ app.use(serve(process.env.FLIGHT_DIST_PATH || '../dist'))
190
+
191
+ if (!argv.disable_vite) {
192
+ console.log(`App served out of dist/ and available on port ${argv.port}`)
193
+ } else {
194
+ console.log(`App served out of ${appHomePath} and available on port ${argv.port}`)
195
+ }
196
+ }
197
+
198
+ app.listen(argv.port, () => {
199
+ console.log(`Server worker ${process.pid} started, All backend services are running on port ${argv.port}`)
200
+ })
201
+
202
+ if (mode === 'development') {
203
+ console.log('Starting flight in development mode')
204
+ const viteProcess = spawn('npx', ['vite', '--port', '3001', '--host', '0.0.0.0'], {
205
+ stdio: 'inherit',
206
+ shell: true
207
+ })
208
+
209
+ viteProcess.on('error', (error) => {
210
+ console.error('Failed to start vite server:', error)
211
+ })
212
+
213
+ viteProcess.on('exit', (code) => {
214
+ if (code !== 0) {
215
+ console.error(`Vite server exited with code ${code}`)
216
+ }
217
+ })
218
+
219
+ process.on('SIGINT', () => {
220
+ viteProcess.kill('SIGINT')
221
+ process.exit(0)
222
+ })
223
+
224
+ console.log(`Vite development server with hot module reload ${process.pid} started on 3001`)
225
+ }
226
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "experimentalDecorators": true,
6
+ "emitDecoratorMetadata": true,
7
+ "moduleResolution": "NodeNext",
8
+ "outDir": "dist"
9
+ }
10
+ }