@nerest/nerest 0.0.4 → 0.0.6
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/README.md +35 -4
- package/build/excludes/empty-module +1 -0
- package/build/excludes/index.ts +20 -0
- package/build/index.ts +23 -0
- package/dist/build/excludes/index.d.ts +8 -0
- package/dist/build/excludes/index.js +23 -0
- package/dist/build/index.js +20 -0
- package/dist/server/development.js +7 -0
- package/dist/server/parts/k8s-probes.d.ts +2 -0
- package/dist/server/parts/k8s-probes.js +31 -0
- package/package.json +25 -17
- package/schemas/nerest-build.schema.d.ts +16 -0
- package/schemas/nerest-build.schema.json +22 -0
- package/server/development.ts +9 -0
- package/server/parts/k8s-probes.ts +30 -0
- package/server/production.ts +10 -0
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ React micro frontend framework
|
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
|
+
TODO: update package
|
|
10
|
+
|
|
9
11
|
```
|
|
10
12
|
npm i --save @nerest/nerest react react-dom
|
|
11
13
|
```
|
|
@@ -23,7 +25,7 @@ npm i --save @nerest/nerest react react-dom
|
|
|
23
25
|
|
|
24
26
|
The `apps` directory must contain all of the apps provided by the micro frontend. E.g. `/apps/foo/index.tsx` is the entrypoint component of the `foo` app. It becomes available as the `/api/foo` route of the micro frontend server.
|
|
25
27
|
|
|
26
|
-
See [nerest-harness](https://
|
|
28
|
+
See [nerest-harness](https://gitlab.tcsbank.ru/tj/nerest-harness) for the minimal example of a nerest micro frontend.
|
|
27
29
|
|
|
28
30
|
### Examples (`/examples/*.json`)
|
|
29
31
|
|
|
@@ -37,7 +39,7 @@ OpenAPI specification is compiled automatically based on the provided schemas an
|
|
|
37
39
|
|
|
38
40
|
## Configuration
|
|
39
41
|
|
|
40
|
-
Different aspects of Nerest apps can be configured via environment variables, JSON configuration and runtime hooks written in TypeScript. Examples of all kinds of configuration can be viewed in the [nerest-harness](https://
|
|
42
|
+
Different aspects of Nerest apps can be configured via environment variables, JSON configuration and runtime hooks written in TypeScript. Examples of all kinds of configuration can be viewed in the [nerest-harness](https://gitlab.tcsbank.ru/tj/nerest-harness) repository.
|
|
41
43
|
|
|
42
44
|
### Environment Variables
|
|
43
45
|
|
|
@@ -47,11 +49,40 @@ Different aspects of Nerest apps can be configured via environment variables, JS
|
|
|
47
49
|
|
|
48
50
|
#### Runtime
|
|
49
51
|
|
|
52
|
+
##### Client
|
|
53
|
+
|
|
50
54
|
All environment variables prefixed with `NEREST_` will be bundled with your app during buildtime. You can access them in the code using the special `import.meta.env` object. For example, `import.meta.env.NEREST_SOMEVAR` will be statically replaced during buildtime with the value of this environment variable on the build machine.
|
|
51
55
|
|
|
56
|
+
##### Server
|
|
57
|
+
|
|
58
|
+
Additional environment variables which can be setup for a runtime:
|
|
59
|
+
|
|
60
|
+
- `ENABLE_K8S_PROBES`: activates additional routes `/livenessProbe` and `/readinessProbe` to check application status
|
|
61
|
+
|
|
52
62
|
### JSON Configuration
|
|
53
63
|
|
|
54
|
-
|
|
64
|
+
You can configure the output of `nerest build` by placing an optional `nerest-build.json` configuration file in the root of the micro frontend. The full schema of this file is located in [schemas/nerest-build.schema.json](schemas/nerest-build.schema.json).
|
|
65
|
+
|
|
66
|
+
#### excludes: `string[]`
|
|
67
|
+
|
|
68
|
+
Excludes modules from the client build and replaces their imports with imports of an empty module instead. You can use this to exclude either JS or CSS modules from the final build.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
"excludes": ["@scope/name"]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### externals: `Record<string, string>`
|
|
75
|
+
|
|
76
|
+
Excludes modules from the client build and maps them to globally available constants instead. You can use this to share common dependencies between different micro frontends by exposing them on the `window` object. For example:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
"externals": {
|
|
80
|
+
"react": "window.React",
|
|
81
|
+
"react-dom": "window['ReactDOM']"
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Here `react` and `react-dom` imports will be replaced with accessing the respective `window` constants.
|
|
55
86
|
|
|
56
87
|
### Runtime Hooks
|
|
57
88
|
|
|
@@ -66,4 +97,4 @@ npm install
|
|
|
66
97
|
npm run build
|
|
67
98
|
```
|
|
68
99
|
|
|
69
|
-
Use [nerest-harness](https://
|
|
100
|
+
Use [nerest-harness](https://gitlab.tcsbank.ru/tj/nerest-harness) to test changes locally.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Empty module to replace excluded imports with. */
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps list of imports into a list of rollup aliases that resolve
|
|
3
|
+
* into an empty module.
|
|
4
|
+
*/
|
|
5
|
+
export function excludes(list: string[] | undefined) {
|
|
6
|
+
return list?.map((exclude) => ({
|
|
7
|
+
// Excluding '@some/package' should exclude both '@some/package' and
|
|
8
|
+
// '@some/package/...` imports
|
|
9
|
+
find: new RegExp(`^${escapeRegExp(exclude)}($|\\/.*)`),
|
|
10
|
+
replacement: '@nerest/nerest/build/excludes/empty-module',
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Escapes string to use inside of a regular expression.
|
|
16
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
|
17
|
+
*/
|
|
18
|
+
function escapeRegExp(str: string) {
|
|
19
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
20
|
+
}
|
package/build/index.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
3
4
|
|
|
4
5
|
import vite from 'vite';
|
|
5
6
|
import type { InlineConfig } from 'vite';
|
|
7
|
+
import { viteExternalsPlugin } from 'vite-plugin-externals';
|
|
6
8
|
|
|
7
9
|
import { loadApps } from '../server/parts/apps';
|
|
10
|
+
import type { BuildConfiguration } from '../schemas/nerest-build.schema';
|
|
11
|
+
import { excludes } from './excludes';
|
|
8
12
|
|
|
9
13
|
export async function buildMicroFrontend() {
|
|
10
14
|
const root = process.cwd();
|
|
@@ -19,6 +23,8 @@ export async function buildMicroFrontend() {
|
|
|
19
23
|
);
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
const buildConfig = await readBuildConfig(root);
|
|
27
|
+
|
|
22
28
|
// Build client
|
|
23
29
|
// TODO: extract shared parts between build/index.ts and server/index.ts
|
|
24
30
|
// into a shared config
|
|
@@ -40,6 +46,14 @@ export async function buildMicroFrontend() {
|
|
|
40
46
|
},
|
|
41
47
|
},
|
|
42
48
|
},
|
|
49
|
+
resolve: {
|
|
50
|
+
// excludes - map buildConfig.excludes packages to an empty module
|
|
51
|
+
alias: excludes(buildConfig?.excludes),
|
|
52
|
+
},
|
|
53
|
+
plugins: [
|
|
54
|
+
// externals - map buildConfig.externals packages to a global variable on window
|
|
55
|
+
viteExternalsPlugin(buildConfig?.externals, { useWindow: false }),
|
|
56
|
+
],
|
|
43
57
|
};
|
|
44
58
|
|
|
45
59
|
console.log('Producing production client build...');
|
|
@@ -80,3 +94,12 @@ async function buildAppsManifest(root: string, staticPath: string) {
|
|
|
80
94
|
{ encoding: 'utf-8' }
|
|
81
95
|
);
|
|
82
96
|
}
|
|
97
|
+
|
|
98
|
+
// TODO: error handling
|
|
99
|
+
async function readBuildConfig(root: string) {
|
|
100
|
+
const configPath = path.join(root, 'nerest-build.json');
|
|
101
|
+
if (existsSync(configPath)) {
|
|
102
|
+
const content = await fs.readFile(configPath, { encoding: 'utf-8' });
|
|
103
|
+
return JSON.parse(content) as BuildConfiguration;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.excludes = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Maps list of imports into a list of rollup aliases that resolve
|
|
6
|
+
* into an empty module.
|
|
7
|
+
*/
|
|
8
|
+
function excludes(list) {
|
|
9
|
+
return list?.map((exclude) => ({
|
|
10
|
+
// Excluding '@some/package' should exclude both '@some/package' and
|
|
11
|
+
// '@some/package/...` imports
|
|
12
|
+
find: new RegExp(`^${escapeRegExp(exclude)}($|\\/.*)`),
|
|
13
|
+
replacement: '@nerest/nerest/build/excludes/empty-module',
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
exports.excludes = excludes;
|
|
17
|
+
/**
|
|
18
|
+
* Escapes string to use inside of a regular expression.
|
|
19
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
|
20
|
+
*/
|
|
21
|
+
function escapeRegExp(str) {
|
|
22
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
23
|
+
}
|
package/dist/build/index.js
CHANGED
|
@@ -6,8 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.buildMicroFrontend = void 0;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const fs_1 = require("fs");
|
|
9
10
|
const vite_1 = __importDefault(require("vite"));
|
|
11
|
+
const vite_plugin_externals_1 = require("vite-plugin-externals");
|
|
10
12
|
const apps_1 = require("../server/parts/apps");
|
|
13
|
+
const excludes_1 = require("./excludes");
|
|
11
14
|
async function buildMicroFrontend() {
|
|
12
15
|
const root = process.cwd();
|
|
13
16
|
const staticPath = process.env.NEREST_STATIC_PATH;
|
|
@@ -17,6 +20,7 @@ async function buildMicroFrontend() {
|
|
|
17
20
|
if (!staticPath) {
|
|
18
21
|
throw new Error('NEREST_STATIC_PATH environment variable is not set but is required for the production build');
|
|
19
22
|
}
|
|
23
|
+
const buildConfig = await readBuildConfig(root);
|
|
20
24
|
// Build client
|
|
21
25
|
// TODO: extract shared parts between build/index.ts and server/index.ts
|
|
22
26
|
// into a shared config
|
|
@@ -38,6 +42,14 @@ async function buildMicroFrontend() {
|
|
|
38
42
|
},
|
|
39
43
|
},
|
|
40
44
|
},
|
|
45
|
+
resolve: {
|
|
46
|
+
// excludes - map buildConfig.excludes packages to an empty module
|
|
47
|
+
alias: (0, excludes_1.excludes)(buildConfig?.excludes),
|
|
48
|
+
},
|
|
49
|
+
plugins: [
|
|
50
|
+
// externals - map buildConfig.externals packages to a global variable on window
|
|
51
|
+
(0, vite_plugin_externals_1.viteExternalsPlugin)(buildConfig?.externals, { useWindow: false }),
|
|
52
|
+
],
|
|
41
53
|
};
|
|
42
54
|
console.log('Producing production client build...');
|
|
43
55
|
await vite_1.default.build(clientConfig);
|
|
@@ -70,3 +82,11 @@ async function buildAppsManifest(root, staticPath) {
|
|
|
70
82
|
const apps = await (0, apps_1.loadApps)(root, staticPath);
|
|
71
83
|
await promises_1.default.writeFile(path_1.default.join(root, 'build/nerest-manifest.json'), JSON.stringify(apps), { encoding: 'utf-8' });
|
|
72
84
|
}
|
|
85
|
+
// TODO: error handling
|
|
86
|
+
async function readBuildConfig(root) {
|
|
87
|
+
const configPath = path_1.default.join(root, 'nerest-build.json');
|
|
88
|
+
if ((0, fs_1.existsSync)(configPath)) {
|
|
89
|
+
const content = await promises_1.default.readFile(configPath, { encoding: 'utf-8' });
|
|
90
|
+
return JSON.parse(content);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -9,11 +9,13 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const vite_1 = __importDefault(require("vite"));
|
|
10
10
|
const fastify_1 = __importDefault(require("fastify"));
|
|
11
11
|
const static_1 = __importDefault(require("@fastify/static"));
|
|
12
|
+
const fastify_graceful_shutdown_1 = __importDefault(require("fastify-graceful-shutdown"));
|
|
12
13
|
const apps_1 = require("./parts/apps");
|
|
13
14
|
const render_1 = require("./parts/render");
|
|
14
15
|
const preview_1 = require("./parts/preview");
|
|
15
16
|
const validator_1 = require("./parts/validator");
|
|
16
17
|
const swagger_1 = require("./parts/swagger");
|
|
18
|
+
const k8s_probes_1 = require("./parts/k8s-probes");
|
|
17
19
|
async function runDevelopmentServer() {
|
|
18
20
|
const root = process.cwd();
|
|
19
21
|
// TODO: move build config into a separate file
|
|
@@ -119,6 +121,11 @@ async function runDevelopmentServer() {
|
|
|
119
121
|
});
|
|
120
122
|
}
|
|
121
123
|
}
|
|
124
|
+
// Add graceful shutdown handler to prevent requests errors
|
|
125
|
+
await app.register(fastify_graceful_shutdown_1.default);
|
|
126
|
+
if (process.env.ENABLE_K8S_PROBES) {
|
|
127
|
+
await (0, k8s_probes_1.setupK8SProbes)(app);
|
|
128
|
+
}
|
|
122
129
|
// TODO: only do this locally, load from CDN in production
|
|
123
130
|
await app.register(static_1.default, {
|
|
124
131
|
root: path_1.default.join(root, 'build'),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setupK8SProbes = void 0;
|
|
4
|
+
// Setup routes for k8s probes to check if application is live
|
|
5
|
+
async function setupK8SProbes(app) {
|
|
6
|
+
// Handler for graceful shutdowns
|
|
7
|
+
// K8s can initiate shutdown at any moment: on pods restart or on deploy.
|
|
8
|
+
// So, if we receive shutdown request, we:
|
|
9
|
+
// - starts to send 503 response code on readiness probes to stop receiving requests
|
|
10
|
+
// - finishes all current requests
|
|
11
|
+
// - shuts down the server
|
|
12
|
+
let isShutdownInProgress = false;
|
|
13
|
+
app.gracefulShutdown((code, next) => {
|
|
14
|
+
// TODO: replace with nerest logger, when it'll be ready
|
|
15
|
+
console.log('Graceful shutdown in process...');
|
|
16
|
+
isShutdownInProgress = true;
|
|
17
|
+
next();
|
|
18
|
+
});
|
|
19
|
+
app.get('/livenessProbe', { logLevel: 'silent' }, (req, res) => {
|
|
20
|
+
res.status(200).send();
|
|
21
|
+
});
|
|
22
|
+
app.get('/readinessProbe', { logLevel: 'silent' }, (req, res) => {
|
|
23
|
+
if (isShutdownInProgress) {
|
|
24
|
+
res.status(503).send({ status: 'Shutdown in progress' });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
res.status(200).send();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
exports.setupK8SProbes = setupK8SProbes;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerest/nerest",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "React micro frontend framework",
|
|
5
5
|
"homepage": "https://github.com/nerestjs/nerest#readme",
|
|
6
6
|
"repository": {
|
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
"/bin",
|
|
16
16
|
"/build",
|
|
17
17
|
"/client",
|
|
18
|
+
"/schemas",
|
|
18
19
|
"/server",
|
|
19
20
|
"/README.md"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
23
|
"build": "tsc -p tsconfig.json -d",
|
|
23
24
|
"lint": "eslint --ext .ts server bin",
|
|
24
|
-
"prepare": "simple-git-hooks"
|
|
25
|
+
"prepare": "simple-git-hooks",
|
|
26
|
+
"typegen": "json2ts -i 'schemas/**/*.json' -o schemas --bannerComment ''"
|
|
25
27
|
},
|
|
26
28
|
"simple-git-hooks": {
|
|
27
29
|
"pre-commit": "npx lint-staged"
|
|
@@ -40,6 +42,9 @@
|
|
|
40
42
|
},
|
|
41
43
|
"prettier": "@tinkoff/prettier-config",
|
|
42
44
|
"eslintConfig": {
|
|
45
|
+
"parserOptions": {
|
|
46
|
+
"project": true
|
|
47
|
+
},
|
|
43
48
|
"extends": [
|
|
44
49
|
"@tinkoff/eslint-config/lib",
|
|
45
50
|
"@tinkoff/eslint-config/jest",
|
|
@@ -47,30 +52,33 @@
|
|
|
47
52
|
]
|
|
48
53
|
},
|
|
49
54
|
"dependencies": {
|
|
50
|
-
"@fastify/static": "^6.
|
|
51
|
-
"@fastify/swagger": "^8.
|
|
52
|
-
"@fastify/swagger-ui": "^1.
|
|
55
|
+
"@fastify/static": "^6.11.2",
|
|
56
|
+
"@fastify/swagger": "^8.12.0",
|
|
57
|
+
"@fastify/swagger-ui": "^1.10.1",
|
|
53
58
|
"ajv": "^8.12.0",
|
|
54
59
|
"ajv-formats": "^2.1.1",
|
|
55
|
-
"dotenv": "^16.1
|
|
60
|
+
"dotenv": "^16.3.1",
|
|
56
61
|
"fast-uri": "^2.2.0",
|
|
57
|
-
"fastify": "^4.
|
|
62
|
+
"fastify": "^4.24.3",
|
|
63
|
+
"fastify-graceful-shutdown": "^3.5.1",
|
|
58
64
|
"nanoid": "^3.3.6",
|
|
59
|
-
"vite": "^4.
|
|
65
|
+
"vite": "^4.5.0",
|
|
66
|
+
"vite-plugin-externals": "^0.6.2"
|
|
60
67
|
},
|
|
61
68
|
"devDependencies": {
|
|
62
|
-
"@tinkoff/eslint-config": "^1.
|
|
63
|
-
"@tinkoff/eslint-config-react": "^1.
|
|
69
|
+
"@tinkoff/eslint-config": "^1.54.4",
|
|
70
|
+
"@tinkoff/eslint-config-react": "^1.54.4",
|
|
64
71
|
"@tinkoff/prettier-config": "^1.52.1",
|
|
65
|
-
"@types/react": "^18.2.
|
|
66
|
-
"@types/react-dom": "^18.2.
|
|
67
|
-
"jest": "^29.
|
|
68
|
-
"
|
|
72
|
+
"@types/react": "^18.2.29",
|
|
73
|
+
"@types/react-dom": "^18.2.14",
|
|
74
|
+
"jest": "^29.7.0",
|
|
75
|
+
"json-schema-to-typescript": "^13.1.1",
|
|
76
|
+
"lint-staged": "^15.0.2",
|
|
69
77
|
"react": "^18.2.0",
|
|
70
78
|
"react-dom": "^18.2.0",
|
|
71
|
-
"simple-git-hooks": "^2.
|
|
72
|
-
"sort-package-json": "^2.
|
|
73
|
-
"typescript": "^5.
|
|
79
|
+
"simple-git-hooks": "^2.9.0",
|
|
80
|
+
"sort-package-json": "^2.6.0",
|
|
81
|
+
"typescript": "^5.2.2"
|
|
74
82
|
},
|
|
75
83
|
"peerDependencies": {
|
|
76
84
|
"react": "^18.0.0",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build configuration placed as nerest-build.schema.json in the root of the application.
|
|
3
|
+
*/
|
|
4
|
+
export interface BuildConfiguration {
|
|
5
|
+
/**
|
|
6
|
+
* Excludes modules from the client build and replaces them with empty modules instead.
|
|
7
|
+
*/
|
|
8
|
+
excludes?: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Excludes modules from the client build and maps them to globally available constants instead.
|
|
11
|
+
*/
|
|
12
|
+
externals?: {
|
|
13
|
+
[k: string]: string;
|
|
14
|
+
};
|
|
15
|
+
[k: string]: unknown;
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft-07/schema",
|
|
3
|
+
"title": "Build Configuration",
|
|
4
|
+
"description": "Build configuration placed as nerest-build.schema.json in the root of the application.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"excludes": {
|
|
8
|
+
"description": "Excludes modules from the client build and replaces them with empty modules instead.",
|
|
9
|
+
"type": "array",
|
|
10
|
+
"items": {
|
|
11
|
+
"type": "string"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"externals": {
|
|
15
|
+
"description": "Excludes modules from the client build and maps them to globally available constants instead.",
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": {
|
|
18
|
+
"type": "string"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/server/development.ts
CHANGED
|
@@ -9,12 +9,14 @@ import type { RollupWatcher, RollupWatcherEvent } from 'rollup';
|
|
|
9
9
|
import type { RouteShorthandOptions } from 'fastify';
|
|
10
10
|
import fastify from 'fastify';
|
|
11
11
|
import fastifyStatic from '@fastify/static';
|
|
12
|
+
import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
|
|
12
13
|
|
|
13
14
|
import { loadApps } from './parts/apps';
|
|
14
15
|
import { renderApp } from './parts/render';
|
|
15
16
|
import { renderPreviewPage } from './parts/preview';
|
|
16
17
|
import { validator } from './parts/validator';
|
|
17
18
|
import { setupSwagger } from './parts/swagger';
|
|
19
|
+
import { setupK8SProbes } from './parts/k8s-probes';
|
|
18
20
|
|
|
19
21
|
export async function runDevelopmentServer() {
|
|
20
22
|
const root = process.cwd();
|
|
@@ -148,6 +150,13 @@ export async function runDevelopmentServer() {
|
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
// Add graceful shutdown handler to prevent requests errors
|
|
154
|
+
await app.register(fastifyGracefulShutdown);
|
|
155
|
+
|
|
156
|
+
if (process.env.ENABLE_K8S_PROBES) {
|
|
157
|
+
await setupK8SProbes(app);
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
// TODO: only do this locally, load from CDN in production
|
|
152
161
|
await app.register(fastifyStatic, {
|
|
153
162
|
root: path.join(root, 'build'),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
|
|
3
|
+
// Setup routes for k8s probes to check if application is live
|
|
4
|
+
export async function setupK8SProbes(app: FastifyInstance) {
|
|
5
|
+
// Handler for graceful shutdowns
|
|
6
|
+
// K8s can initiate shutdown at any moment: on pods restart or on deploy.
|
|
7
|
+
// So, if we receive shutdown request, we:
|
|
8
|
+
// - starts to send 503 response code on readiness probes to stop receiving requests
|
|
9
|
+
// - finishes all current requests
|
|
10
|
+
// - shuts down the server
|
|
11
|
+
let isShutdownInProgress = false;
|
|
12
|
+
app.gracefulShutdown((code, next) => {
|
|
13
|
+
// TODO: replace with nerest logger, when it'll be ready
|
|
14
|
+
console.log('Graceful shutdown in process...');
|
|
15
|
+
isShutdownInProgress = true;
|
|
16
|
+
next();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.get('/livenessProbe', { logLevel: 'silent' }, (req, res) => {
|
|
20
|
+
res.status(200).send();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
app.get('/readinessProbe', { logLevel: 'silent' }, (req, res) => {
|
|
24
|
+
if (isShutdownInProgress) {
|
|
25
|
+
res.status(503).send({ status: 'Shutdown in progress' });
|
|
26
|
+
} else {
|
|
27
|
+
res.status(200).send();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
package/server/production.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
|
|
5
5
|
import fastify from 'fastify';
|
|
6
|
+
import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
|
|
6
7
|
import type { RouteShorthandOptions } from 'fastify';
|
|
7
8
|
|
|
8
9
|
import type { AppEntry } from './parts/apps';
|
|
@@ -10,6 +11,7 @@ import { renderApp } from './parts/render';
|
|
|
10
11
|
import { setupSwagger } from './parts/swagger';
|
|
11
12
|
import { validator } from './parts/validator';
|
|
12
13
|
import { renderPreviewPage } from './parts/preview';
|
|
14
|
+
import { setupK8SProbes } from './parts/k8s-probes';
|
|
13
15
|
|
|
14
16
|
// TODO: refactor to merge the similar parts between production and development server?
|
|
15
17
|
async function runProductionServer() {
|
|
@@ -22,6 +24,7 @@ async function runProductionServer() {
|
|
|
22
24
|
})
|
|
23
25
|
) as Record<string, AppEntry>;
|
|
24
26
|
|
|
27
|
+
// TODO: fix client-side vite types
|
|
25
28
|
const components = import.meta.glob('/apps/*/index.tsx', {
|
|
26
29
|
import: 'default',
|
|
27
30
|
eager: true,
|
|
@@ -93,6 +96,13 @@ async function runProductionServer() {
|
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
// Add graceful shutdown handler to prevent requests errors
|
|
100
|
+
await app.register(fastifyGracefulShutdown);
|
|
101
|
+
|
|
102
|
+
if (process.env.ENABLE_K8S_PROBES) {
|
|
103
|
+
await setupK8SProbes(app);
|
|
104
|
+
}
|
|
105
|
+
|
|
96
106
|
// TODO: remove hardcoded port
|
|
97
107
|
await app.listen({
|
|
98
108
|
host: '0.0.0.0',
|