@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.
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.productionSpaPipelineActive = productionSpaPipelineActive;
7
+ exports.applyTrustProxy = applyTrustProxy;
8
+ exports.parseCommaPrefixes = parseCommaPrefixes;
9
+ exports.resolveDistRoot = resolveDistRoot;
10
+ exports.spaIndexRelative = spaIndexRelative;
11
+ exports.spaIndexHtmlFallback = spaIndexHtmlFallback;
12
+ exports.shouldSkipRateLimitForPath = shouldSkipRateLimitForPath;
13
+ exports.ratelimitWithPrefixSkips = ratelimitWithPrefixSkips;
14
+ exports.httpCacheEnabledInSpaPipeline = httpCacheEnabledInSpaPipeline;
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const koa_ratelimit_1 = __importDefault(require("koa-ratelimit"));
18
+ /** Option B: production + built assets (disable_vite), unless explicitly opted out. */
19
+ function productionSpaPipelineActive(mode, disableVite, env = process.env) {
20
+ if (mode !== 'production')
21
+ return false;
22
+ if (!disableVite)
23
+ return false;
24
+ const d = env.FLIGHT_DISABLE_SPA_PIPELINE;
25
+ if (d === '1' || d === 'true' || d === 'yes')
26
+ return false;
27
+ return true;
28
+ }
29
+ function applyTrustProxy(app, env = process.env) {
30
+ const t = env.FLIGHT_TRUST_PROXY;
31
+ app.proxy = t === '1' || t === 'true' || t === 'yes';
32
+ }
33
+ function parseCommaPrefixes(value, fallback) {
34
+ const raw = (value ?? fallback).trim();
35
+ if (!raw)
36
+ return [];
37
+ return raw
38
+ .split(',')
39
+ .map((s) => s.trim())
40
+ .filter(Boolean)
41
+ .map((p) => (p.startsWith('/') ? p : `/${p}`));
42
+ }
43
+ function resolveDistRoot(cwd, env = process.env) {
44
+ return path_1.default.resolve(cwd, env.FLIGHT_DIST_PATH || '../dist');
45
+ }
46
+ function spaIndexRelative(env = process.env) {
47
+ const v = (env.FLIGHT_SPA_INDEX || 'index.html').trim();
48
+ return v.replace(/^\/+/, '');
49
+ }
50
+ const DEFAULT_DENY_PREFIXES = ['/api', '/health'];
51
+ /** Match connect-history-api-fallback default: do not rewrite paths whose last segment looks like a file name. */
52
+ function lastPathSegmentLooksLikeFile(urlPath) {
53
+ const base = urlPath.slice(urlPath.lastIndexOf('/') + 1);
54
+ return base.includes('.');
55
+ }
56
+ /**
57
+ * After koa-static: serve index.html for document navigations that are not API, health, or static-like paths.
58
+ */
59
+ function spaIndexHtmlFallback(distRoot, indexRel, extraDenyPrefixes = []) {
60
+ const indexAbs = path_1.default.join(distRoot, indexRel);
61
+ const deny = dedupePathList([...DEFAULT_DENY_PREFIXES, ...extraDenyPrefixes]);
62
+ return async (ctx, next) => {
63
+ if (ctx.method !== 'GET' && ctx.method !== 'HEAD')
64
+ return next();
65
+ if (ctx.body != null)
66
+ return next();
67
+ const accept = ctx.get('accept') || '';
68
+ if (accept && !accept.includes('text/html') && !accept.includes('*/*'))
69
+ return next();
70
+ const urlPath = ctx.path.split('?')[0];
71
+ if (lastPathSegmentLooksLikeFile(urlPath))
72
+ return next();
73
+ for (const pre of deny) {
74
+ if (urlPath === pre || (pre !== '/' && urlPath.startsWith(`${pre}/`))) {
75
+ return next();
76
+ }
77
+ }
78
+ try {
79
+ await fs_1.default.promises.access(indexAbs, fs_1.default.constants.R_OK);
80
+ }
81
+ catch {
82
+ return next();
83
+ }
84
+ ctx.type = 'text/html; charset=utf-8';
85
+ ctx.body = fs_1.default.createReadStream(indexAbs);
86
+ };
87
+ }
88
+ function dedupePathList(paths) {
89
+ return [...new Set(paths)];
90
+ }
91
+ function shouldSkipRateLimitForPath(path, method, prefixes) {
92
+ if (method !== 'GET' && method !== 'HEAD')
93
+ return false;
94
+ return prefixes.some((pre) => path === pre || path.startsWith(`${pre}/`));
95
+ }
96
+ /** Same ratelimit as Flight legacy, but skips GET/HEAD under configured prefixes (e.g. hashed assets). */
97
+ function ratelimitWithPrefixSkips(redis, skipPrefixes) {
98
+ const inner = (0, koa_ratelimit_1.default)({
99
+ driver: 'redis',
100
+ db: redis,
101
+ duration: 60000,
102
+ errorMessage: 'Sometimes You Just Have to Slow Down.',
103
+ id: (ctx) => ctx.get('x-forwarded-for') || ctx.ip,
104
+ headers: {
105
+ remaining: 'Rate-Limit-Remaining',
106
+ reset: 'Rate-Limit-Reset',
107
+ total: 'Rate-Limit-Total'
108
+ },
109
+ max: 1200,
110
+ disableHeader: false
111
+ });
112
+ return async (ctx, next) => {
113
+ if (shouldSkipRateLimitForPath(ctx.path, ctx.method, skipPrefixes)) {
114
+ return next();
115
+ }
116
+ await inner(ctx, next);
117
+ };
118
+ }
119
+ function httpCacheEnabledInSpaPipeline(env = process.env) {
120
+ const v = env.FLIGHT_HTTP_CACHE;
121
+ return v === '1' || v === 'true' || v === 'yes';
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thoughtpivot/flight",
3
- "version": "1.0.8",
3
+ "version": "2.0.1",
4
4
  "description": "A web server designed for running full stack, component based applications at enterprise scale.",
5
5
  "main": "src/flight.ts",
6
6
  "publishConfig": {
@@ -12,7 +12,8 @@
12
12
  "scripts": {
13
13
  "lint": "eslint . --fix && prettier -w .",
14
14
  "build": "rm -rf dist; node_modules/.bin/tsc --project .",
15
- "test": "echo \"Error: no test specified\" && exit 0"
15
+ "test": "npm run build && node --test dist/spa-pipeline.test.js",
16
+ "prepublishOnly": "npm run build"
16
17
  },
17
18
  "repository": {
18
19
  "type": "git",
@@ -24,6 +25,13 @@
24
25
  "url": "https://github.com/thoughtpivot/flight/issues"
25
26
  },
26
27
  "homepage": "https://github.com/thoughtpivot/flight#readme",
28
+ "files": [
29
+ "dist/flight.js",
30
+ "dist/spa-pipeline.js",
31
+ "README.md",
32
+ "CHANGELOG.md",
33
+ "readme-assets"
34
+ ],
27
35
  "dependencies": {
28
36
  "@koa/cors": "^5.0.0",
29
37
  "@koa/router": "^13.1.0",
@@ -64,11 +72,13 @@
64
72
  "@types/koa-session": "^6.4.5",
65
73
  "@types/koa-static": "^4.0.4",
66
74
  "@types/node": "^22.19.17",
75
+ "@types/supertest": "^6.0.2",
67
76
  "@typescript-eslint/parser": "^8.59.1",
68
77
  "eslint": "^9.39.4",
69
78
  "eslint-plugin-jsdoc": "^50.8.0",
70
79
  "eslint-plugin-vue": "^9.33.0",
71
80
  "prettier": "^3.8.3",
81
+ "supertest": "^7.0.0",
72
82
  "ts-node": "^10.9.2",
73
83
  "typescript": "^5.9.3"
74
84
  }
@@ -0,0 +1,17 @@
1
+ <svg width="5343" height="865" viewBox="0 0 5343 865" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M642.666 865C533.606 865 431.066 865 322.006 865C393.594 596.025 462.176 327.554 533.606 58.4919C543.977 18.4241 561.039 0.0278033 599.015 0.0278033C743.037 0.0278033 887.058 0.0278033 1031.25 0.0278033C993.277 0.0278033 976.214 18.4241 965.844 58.4919C894.413 327.554 854.267 487.462 783.01 756.611C765.781 821.507 708.067 865 642.666 865Z" fill="black"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M0 187.175C9.20051 152.387 24.7565 93.4178 33.957 58.4641C44.3263 18.3964 61.3904 3.89425e-05 99.3628 3.89425e-05C243.386 3.89425e-05 387.581 3.89425e-05 531.602 3.89425e-05C493.791 3.89425e-05 476.737 18.3964 466.193 58.4641C456.494 95.2567 440.939 153.973 432.234 187.175H0Z" fill="black"/>
4
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M1426.35 206.948C1459.81 76.2171 1380.18 0.0276436 1243.85 0.0276436H1098.49C1060.69 0.0276436 1043.63 18.424 1033.08 58.4917C961.661 327.554 970.193 302.623 898.77 571.599C1007.83 571.599 1110.2 571.599 1219.26 571.599C1284.83 571.599 1342.37 528.114 1359.61 463.123C1410.8 270.337 1406.27 284.227 1425.85 208.7V208.613C1426.02 208.029 1426.19 207.445 1426.35 206.948Z" fill="black"/>
5
+ <path d="M1890.76 188.564C1893.08 188.564 1894.93 189.492 1896.32 191.346C1898.18 192.737 1899.1 194.592 1899.1 196.911V264.377C1899.1 266.696 1898.18 268.782 1896.32 270.637C1894.93 272.028 1893.08 272.723 1890.76 272.723H1803.81C1801.49 272.723 1800.33 273.883 1800.33 276.201V667.089C1800.33 669.408 1799.41 671.494 1797.55 673.349C1796.16 674.74 1794.31 675.436 1791.99 675.436H1710.61C1708.29 675.436 1706.2 674.74 1704.35 673.349C1702.96 671.494 1702.26 669.408 1702.26 667.089V276.201C1702.26 273.883 1701.1 272.723 1698.78 272.723H1615.31C1613 272.723 1610.91 272.028 1609.05 270.637C1607.66 268.782 1606.97 266.696 1606.97 264.377V196.911C1606.97 194.592 1607.66 192.737 1609.05 191.346C1610.91 189.492 1613 188.564 1615.31 188.564H1890.76Z" fill="black"/>
6
+ <path d="M2120.83 196.911C2120.83 194.592 2121.53 192.737 2122.92 191.346C2124.77 189.492 2126.86 188.564 2129.18 188.564H2210.56C2212.88 188.564 2214.73 189.492 2216.12 191.346C2217.98 192.737 2218.91 194.592 2218.91 196.911V667.089C2218.91 669.408 2217.98 671.494 2216.12 673.349C2214.73 674.74 2212.88 675.436 2210.56 675.436H2129.18C2126.86 675.436 2124.77 674.74 2122.92 673.349C2121.53 671.494 2120.83 669.408 2120.83 667.089V477.209C2120.83 474.891 2119.67 473.732 2117.35 473.732H2047.8C2045.48 473.732 2044.32 474.891 2044.32 477.209V667.089C2044.32 669.408 2043.39 671.494 2041.54 673.349C2040.15 674.74 2038.29 675.436 2035.97 675.436H1954.59C1952.27 675.436 1950.19 674.74 1948.33 673.349C1946.94 671.494 1946.25 669.408 1946.25 667.089V196.911C1946.25 194.592 1946.94 192.737 1948.33 191.346C1950.19 189.492 1952.27 188.564 1954.59 188.564H2035.97C2038.29 188.564 2040.15 189.492 2041.54 191.346C2043.39 192.737 2044.32 194.592 2044.32 196.911V386.095C2044.32 388.413 2045.48 389.573 2047.8 389.573H2117.35C2119.67 389.573 2120.83 388.413 2120.83 386.095V196.911Z" fill="black"/>
7
+ <path d="M2413.99 681C2371.33 681 2337.25 668.48 2311.74 643.441C2286.24 618.402 2273.49 585.017 2273.49 543.285V320.715C2273.49 278.983 2286.24 245.598 2311.74 220.559C2337.25 195.52 2371.33 183 2413.99 183C2456.65 183 2490.74 195.52 2516.24 220.559C2542.21 245.598 2555.19 278.983 2555.19 320.715V543.285C2555.19 585.017 2542.21 618.402 2516.24 643.441C2490.74 668.48 2456.65 681 2413.99 681ZM2413.99 596.841C2426.98 596.841 2437.41 592.436 2445.29 583.626C2453.17 574.352 2457.12 562.296 2457.12 547.458V316.542C2457.12 301.704 2453.17 289.88 2445.29 281.07C2437.41 271.796 2426.98 267.159 2413.99 267.159C2401.01 267.159 2390.57 271.796 2382.69 281.07C2375.27 289.88 2371.56 301.704 2371.56 316.542V547.458C2371.56 562.296 2375.27 574.352 2382.69 583.626C2390.57 592.436 2401.01 596.841 2413.99 596.841Z" fill="black"/>
8
+ <path d="M2745.54 681C2703.8 681 2670.18 668.017 2644.68 642.05C2619.64 615.62 2607.12 580.612 2607.12 537.025V196.911C2607.12 194.592 2607.81 192.737 2609.21 191.346C2611.06 189.492 2613.15 188.564 2615.47 188.564H2696.85C2699.17 188.564 2701.02 189.492 2702.41 191.346C2704.27 192.737 2705.19 194.592 2705.19 196.911V546.763C2705.19 561.601 2708.9 573.656 2716.32 582.93C2723.74 592.204 2733.48 596.841 2745.54 596.841C2757.59 596.841 2767.1 592.204 2774.06 582.93C2781.47 573.656 2785.18 561.601 2785.18 546.763V196.911C2785.18 194.592 2785.88 192.737 2787.27 191.346C2789.13 189.492 2791.21 188.564 2793.53 188.564H2874.91C2877.23 188.564 2879.09 189.492 2880.48 191.346C2882.33 192.737 2883.26 194.592 2883.26 196.911V537.025C2883.26 580.612 2870.51 615.62 2845 642.05C2819.96 668.017 2786.81 681 2745.54 681Z" fill="black"/>
9
+ <path d="M3071.47 681C3029.74 681 2996.12 669.176 2970.61 645.528C2945.57 621.416 2933.05 589.422 2933.05 549.545V314.455C2933.05 274.578 2945.57 242.816 2970.61 219.168C2996.12 195.056 3029.74 183 3071.47 183C3113.2 183 3146.82 195.056 3172.33 219.168C3197.83 243.279 3210.58 275.274 3210.58 315.151V341.581C3210.58 343.899 3209.65 345.986 3207.8 347.841C3206.41 349.232 3204.55 349.927 3202.24 349.927H3120.85C3118.54 349.927 3116.45 349.232 3114.59 347.841C3113.2 345.986 3112.51 343.899 3112.51 341.581V313.76C3112.51 299.849 3108.8 288.721 3101.38 280.374C3093.96 271.564 3083.99 267.159 3071.47 267.159C3059.41 267.159 3049.67 271.564 3042.26 280.374C3034.84 288.721 3031.13 299.849 3031.13 313.76V550.24C3031.13 564.151 3034.84 575.511 3042.26 584.321C3049.67 592.668 3059.41 596.841 3071.47 596.841C3083.99 596.841 3093.96 592.668 3101.38 584.321C3108.8 575.511 3112.51 564.151 3112.51 550.24V494.598C3112.51 492.279 3111.35 491.12 3109.03 491.12H3078.42C3076.11 491.12 3074.02 490.425 3072.16 489.034C3070.77 487.179 3070.08 485.092 3070.08 482.774V420.871C3070.08 418.553 3070.77 416.698 3072.16 415.307C3074.02 413.452 3076.11 412.525 3078.42 412.525H3202.24C3204.55 412.525 3206.41 413.452 3207.8 415.307C3209.65 416.698 3210.58 418.553 3210.58 420.871V549.545C3210.58 589.422 3197.83 621.416 3172.33 645.528C3146.82 669.176 3113.2 681 3071.47 681Z" fill="black"/>
10
+ <path d="M3439.28 196.911C3439.28 194.592 3439.98 192.737 3441.37 191.346C3443.22 189.492 3445.31 188.564 3447.63 188.564H3529.01C3531.33 188.564 3533.18 189.492 3534.57 191.346C3536.43 192.737 3537.36 194.592 3537.36 196.911V667.089C3537.36 669.408 3536.43 671.494 3534.57 673.349C3533.18 674.74 3531.33 675.436 3529.01 675.436H3447.63C3445.31 675.436 3443.22 674.74 3441.37 673.349C3439.98 671.494 3439.28 669.408 3439.28 667.089V477.209C3439.28 474.891 3438.12 473.732 3435.8 473.732H3366.25C3363.93 473.732 3362.77 474.891 3362.77 477.209V667.089C3362.77 669.408 3361.84 671.494 3359.99 673.349C3358.6 674.74 3356.74 675.436 3354.42 675.436H3273.04C3270.72 675.436 3268.64 674.74 3266.78 673.349C3265.39 671.494 3264.69 669.408 3264.69 667.089V196.911C3264.69 194.592 3265.39 192.737 3266.78 191.346C3268.64 189.492 3270.72 188.564 3273.04 188.564H3354.42C3356.74 188.564 3358.6 189.492 3359.99 191.346C3361.84 192.737 3362.77 194.592 3362.77 196.911V386.095C3362.77 388.413 3363.93 389.573 3366.25 389.573H3435.8C3438.12 389.573 3439.28 388.413 3439.28 386.095V196.911Z" fill="black"/>
11
+ <path d="M3868.77 188.564C3871.09 188.564 3872.94 189.492 3874.34 191.346C3876.19 192.737 3877.12 194.592 3877.12 196.911V264.377C3877.12 266.696 3876.19 268.782 3874.34 270.637C3872.94 272.028 3871.09 272.723 3868.77 272.723H3781.83C3779.51 272.723 3778.35 273.883 3778.35 276.201V667.089C3778.35 669.408 3777.42 671.494 3775.57 673.349C3774.17 674.74 3772.32 675.436 3770 675.436H3688.62C3686.3 675.436 3684.21 674.74 3682.36 673.349C3680.97 671.494 3680.27 669.408 3680.27 667.089V276.201C3680.27 273.883 3679.11 272.723 3676.8 272.723H3593.33C3591.01 272.723 3588.92 272.028 3587.07 270.637C3585.68 268.782 3584.98 266.696 3584.98 264.377V196.911C3584.98 194.592 3585.68 192.737 3587.07 191.346C3588.92 189.492 3591.01 188.564 3593.33 188.564H3868.77Z" fill="black"/>
12
+ <path d="M4072.41 187.869C4097.92 187.869 4120.64 194.128 4140.58 206.648C4160.52 219.168 4175.82 236.788 4186.49 259.508C4197.62 281.765 4203.18 307.268 4203.18 336.017C4203.18 379.14 4191.59 413.684 4168.4 439.651C4145.22 465.617 4114.84 478.601 4077.28 478.601H4025.12C4022.8 478.601 4021.64 479.76 4021.64 482.078V667.089C4021.64 669.408 4020.71 671.494 4018.86 673.349C4017.46 674.74 4015.61 675.436 4013.29 675.436H3931.91C3929.59 675.436 3927.5 674.74 3925.65 673.349C3924.26 671.494 3923.56 669.408 3923.56 667.089V196.215C3923.56 193.897 3924.26 192.042 3925.65 190.651C3927.5 188.796 3929.59 187.869 3931.91 187.869H4072.41ZM4055.72 402.092C4070.56 402.092 4082.38 396.528 4091.19 385.399C4100.47 373.807 4105.11 358.042 4105.11 338.103C4105.11 317.701 4100.47 301.704 4091.19 290.112C4082.38 278.52 4070.56 272.723 4055.72 272.723H4025.12C4022.8 272.723 4021.64 273.883 4021.64 276.201V398.614C4021.64 400.933 4022.8 402.092 4025.12 402.092H4055.72Z" fill="#4B7CB5"/>
13
+ <path d="M4255.93 675.436C4253.62 675.436 4251.53 674.74 4249.67 673.349C4248.28 671.494 4247.59 669.408 4247.59 667.089V196.911C4247.59 194.592 4248.28 192.737 4249.67 191.346C4251.53 189.492 4253.62 188.564 4255.93 188.564H4337.32C4339.63 188.564 4341.49 189.492 4342.88 191.346C4344.74 192.737 4345.66 194.592 4345.66 196.911V667.089C4345.66 669.408 4344.74 671.494 4342.88 673.349C4341.49 674.74 4339.63 675.436 4337.32 675.436H4255.93Z" fill="#4B7CB5"/>
14
+ <path d="M4501.8 675.436C4496.69 675.436 4493.68 672.885 4492.75 667.785L4393.29 197.606L4392.59 194.824C4392.59 190.651 4395.14 188.564 4400.24 188.564H4489.97C4495.54 188.564 4498.55 191.115 4499.01 196.215L4547.01 502.249C4547.47 504.103 4548.17 505.031 4549.09 505.031C4550.02 505.031 4550.72 504.103 4551.18 502.249L4597.78 196.215C4598.25 191.115 4601.26 188.564 4606.83 188.564H4693.77C4699.8 188.564 4702.12 191.578 4700.73 197.606L4600.57 667.785C4599.64 672.885 4596.62 675.436 4591.52 675.436H4501.8Z" fill="#4B7CB5"/>
15
+ <path d="M4869.53 681C4826.87 681 4792.78 668.48 4767.28 643.441C4741.77 618.402 4729.02 585.017 4729.02 543.285V320.715C4729.02 278.983 4741.77 245.598 4767.28 220.559C4792.78 195.52 4826.87 183 4869.53 183C4912.19 183 4946.27 195.52 4971.77 220.559C4997.74 245.598 5010.73 278.983 5010.73 320.715V543.285C5010.73 585.017 4997.74 618.402 4971.77 643.441C4946.27 668.48 4912.19 681 4869.53 681ZM4869.53 596.841C4882.51 596.841 4892.94 592.436 4900.83 583.626C4908.71 574.352 4912.65 562.296 4912.65 547.458V316.542C4912.65 301.704 4908.71 289.88 4900.83 281.07C4892.94 271.796 4882.51 267.159 4869.53 267.159C4856.54 267.159 4846.11 271.796 4838.23 281.07C4830.81 289.88 4827.1 301.704 4827.1 316.542V547.458C4827.1 562.296 4830.81 574.352 4838.23 583.626C4846.11 592.436 4856.54 596.841 4869.53 596.841Z" fill="#4B7CB5"/>
16
+ <path d="M5334.62 188.564C5336.94 188.564 5338.79 189.492 5340.19 191.346C5342.04 192.737 5342.97 194.592 5342.97 196.911V264.377C5342.97 266.696 5342.04 268.782 5340.19 270.637C5338.79 272.028 5336.94 272.723 5334.62 272.723H5247.68C5245.36 272.723 5244.2 273.883 5244.2 276.201V667.089C5244.2 669.408 5243.27 671.494 5241.41 673.349C5240.02 674.74 5238.17 675.436 5235.85 675.436H5154.47C5152.15 675.436 5150.06 674.74 5148.21 673.349C5146.82 671.494 5146.12 669.408 5146.12 667.089V276.201C5146.12 273.883 5144.96 272.723 5142.64 272.723H5059.18C5056.86 272.723 5054.77 272.028 5052.92 270.637C5051.53 268.782 5050.83 266.696 5050.83 264.377V196.911C5050.83 194.592 5051.53 192.737 5052.92 191.346C5054.77 189.492 5056.86 188.564 5059.18 188.564H5334.62Z" fill="#4B7CB5"/>
17
+ </svg>
package/src/flight.ts CHANGED
@@ -1,4 +1,6 @@
1
- #!ts-node
1
+ #!/usr/bin/env node
2
+
3
+ import 'dotenv/config'
2
4
 
3
5
  import { exec, spawn } from 'child_process'
4
6
 
@@ -19,6 +21,17 @@ import ratelimit from 'koa-ratelimit'
19
21
  import serve from 'koa-static'
20
22
  import session from 'koa-session'
21
23
 
24
+ import {
25
+ applyTrustProxy,
26
+ httpCacheEnabledInSpaPipeline,
27
+ parseCommaPrefixes,
28
+ productionSpaPipelineActive,
29
+ ratelimitWithPrefixSkips,
30
+ resolveDistRoot,
31
+ spaIndexHtmlFallback,
32
+ spaIndexRelative
33
+ } from './spa-pipeline.js'
34
+
22
35
  /** CLI argv shape after Flight applies defaults (see block below). */
23
36
  interface FlightArgv {
24
37
  session_duration?: number
@@ -29,13 +42,98 @@ interface FlightArgv {
29
42
  payload_limit?: string
30
43
  disable_vite?: boolean
31
44
  mode?: string
45
+ exclude_paths?: string[]
46
+ }
47
+
48
+ function normalizeExcludePaths(value: unknown): string[] {
49
+ if (value == null || value === '') return []
50
+ const parts = Array.isArray(value) ? value : [value]
51
+ const out: string[] = []
52
+ for (const p of parts) {
53
+ const s = String(p).trim()
54
+ if (!s) continue
55
+ out.push(
56
+ ...s
57
+ .split(',')
58
+ .map((x) => x.trim())
59
+ .filter(Boolean)
60
+ )
61
+ }
62
+ return out
63
+ }
64
+
65
+ function dedupeStrings(items: string[]): string[] {
66
+ return [...new Set(items)]
67
+ }
68
+
69
+ /** Build fast-glob ignore globs for trees rooted under `appRootAbs`. */
70
+ function backendDiscoveryIgnorePatterns(appRootAbs: string, excludeRelativeDirs: string[]): string[] {
71
+ const patterns: string[] = []
72
+ for (const raw of excludeRelativeDirs) {
73
+ const trimmed = raw.trim()
74
+ if (!trimmed) continue
75
+ const resolved = path.resolve(appRootAbs, trimmed)
76
+ const rel = path.relative(appRootAbs, resolved)
77
+ const relPosix = rel.replace(/\\/g, '/')
78
+ if (!relPosix || relPosix.startsWith('..') || path.isAbsolute(rel)) {
79
+ console.warn(`Flight: exclude_paths entry skipped (outside app_home): ${trimmed}`)
80
+ continue
81
+ }
82
+ patterns.push(`${relPosix}/**`)
83
+ }
84
+ return patterns
32
85
  }
33
86
 
34
87
  // tsx (and similar loaders) may expose require('yargs/yargs') as { default: factory } instead of factory.
35
- type YargsFactoryFn = (args: string[]) => { argv: FlightArgv }
36
- const _yargs = require('yargs/yargs') as YargsFactoryFn | { default: YargsFactoryFn }
37
- const yargsFactory: YargsFactoryFn = typeof _yargs === 'function' ? _yargs : _yargs.default
38
- const argv = yargsFactory(process.argv.slice(2)).argv
88
+ interface YargsInstance {
89
+ option(key: string, opts: Record<string, unknown>): YargsInstance
90
+ parseSync(): FlightArgv
91
+ }
92
+ type YargsFn = (args: string[]) => YargsInstance
93
+ const _yargs = require('yargs/yargs') as YargsFn | { default: YargsFn }
94
+ const yargsEntry: YargsFn = typeof _yargs === 'function' ? _yargs : _yargs.default
95
+ const argv = yargsEntry(process.argv.slice(2))
96
+ .option('exclude_paths', {
97
+ alias: 'exclude-paths',
98
+ type: 'array',
99
+ string: true,
100
+ default: [],
101
+ describe: 'Directories under app_home to skip when discovering **/*.backend.ts (repeat flag or comma-separated)'
102
+ })
103
+ .option('mode', {
104
+ type: 'string',
105
+ describe: 'development (Vite HMR) or production'
106
+ })
107
+ .option('port', {
108
+ type: 'number',
109
+ describe: 'HTTP listen port'
110
+ })
111
+ .option('app_home', {
112
+ alias: 'app-home',
113
+ type: 'string',
114
+ describe: 'Application root directory'
115
+ })
116
+ .option('app_key', {
117
+ alias: 'app-key',
118
+ type: 'string',
119
+ describe: 'Session cookie name'
120
+ })
121
+ .option('app_secret', {
122
+ alias: 'app-secret',
123
+ type: 'string',
124
+ describe: 'Session signing secret(s), comma-separated for rotation'
125
+ })
126
+ .option('payload_limit', {
127
+ alias: 'payload-limit',
128
+ type: 'string',
129
+ describe: 'koa-bodyparser JSON body limit'
130
+ })
131
+ .option('disable_vite', {
132
+ alias: 'disable-vite',
133
+ type: 'boolean',
134
+ describe: 'Skip vite build in production; enables built-assets / SPA pipeline when mode is production'
135
+ })
136
+ .parseSync() as FlightArgv
39
137
 
40
138
  // Set default session duration (24 hours in milliseconds)
41
139
  const DEFAULT_SESSION_DURATION = 86400000 // 24 hours in milliseconds
@@ -50,15 +148,15 @@ if (isNaN(argv.session_duration) || argv.session_duration < 0) {
50
148
  }
51
149
 
52
150
  if (!argv.app_home) {
53
- argv.app_home = '.'
151
+ argv.app_home = process.env.FLIGHT_APP_HOME || '.'
54
152
  }
55
153
 
56
154
  if (!argv.app_key) {
57
- argv.app_key = 'flightApp'
155
+ argv.app_key = process.env.FLIGHT_APP_KEY || 'flightApp'
58
156
  }
59
157
 
60
158
  if (!argv.app_secret) {
61
- argv.app_secret = 'the best secret key in the world'
159
+ argv.app_secret = process.env.FLIGHT_APP_SECRET || 'the best secret key in the world'
62
160
  }
63
161
 
64
162
  // Set default port values
@@ -82,17 +180,29 @@ if (isNaN(argv.port) || argv.port < 1 || argv.port > 65535) {
82
180
 
83
181
  // Set default value for disable_vite flag
84
182
  if (argv.disable_vite === undefined) {
85
- // Check for environment variable first, then default to false
86
- argv.disable_vite = process.env.FLIGHT_DISABLE_VITE === 'true' ? true : false
183
+ const dv = process.env.FLIGHT_DISABLE_VITE
184
+ argv.disable_vite = dv === 'true' || dv === '1' || dv === 'yes'
87
185
  }
88
186
 
89
187
  // Ensure the value is a boolean
90
188
  argv.disable_vite = Boolean(argv.disable_vite)
91
189
 
92
190
  const appHomePath = path.resolve(argv.app_home)
191
+ const excludePathsConfigured = dedupeStrings([
192
+ ...normalizeExcludePaths(argv.exclude_paths),
193
+ ...normalizeExcludePaths(process.env.FLIGHT_EXCLUDE_PATHS)
194
+ ])
195
+ const backendDiscoveryIgnores = backendDiscoveryIgnorePatterns(appHomePath, excludePathsConfigured)
196
+
93
197
  process.chdir(appHomePath)
94
198
 
95
199
  console.log(appHomePath)
200
+ if (backendDiscoveryIgnores.length > 0) {
201
+ console.log(
202
+ 'Flight: excluding **/*.backend.ts discovery under:',
203
+ backendDiscoveryIgnores.map((g) => g.replace(/\*\*$/, '')).join(', ')
204
+ )
205
+ }
96
206
 
97
207
  const mode = process.env.FLIGHT_MODE || argv.mode || 'production'
98
208
 
@@ -128,6 +238,8 @@ if (cluster.isPrimary) {
128
238
  })
129
239
  } else {
130
240
  const app = new Koa()
241
+ applyTrustProxy(app)
242
+
131
243
  app.use(logger())
132
244
 
133
245
  app.keys = argv.app_secret.split(',')
@@ -152,7 +264,9 @@ if (cluster.isPrimary) {
152
264
  })
153
265
  )
154
266
 
155
- const backEndFiles = fg.sync('**/*.backend.ts')
267
+ const backEndFiles = fg.sync('**/*.backend.ts', {
268
+ ignore: backendDiscoveryIgnores
269
+ })
156
270
  backEndFiles.forEach((file) => {
157
271
  const serverRoutes = require(path.resolve(file))
158
272
 
@@ -179,30 +293,52 @@ if (cluster.isPrimary) {
179
293
  })
180
294
  }
181
295
 
182
- app.use(compress())
183
- app.use(
184
- ratelimit({
185
- driver: 'redis',
186
- db: redis,
187
- duration: 60000,
188
- errorMessage: 'Sometimes You Just Have to Slow Down.',
189
- id: (ctx) => ctx.get('x-forwarded-for') || ctx.ip,
190
- headers: {
191
- remaining: 'Rate-Limit-Remaining',
192
- reset: 'Rate-Limit-Reset',
193
- total: 'Rate-Limit-Total'
194
- },
195
- max: 1200,
196
- disableHeader: false
197
- })
198
- )
199
- app.use(
200
- koaCash({
201
- get: (key) => redis.get(key),
202
- set: (key, value) => redis.set(key, value, 'EX', 30)
203
- })
204
- )
205
- app.use(serve(process.env.FLIGHT_DIST_PATH || '../dist'))
296
+ const distRoot = resolveDistRoot(process.cwd())
297
+ const useSpaPipeline = productionSpaPipelineActive(mode, Boolean(argv.disable_vite))
298
+ const staticPrefixes = parseCommaPrefixes(process.env.FLIGHT_STATIC_PREFIXES, '/assets,/fonts')
299
+ const rateLimitSkipPrefixes = dedupeStrings([
300
+ ...staticPrefixes,
301
+ ...parseCommaPrefixes(process.env.FLIGHT_RATE_LIMIT_EXCLUDE_PREFIXES, '')
302
+ ])
303
+ const spaDenyExtra = parseCommaPrefixes(process.env.FLIGHT_SPA_DENY_PREFIXES, '')
304
+
305
+ const productionRatelimit = ratelimit({
306
+ driver: 'redis',
307
+ db: redis,
308
+ duration: 60000,
309
+ errorMessage: 'Sometimes You Just Have to Slow Down.',
310
+ id: (ctx) => ctx.get('x-forwarded-for') || ctx.ip,
311
+ headers: {
312
+ remaining: 'Rate-Limit-Remaining',
313
+ reset: 'Rate-Limit-Reset',
314
+ total: 'Rate-Limit-Total'
315
+ },
316
+ max: 1200,
317
+ disableHeader: false
318
+ })
319
+
320
+ const productionKoaCash = koaCash({
321
+ get: (key) => redis.get(key),
322
+ set: (key, value) => redis.set(key, value, 'EX', 30)
323
+ })
324
+
325
+ if (useSpaPipeline) {
326
+ console.log(
327
+ 'Flight: production SPA pipeline (static + index.html fallback before compress / rate limit); opt out with FLIGHT_DISABLE_SPA_PIPELINE=1'
328
+ )
329
+ app.use(serve(distRoot))
330
+ app.use(spaIndexHtmlFallback(distRoot, spaIndexRelative(), spaDenyExtra))
331
+ app.use(compress())
332
+ app.use(ratelimitWithPrefixSkips(redis, rateLimitSkipPrefixes))
333
+ if (httpCacheEnabledInSpaPipeline()) {
334
+ app.use(productionKoaCash)
335
+ }
336
+ } else {
337
+ app.use(compress())
338
+ app.use(productionRatelimit)
339
+ app.use(productionKoaCash)
340
+ app.use(serve(distRoot))
341
+ }
206
342
 
207
343
  if (!argv.disable_vite) {
208
344
  console.log(`App served out of dist/ and available on port ${argv.port}`)
@@ -1,83 +0,0 @@
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 DELETED
@@ -1,5 +0,0 @@
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
@@ -1,11 +0,0 @@
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 DELETED
@@ -1 +0,0 @@
1
- 20.11
package/.prettierrc DELETED
@@ -1,11 +0,0 @@
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
- }
@@ -1,3 +0,0 @@
1
- {
2
- "recommendations": ["esbenp.prettier-vscode"]
3
- }
@@ -1,10 +0,0 @@
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/eslint.config.mjs DELETED
@@ -1,32 +0,0 @@
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/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "NodeNext",
5
- "experimentalDecorators": true,
6
- "emitDecoratorMetadata": true,
7
- "moduleResolution": "NodeNext",
8
- "outDir": "dist"
9
- }
10
- }