@thoughtpivot/flight 1.0.8 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +169 -137
- package/dist/flight.js +128 -15
- package/dist/spa-pipeline.js +122 -0
- package/package.json +12 -2
- package/readme-assets/thoughtpivot-logo.svg +17 -0
- package/src/flight.ts +171 -35
- package/.circleci/config.yml +0 -83
- package/.editorconfig +0 -5
- package/.github/dependabot.yml +0 -11
- package/.nvmrc +0 -1
- package/.prettierrc +0 -11
- package/.vscode/extensions.json +0 -3
- package/.vscode/settings.json +0 -10
- package/eslint.config.mjs +0 -32
- package/tsconfig.json +0 -10
|
@@ -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": "
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
86
|
-
argv.disable_vite =
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
)
|
|
205
|
-
|
|
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}`)
|
package/.circleci/config.yml
DELETED
|
@@ -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
|
package/.github/dependabot.yml
DELETED
|
@@ -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
package/.vscode/extensions.json
DELETED
package/.vscode/settings.json
DELETED
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
|
-
]
|