@nerest/nerest 0.0.6 → 0.0.8
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 +11 -1
- package/bin/build.ts +1 -1
- package/bin/index.ts +2 -2
- package/bin/watch.ts +1 -1
- package/build/index.ts +6 -6
- package/client/index.ts +5 -1
- package/dist/bin/build.js +3 -7
- package/dist/bin/index.js +5 -7
- package/dist/bin/watch.js +3 -7
- package/dist/build/excludes/index.js +1 -5
- package/dist/build/index.js +17 -24
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +32 -0
- package/dist/server/development.js +31 -34
- package/dist/server/loaders/assets.js +1 -5
- package/dist/server/loaders/examples.js +10 -17
- package/dist/server/loaders/manifest.js +5 -12
- package/dist/server/loaders/schema.js +7 -14
- package/dist/server/parts/apps.js +15 -22
- package/dist/server/parts/k8s-probes.js +1 -5
- package/dist/server/parts/preview.js +1 -5
- package/dist/server/parts/render.js +6 -13
- package/dist/server/parts/runtime-hook.d.ts +2 -0
- package/dist/server/parts/runtime-hook.js +28 -0
- package/dist/server/parts/swagger.js +8 -15
- package/dist/server/parts/validator.d.ts +1 -1
- package/dist/server/parts/validator.js +9 -12
- package/dist/server/production.d.ts +1 -0
- package/dist/server/production.js +85 -0
- package/package.json +6 -5
- package/server/development.ts +14 -9
- package/server/parts/apps.ts +4 -4
- package/server/parts/runtime-hook.ts +35 -0
- package/server/parts/validator.ts +5 -2
- package/server/production.ts +13 -6
package/README.md
CHANGED
|
@@ -86,7 +86,17 @@ Here `react` and `react-dom` imports will be replaced with accessing the respect
|
|
|
86
86
|
|
|
87
87
|
### Runtime Hooks
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
If the module `nerest-runtime.ts` exists in the root of the micro frontend and exports a default function, this function will be executed when the server starts, and the fastify app instance will be passed to it as its only argument. Example of `nerest-runtime.ts`:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import type { FastifyInstance } from 'fastify';
|
|
93
|
+
|
|
94
|
+
export default function (app: FastifyInstance) {
|
|
95
|
+
console.log('Hello from nerest-runtime.ts');
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This runtime hook can be used to adjust fastify settings, register additional plugins or add custom routes.
|
|
90
100
|
|
|
91
101
|
## Development
|
|
92
102
|
|
package/bin/build.ts
CHANGED
package/bin/index.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// All executions of `nerest <command>` get routed through here
|
|
3
3
|
import 'dotenv/config';
|
|
4
4
|
|
|
5
|
-
import { build } from './build';
|
|
6
|
-
import { watch } from './watch';
|
|
5
|
+
import { build } from './build.js';
|
|
6
|
+
import { watch } from './watch.js';
|
|
7
7
|
|
|
8
8
|
// TODO: add CLI help and manual, maybe use a CLI framework like oclif
|
|
9
9
|
async function cliEntry(args: string[]) {
|
package/bin/watch.ts
CHANGED
package/build/index.ts
CHANGED
|
@@ -2,13 +2,13 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { build } from 'vite';
|
|
6
6
|
import type { InlineConfig } from 'vite';
|
|
7
7
|
import { viteExternalsPlugin } from 'vite-plugin-externals';
|
|
8
8
|
|
|
9
|
-
import { loadApps } from '../server/parts/apps';
|
|
10
|
-
import type { BuildConfiguration } from '../schemas/nerest-build.schema';
|
|
11
|
-
import { excludes } from './excludes';
|
|
9
|
+
import { loadApps } from '../server/parts/apps.js';
|
|
10
|
+
import type { BuildConfiguration } from '../schemas/nerest-build.schema.js';
|
|
11
|
+
import { excludes } from './excludes/index.js';
|
|
12
12
|
|
|
13
13
|
export async function buildMicroFrontend() {
|
|
14
14
|
const root = process.cwd();
|
|
@@ -57,7 +57,7 @@ export async function buildMicroFrontend() {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
console.log('Producing production client build...');
|
|
60
|
-
await
|
|
60
|
+
await build(clientConfig);
|
|
61
61
|
|
|
62
62
|
console.log('Producing Nerest manifest file...');
|
|
63
63
|
await buildAppsManifest(root, staticPath);
|
|
@@ -83,7 +83,7 @@ export async function buildMicroFrontend() {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
console.log('Producing production server build...');
|
|
86
|
-
await
|
|
86
|
+
await build(serverConfig);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
async function buildAppsManifest(root: string, staticPath: string) {
|
package/client/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Shared client entrypoint that bootstraps all of the apps supplied
|
|
2
2
|
// by current microfrontend
|
|
3
3
|
import React from 'react';
|
|
4
|
+
import type { ComponentType } from 'react';
|
|
4
5
|
import ReactDOM from 'react-dom/client';
|
|
5
6
|
|
|
6
7
|
// Since this is a shared entrypoint, it dynamically imports all of the
|
|
@@ -28,7 +29,10 @@ async function runHydration() {
|
|
|
28
29
|
// TODO: more robust error handling and error logging
|
|
29
30
|
const props = JSON.parse(propsContainer?.textContent ?? '{}');
|
|
30
31
|
|
|
31
|
-
const reactElement = React.createElement(
|
|
32
|
+
const reactElement = React.createElement(
|
|
33
|
+
(await appModuleLoader()) as ComponentType,
|
|
34
|
+
props
|
|
35
|
+
);
|
|
32
36
|
ReactDOM.hydrateRoot(container, reactElement);
|
|
33
37
|
}
|
|
34
38
|
}
|
package/dist/bin/build.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.build = void 0;
|
|
4
|
-
const build_1 = require("../build");
|
|
1
|
+
import { buildMicroFrontend } from '../build/index.js';
|
|
5
2
|
// Produce the production build of the Nerest micro frontend
|
|
6
|
-
async function build() {
|
|
3
|
+
export async function build() {
|
|
7
4
|
console.log('Nerest is preparing production build...');
|
|
8
|
-
await
|
|
5
|
+
await buildMicroFrontend();
|
|
9
6
|
console.log('Nerest build is finished');
|
|
10
7
|
}
|
|
11
|
-
exports.build = build;
|
package/dist/bin/index.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
2
|
// All executions of `nerest <command>` get routed through here
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import 'dotenv/config';
|
|
4
|
+
import { build } from './build.js';
|
|
5
|
+
import { watch } from './watch.js';
|
|
8
6
|
// TODO: add CLI help and manual, maybe use a CLI framework like oclif
|
|
9
7
|
async function cliEntry(args) {
|
|
10
8
|
if (args[0] === 'build') {
|
|
11
|
-
await
|
|
9
|
+
await build();
|
|
12
10
|
}
|
|
13
11
|
else if (args[0] === 'watch') {
|
|
14
|
-
await
|
|
12
|
+
await watch();
|
|
15
13
|
}
|
|
16
14
|
}
|
|
17
15
|
// [<path to node>, <path to nerest binary>, ...args]
|
package/dist/bin/watch.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.watch = void 0;
|
|
4
|
-
const development_1 = require("../server/development");
|
|
1
|
+
import { runDevelopmentServer } from '../server/development.js';
|
|
5
2
|
// Start dev server in watch mode, that restarts on file change
|
|
6
3
|
// and rebuilds the client static files
|
|
7
|
-
async function watch() {
|
|
4
|
+
export async function watch() {
|
|
8
5
|
// TODO: will be replaced with nerest logger
|
|
9
6
|
console.log('Starting Nerest watch...');
|
|
10
|
-
await
|
|
7
|
+
await runDevelopmentServer();
|
|
11
8
|
}
|
|
12
|
-
exports.watch = watch;
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.excludes = void 0;
|
|
4
1
|
/**
|
|
5
2
|
* Maps list of imports into a list of rollup aliases that resolve
|
|
6
3
|
* into an empty module.
|
|
7
4
|
*/
|
|
8
|
-
function excludes(list) {
|
|
5
|
+
export function excludes(list) {
|
|
9
6
|
return list?.map((exclude) => ({
|
|
10
7
|
// Excluding '@some/package' should exclude both '@some/package' and
|
|
11
8
|
// '@some/package/...` imports
|
|
@@ -13,7 +10,6 @@ function excludes(list) {
|
|
|
13
10
|
replacement: '@nerest/nerest/build/excludes/empty-module',
|
|
14
11
|
}));
|
|
15
12
|
}
|
|
16
|
-
exports.excludes = excludes;
|
|
17
13
|
/**
|
|
18
14
|
* Escapes string to use inside of a regular expression.
|
|
19
15
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
package/dist/build/index.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const fs_1 = require("fs");
|
|
10
|
-
const vite_1 = __importDefault(require("vite"));
|
|
11
|
-
const vite_plugin_externals_1 = require("vite-plugin-externals");
|
|
12
|
-
const apps_1 = require("../server/parts/apps");
|
|
13
|
-
const excludes_1 = require("./excludes");
|
|
14
|
-
async function buildMicroFrontend() {
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { build } from 'vite';
|
|
5
|
+
import { viteExternalsPlugin } from 'vite-plugin-externals';
|
|
6
|
+
import { loadApps } from '../server/parts/apps.js';
|
|
7
|
+
import { excludes } from './excludes/index.js';
|
|
8
|
+
export async function buildMicroFrontend() {
|
|
15
9
|
const root = process.cwd();
|
|
16
10
|
const staticPath = process.env.NEREST_STATIC_PATH;
|
|
17
11
|
// TODO: The path where the client files are deployed is built-in during
|
|
@@ -44,15 +38,15 @@ async function buildMicroFrontend() {
|
|
|
44
38
|
},
|
|
45
39
|
resolve: {
|
|
46
40
|
// excludes - map buildConfig.excludes packages to an empty module
|
|
47
|
-
alias:
|
|
41
|
+
alias: excludes(buildConfig?.excludes),
|
|
48
42
|
},
|
|
49
43
|
plugins: [
|
|
50
44
|
// externals - map buildConfig.externals packages to a global variable on window
|
|
51
|
-
|
|
45
|
+
viteExternalsPlugin(buildConfig?.externals, { useWindow: false }),
|
|
52
46
|
],
|
|
53
47
|
};
|
|
54
48
|
console.log('Producing production client build...');
|
|
55
|
-
await
|
|
49
|
+
await build(clientConfig);
|
|
56
50
|
console.log('Producing Nerest manifest file...');
|
|
57
51
|
await buildAppsManifest(root, staticPath);
|
|
58
52
|
// Build server using the client manifest
|
|
@@ -75,18 +69,17 @@ async function buildMicroFrontend() {
|
|
|
75
69
|
},
|
|
76
70
|
};
|
|
77
71
|
console.log('Producing production server build...');
|
|
78
|
-
await
|
|
72
|
+
await build(serverConfig);
|
|
79
73
|
}
|
|
80
|
-
exports.buildMicroFrontend = buildMicroFrontend;
|
|
81
74
|
async function buildAppsManifest(root, staticPath) {
|
|
82
|
-
const apps = await
|
|
83
|
-
await
|
|
75
|
+
const apps = await loadApps(root, staticPath);
|
|
76
|
+
await fs.writeFile(path.join(root, 'build/nerest-manifest.json'), JSON.stringify(apps), { encoding: 'utf-8' });
|
|
84
77
|
}
|
|
85
78
|
// TODO: error handling
|
|
86
79
|
async function readBuildConfig(root) {
|
|
87
|
-
const configPath =
|
|
88
|
-
if (
|
|
89
|
-
const content = await
|
|
80
|
+
const configPath = path.join(root, 'nerest-build.json');
|
|
81
|
+
if (existsSync(configPath)) {
|
|
82
|
+
const content = await fs.readFile(configPath, { encoding: 'utf-8' });
|
|
90
83
|
return JSON.parse(content);
|
|
91
84
|
}
|
|
92
85
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Shared client entrypoint that bootstraps all of the apps supplied
|
|
2
|
+
// by current microfrontend
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import ReactDOM from 'react-dom/client';
|
|
5
|
+
// Since this is a shared entrypoint, it dynamically imports all of the
|
|
6
|
+
// available apps. They will be built as separate chunks and only loaded
|
|
7
|
+
// if needed.
|
|
8
|
+
const modules = import.meta.glob('/apps/*/index.tsx', { import: 'default' });
|
|
9
|
+
async function runHydration() {
|
|
10
|
+
for (const container of document.querySelectorAll('div[data-app-name]')) {
|
|
11
|
+
const appName = container.getAttribute('data-app-name');
|
|
12
|
+
const appModuleLoader = modules[`/apps/${appName}/index.tsx`];
|
|
13
|
+
if (!appModuleLoader || container.hasAttribute('data-app-hydrated')) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
// Mark container as hydrated, in case there are multiple instances
|
|
17
|
+
// of the same microfrontend script on the page
|
|
18
|
+
container.setAttribute('data-app-hydrated', 'true');
|
|
19
|
+
const appId = container.getAttribute('data-app-id');
|
|
20
|
+
const propsContainer = document.querySelector(`script[data-app-id="${appId}"]`);
|
|
21
|
+
// TODO: more robust error handling and error logging
|
|
22
|
+
const props = JSON.parse(propsContainer?.textContent ?? '{}');
|
|
23
|
+
const reactElement = React.createElement((await appModuleLoader()), props);
|
|
24
|
+
ReactDOM.hydrateRoot(container, reactElement);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (document.readyState !== 'complete') {
|
|
28
|
+
document.addEventListener('DOMContentLoaded', runHydration);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
runHydration();
|
|
32
|
+
}
|
|
@@ -1,22 +1,18 @@
|
|
|
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.runDevelopmentServer = void 0;
|
|
7
1
|
// This is the nerest development server entrypoint
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { build, createServer } from 'vite';
|
|
4
|
+
import fastify from 'fastify';
|
|
5
|
+
import fastifyStatic from '@fastify/static';
|
|
6
|
+
import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
|
|
7
|
+
import { loadApps } from './parts/apps.js';
|
|
8
|
+
import { renderApp } from './parts/render.js';
|
|
9
|
+
import { renderPreviewPage } from './parts/preview.js';
|
|
10
|
+
import { validator } from './parts/validator.js';
|
|
11
|
+
import { setupSwagger } from './parts/swagger.js';
|
|
12
|
+
import { setupK8SProbes } from './parts/k8s-probes.js';
|
|
13
|
+
import { runRuntimeHook } from './parts/runtime-hook.js';
|
|
14
|
+
// eslint-disable-next-line max-statements
|
|
15
|
+
export async function runDevelopmentServer() {
|
|
20
16
|
const root = process.cwd();
|
|
21
17
|
// TODO: move build config into a separate file
|
|
22
18
|
// TODO: look at @vitejs/plugin-react (everything seems fine without it though)
|
|
@@ -53,15 +49,15 @@ async function runDevelopmentServer() {
|
|
|
53
49
|
await startClientBuildWatcher(config);
|
|
54
50
|
// Load app entries following the `apps/{name}/index.tsx` convention
|
|
55
51
|
// TODO: remove hardcoded port
|
|
56
|
-
const apps = await
|
|
52
|
+
const apps = await loadApps(root, 'http://0.0.0.0:3000/');
|
|
57
53
|
// Start vite server that will be rendering SSR components
|
|
58
|
-
const viteSsr = await
|
|
59
|
-
const app = (
|
|
54
|
+
const viteSsr = await createServer(config);
|
|
55
|
+
const app = fastify();
|
|
60
56
|
// Setup schema validation. We have to use our own ajv instance that
|
|
61
57
|
// we can use both to validate request bodies and examples against
|
|
62
58
|
// app schemas
|
|
63
|
-
app.setValidatorCompiler(({ schema }) =>
|
|
64
|
-
await
|
|
59
|
+
app.setValidatorCompiler(({ schema }) => validator.compile(schema));
|
|
60
|
+
await setupSwagger(app);
|
|
65
61
|
for (const appEntry of Object.values(apps)) {
|
|
66
62
|
const { name, entry, examples, schema } = appEntry;
|
|
67
63
|
const routeOptions = {};
|
|
@@ -86,7 +82,7 @@ async function runDevelopmentServer() {
|
|
|
86
82
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
87
83
|
fixStacktrace: true,
|
|
88
84
|
});
|
|
89
|
-
return
|
|
85
|
+
return renderApp({
|
|
90
86
|
name,
|
|
91
87
|
assets: appEntry.assets,
|
|
92
88
|
component: ssrComponent.default,
|
|
@@ -94,9 +90,9 @@ async function runDevelopmentServer() {
|
|
|
94
90
|
});
|
|
95
91
|
for (const [exampleName, example] of Object.entries(examples)) {
|
|
96
92
|
// Validate example against schema when specified
|
|
97
|
-
if (schema && !
|
|
93
|
+
if (schema && !validator.validate(schema, example)) {
|
|
98
94
|
// TODO: use logger and display errors more prominently
|
|
99
|
-
console.error(`Example "${exampleName}" of app "${name}" does not satisfy schema: ${
|
|
95
|
+
console.error(`Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator.errorsText()}`);
|
|
100
96
|
}
|
|
101
97
|
// GET /api/{name}/examples/{example} -> render a preview page
|
|
102
98
|
// with a predefined example body
|
|
@@ -111,24 +107,24 @@ async function runDevelopmentServer() {
|
|
|
111
107
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
112
108
|
fixStacktrace: true,
|
|
113
109
|
});
|
|
114
|
-
const { html, assets } =
|
|
110
|
+
const { html, assets } = renderApp({
|
|
115
111
|
name,
|
|
116
112
|
assets: appEntry.assets,
|
|
117
113
|
component: ssrComponent.default,
|
|
118
114
|
}, example);
|
|
119
115
|
reply.type('text/html');
|
|
120
|
-
return
|
|
116
|
+
return renderPreviewPage(html, assets);
|
|
121
117
|
});
|
|
122
118
|
}
|
|
123
119
|
}
|
|
124
120
|
// Add graceful shutdown handler to prevent requests errors
|
|
125
|
-
await app.register(
|
|
121
|
+
await app.register(fastifyGracefulShutdown);
|
|
126
122
|
if (process.env.ENABLE_K8S_PROBES) {
|
|
127
|
-
await
|
|
123
|
+
await setupK8SProbes(app);
|
|
128
124
|
}
|
|
129
125
|
// TODO: only do this locally, load from CDN in production
|
|
130
|
-
await app.register(
|
|
131
|
-
root:
|
|
126
|
+
await app.register(fastifyStatic, {
|
|
127
|
+
root: path.join(root, 'build'),
|
|
132
128
|
// TODO: maybe use @fastify/cors instead
|
|
133
129
|
setHeaders(res) {
|
|
134
130
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -136,6 +132,8 @@ async function runDevelopmentServer() {
|
|
|
136
132
|
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, content-type, Authorization');
|
|
137
133
|
},
|
|
138
134
|
});
|
|
135
|
+
// Execute runtime hook in nerest-runtime.ts if it exists
|
|
136
|
+
await runRuntimeHook(app, () => viteSsr.ssrLoadModule('/nerest-runtime.ts'));
|
|
139
137
|
// TODO: remove hardcoded port
|
|
140
138
|
await app.listen({
|
|
141
139
|
host: '0.0.0.0',
|
|
@@ -143,10 +141,9 @@ async function runDevelopmentServer() {
|
|
|
143
141
|
});
|
|
144
142
|
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
145
143
|
}
|
|
146
|
-
exports.runDevelopmentServer = runDevelopmentServer;
|
|
147
144
|
// TODO: this should probably be moved from here
|
|
148
145
|
async function startClientBuildWatcher(config) {
|
|
149
|
-
const watcher = (await
|
|
146
|
+
const watcher = (await build(config));
|
|
150
147
|
return new Promise((resolve) => {
|
|
151
148
|
// We need to have a built manifest.json to provide assets
|
|
152
149
|
// links in SSR. We will wait for rollup to report when it
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.loadAppAssets = void 0;
|
|
4
1
|
// Extracts the list of assets for a given app from the manifest file
|
|
5
|
-
function loadAppAssets(appName, manifest, staticPath) {
|
|
2
|
+
export function loadAppAssets(appName, manifest, staticPath) {
|
|
6
3
|
// TODO: handling errors and potentially missing entries
|
|
7
4
|
// All apps share the same JS entry that dynamically imports the chunks of the apps
|
|
8
5
|
// that are used on the page based on their name
|
|
@@ -11,4 +8,3 @@ function loadAppAssets(appName, manifest, staticPath) {
|
|
|
11
8
|
const appCss = manifest[`apps/${appName}/index.tsx`].css ?? [];
|
|
12
9
|
return [clientEntryJs, ...appCss].map((x) => staticPath + x);
|
|
13
10
|
}
|
|
14
|
-
exports.loadAppAssets = loadAppAssets;
|
|
@@ -1,31 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.loadAppExamples = void 0;
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const fs_1 = require("fs");
|
|
9
|
-
const promises_1 = __importDefault(require("fs/promises"));
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import fs from 'fs/promises';
|
|
10
4
|
// Loads and parses the example json files for providing
|
|
11
5
|
// `/examples/` routes of the dev server
|
|
12
|
-
async function loadAppExamples(appRoot) {
|
|
13
|
-
const examplesRoot =
|
|
6
|
+
export async function loadAppExamples(appRoot) {
|
|
7
|
+
const examplesRoot = path.join(appRoot, 'examples');
|
|
14
8
|
// Examples are optional and may not exist
|
|
15
|
-
if (!
|
|
9
|
+
if (!existsSync(examplesRoot)) {
|
|
16
10
|
return {};
|
|
17
11
|
}
|
|
18
|
-
const exampleFiles = (await
|
|
12
|
+
const exampleFiles = (await fs.readdir(examplesRoot, { withFileTypes: true }))
|
|
19
13
|
.filter((d) => d.isFile() && d.name.endsWith('.json'))
|
|
20
14
|
.map((d) => d.name);
|
|
21
15
|
const examples = {};
|
|
22
16
|
// TODO: error handling and reporting
|
|
23
17
|
for (const filename of exampleFiles) {
|
|
24
|
-
const file =
|
|
25
|
-
const content = await
|
|
18
|
+
const file = path.join(examplesRoot, filename);
|
|
19
|
+
const content = await fs.readFile(file, { encoding: 'utf8' });
|
|
26
20
|
const json = JSON.parse(content);
|
|
27
|
-
examples[
|
|
21
|
+
examples[path.basename(filename, '.json')] = json;
|
|
28
22
|
}
|
|
29
23
|
return examples;
|
|
30
24
|
}
|
|
31
|
-
exports.loadAppExamples = loadAppExamples;
|
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.loadAppManifest = void 0;
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const promises_1 = __importDefault(require("fs/promises"));
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
9
3
|
// Manifest is used to provide assets list for every app
|
|
10
4
|
// for use with SSR
|
|
11
|
-
async function loadAppManifest(root) {
|
|
5
|
+
export async function loadAppManifest(root) {
|
|
12
6
|
// TODO: error handling
|
|
13
|
-
const manifestPath =
|
|
14
|
-
const manifestData = await
|
|
7
|
+
const manifestPath = path.join(root, 'build', 'manifest.json');
|
|
8
|
+
const manifestData = await fs.readFile(manifestPath, { encoding: 'utf8' });
|
|
15
9
|
return JSON.parse(manifestData);
|
|
16
10
|
}
|
|
17
|
-
exports.loadAppManifest = loadAppManifest;
|
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.loadAppSchema = void 0;
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const fs_1 = require("fs");
|
|
9
|
-
const promises_1 = __importDefault(require("fs/promises"));
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import fs from 'fs/promises';
|
|
10
4
|
// Loads and parses the schema file for a specific app
|
|
11
|
-
async function loadAppSchema(appRoot) {
|
|
12
|
-
const schemaPath =
|
|
5
|
+
export async function loadAppSchema(appRoot) {
|
|
6
|
+
const schemaPath = path.join(appRoot, 'schema.json');
|
|
13
7
|
let schema = null;
|
|
14
8
|
// TODO: error handling and reporting
|
|
15
|
-
if (
|
|
16
|
-
const file = await
|
|
9
|
+
if (existsSync(schemaPath)) {
|
|
10
|
+
const file = await fs.readFile(schemaPath, { encoding: 'utf-8' });
|
|
17
11
|
schema = JSON.parse(file);
|
|
18
12
|
}
|
|
19
13
|
return schema;
|
|
20
14
|
}
|
|
21
|
-
exports.loadAppSchema = loadAppSchema;
|
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
-
const assets_1 = require("../loaders/assets");
|
|
10
|
-
const examples_1 = require("../loaders/examples");
|
|
11
|
-
const schema_1 = require("../loaders/schema");
|
|
12
|
-
const manifest_1 = require("../loaders/manifest");
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { loadAppAssets } from '../loaders/assets.js';
|
|
4
|
+
import { loadAppExamples } from '../loaders/examples.js';
|
|
5
|
+
import { loadAppSchema } from '../loaders/schema.js';
|
|
6
|
+
import { loadAppManifest } from '../loaders/manifest.js';
|
|
13
7
|
// Build the record of the available apps by convention
|
|
14
8
|
// apps -> /apps/{name}/index.tsx
|
|
15
9
|
// examples -> /apps/{name}/examples/{example}.json
|
|
16
|
-
async function loadApps(root, deployedStaticPath) {
|
|
17
|
-
const appsRoot =
|
|
18
|
-
const manifest = await
|
|
19
|
-
const appsDirs = (await
|
|
10
|
+
export async function loadApps(root, deployedStaticPath) {
|
|
11
|
+
const appsRoot = path.join(root, 'apps');
|
|
12
|
+
const manifest = await loadAppManifest(root);
|
|
13
|
+
const appsDirs = (await fs.readdir(appsRoot, { withFileTypes: true }))
|
|
20
14
|
.filter((d) => d.isDirectory())
|
|
21
15
|
.map((d) => d.name);
|
|
22
16
|
const apps = [];
|
|
@@ -25,19 +19,18 @@ async function loadApps(root, deployedStaticPath) {
|
|
|
25
19
|
}
|
|
26
20
|
return Object.fromEntries(apps);
|
|
27
21
|
}
|
|
28
|
-
exports.loadApps = loadApps;
|
|
29
22
|
async function loadApp(appsRoot, name, manifest, deployedStaticPath) {
|
|
30
23
|
// TODO: report problems with loading entries, assets and/or examples
|
|
31
|
-
const appRoot =
|
|
24
|
+
const appRoot = path.join(appsRoot, name);
|
|
32
25
|
return [
|
|
33
26
|
name,
|
|
34
27
|
{
|
|
35
28
|
name,
|
|
36
29
|
root: appRoot,
|
|
37
|
-
entry:
|
|
38
|
-
assets:
|
|
39
|
-
examples: await
|
|
40
|
-
schema: await
|
|
30
|
+
entry: path.join(appRoot, 'index.tsx'),
|
|
31
|
+
assets: loadAppAssets(name, manifest, deployedStaticPath),
|
|
32
|
+
examples: await loadAppExamples(appRoot),
|
|
33
|
+
schema: await loadAppSchema(appRoot),
|
|
41
34
|
},
|
|
42
35
|
];
|
|
43
36
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setupK8SProbes = void 0;
|
|
4
1
|
// Setup routes for k8s probes to check if application is live
|
|
5
|
-
async function setupK8SProbes(app) {
|
|
2
|
+
export async function setupK8SProbes(app) {
|
|
6
3
|
// Handler for graceful shutdowns
|
|
7
4
|
// K8s can initiate shutdown at any moment: on pods restart or on deploy.
|
|
8
5
|
// So, if we receive shutdown request, we:
|
|
@@ -28,4 +25,3 @@ async function setupK8SProbes(app) {
|
|
|
28
25
|
}
|
|
29
26
|
});
|
|
30
27
|
}
|
|
31
|
-
exports.setupK8SProbes = setupK8SProbes;
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.renderPreviewPage = void 0;
|
|
4
1
|
// Renders the preview page available by convention at /api/{name}/examples/{example}
|
|
5
|
-
function renderPreviewPage(html, assets) {
|
|
2
|
+
export function renderPreviewPage(html, assets) {
|
|
6
3
|
const { scripts, styles } = mapAssets(assets);
|
|
7
4
|
return `
|
|
8
5
|
<html>
|
|
@@ -16,7 +13,6 @@ function renderPreviewPage(html, assets) {
|
|
|
16
13
|
</html>
|
|
17
14
|
`;
|
|
18
15
|
}
|
|
19
|
-
exports.renderPreviewPage = renderPreviewPage;
|
|
20
16
|
function mapAssets(assets) {
|
|
21
17
|
// TODO: script type="module" is not supported by older browsers
|
|
22
18
|
// but vite doesn't provide `nomodule` fallback by default
|
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.renderApp = void 0;
|
|
7
|
-
const react_1 = __importDefault(require("react"));
|
|
8
|
-
const server_1 = require("react-dom/server");
|
|
9
|
-
const nanoid_1 = require("nanoid");
|
|
10
|
-
function renderApp({ name, assets, component }, props = {}) {
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderToString } from 'react-dom/server';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
export function renderApp({ name, assets, component }, props = {}) {
|
|
11
5
|
const html = renderSsrComponent(name, component, props);
|
|
12
6
|
return { html, assets };
|
|
13
7
|
}
|
|
14
|
-
exports.renderApp = renderApp;
|
|
15
8
|
function renderSsrComponent(appName, appComponent, props) {
|
|
16
|
-
const html =
|
|
9
|
+
const html = renderToString(React.createElement(appComponent, props));
|
|
17
10
|
// There may be multiple instances of the same app on the page,
|
|
18
11
|
// so we will use a randomized id to avoid collisions
|
|
19
|
-
const appId =
|
|
12
|
+
const appId = nanoid();
|
|
20
13
|
// data-app-name and data-app-id are used by client entrypoint to hydrate
|
|
21
14
|
// apps using correct serialized props
|
|
22
15
|
const container = `<div data-app-name="${appName}" data-app-id="${appId}">${html}</div>`;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Load the runtime hook module and run it if it exists, passing down our
|
|
2
|
+
// fastify instance. This hook can be used to modify fastify settings, add
|
|
3
|
+
// plugins or routes on an individual app level.
|
|
4
|
+
export async function runRuntimeHook(app, loader) {
|
|
5
|
+
let module;
|
|
6
|
+
try {
|
|
7
|
+
module = (await loader());
|
|
8
|
+
}
|
|
9
|
+
catch { }
|
|
10
|
+
if (typeof module?.default === 'function') {
|
|
11
|
+
// If module exists and exports a default function, execute it and
|
|
12
|
+
// pass down the fastify instance
|
|
13
|
+
try {
|
|
14
|
+
await module.default(app);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
console.error('Failed to execute runtime hook', e);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (module) {
|
|
22
|
+
console.error("Runtime hook found, but doesn't export default function!");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log('Runtime hook not found, skipping...');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.setupSwagger = void 0;
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const fs_1 = __importDefault(require("fs"));
|
|
9
|
-
const swagger_1 = __importDefault(require("@fastify/swagger"));
|
|
10
|
-
const swagger_ui_1 = __importDefault(require("@fastify/swagger-ui"));
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import fastifySwagger from '@fastify/swagger';
|
|
4
|
+
import fastifySwaggerUi from '@fastify/swagger-ui';
|
|
11
5
|
// Setup automatic OpenAPI specification compilation and enable
|
|
12
6
|
// Swagger UI at the `/api` route
|
|
13
|
-
async function setupSwagger(app) {
|
|
7
|
+
export async function setupSwagger(app) {
|
|
14
8
|
let appInfo = {};
|
|
15
9
|
try {
|
|
16
|
-
const packageJson =
|
|
10
|
+
const packageJson = fs.readFileSync(path.join(process.cwd(), 'package.json'), { encoding: 'utf-8' });
|
|
17
11
|
appInfo = JSON.parse(packageJson);
|
|
18
12
|
}
|
|
19
13
|
catch (e) {
|
|
@@ -24,7 +18,7 @@ async function setupSwagger(app) {
|
|
|
24
18
|
(typeof appInfo.repository === 'string'
|
|
25
19
|
? appInfo.repository
|
|
26
20
|
: appInfo.repository?.url);
|
|
27
|
-
await app.register(
|
|
21
|
+
await app.register(fastifySwagger, {
|
|
28
22
|
openapi: {
|
|
29
23
|
info: {
|
|
30
24
|
title: appInfo.name ?? 'Nerest micro frontend',
|
|
@@ -43,8 +37,7 @@ async function setupSwagger(app) {
|
|
|
43
37
|
},
|
|
44
38
|
},
|
|
45
39
|
});
|
|
46
|
-
await app.register(
|
|
40
|
+
await app.register(fastifySwaggerUi, {
|
|
47
41
|
routePrefix: '/api',
|
|
48
42
|
});
|
|
49
43
|
}
|
|
50
|
-
exports.setupSwagger = setupSwagger;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import Ajv from 'ajv';
|
|
2
|
-
export declare const validator: Ajv;
|
|
2
|
+
export declare const validator: Ajv.default;
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const fast_uri_1 = __importDefault(require("fast-uri"));
|
|
9
|
-
const ajv_formats_1 = __importDefault(require("ajv-formats"));
|
|
10
|
-
exports.validator = new ajv_1.default({
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import fastUri from 'fast-uri';
|
|
3
|
+
import addFormats from 'ajv-formats';
|
|
4
|
+
// Ajv default export is broken, so we have to specify `.default`
|
|
5
|
+
// manually: https://github.com/ajv-validator/ajv/issues/2132
|
|
6
|
+
// eslint-disable-next-line new-cap
|
|
7
|
+
export const validator = new Ajv.default({
|
|
11
8
|
coerceTypes: 'array',
|
|
12
9
|
useDefaults: true,
|
|
13
10
|
removeAdditional: true,
|
|
14
|
-
uriResolver:
|
|
11
|
+
uriResolver: fastUri,
|
|
15
12
|
addUsedSchema: false,
|
|
16
13
|
// Explicitly set allErrors to `false`.
|
|
17
14
|
// When set to `true`, a DoS attack is possible.
|
|
@@ -20,4 +17,4 @@ exports.validator = new ajv_1.default({
|
|
|
20
17
|
// Support additional type formats in JSON schema like `date`,
|
|
21
18
|
// `email`, `url`, etc. Used by default in fastify
|
|
22
19
|
// https://www.npmjs.com/package/ajv-formats
|
|
23
|
-
|
|
20
|
+
addFormats.default(validator);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// This is the nerest production server entrypoint
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import fastify from 'fastify';
|
|
5
|
+
import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
|
|
6
|
+
import { renderApp } from './parts/render.js';
|
|
7
|
+
import { setupSwagger } from './parts/swagger.js';
|
|
8
|
+
import { validator } from './parts/validator.js';
|
|
9
|
+
import { renderPreviewPage } from './parts/preview.js';
|
|
10
|
+
import { setupK8SProbes } from './parts/k8s-probes.js';
|
|
11
|
+
import { runRuntimeHook } from './parts/runtime-hook.js';
|
|
12
|
+
// TODO: refactor to merge the similar parts between production and development server?
|
|
13
|
+
async function runProductionServer() {
|
|
14
|
+
const root = process.cwd();
|
|
15
|
+
// TODO: error handling for file reading
|
|
16
|
+
const apps = JSON.parse(await fs.readFile(path.join(root, 'build/nerest-manifest.json'), {
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
}));
|
|
19
|
+
// TODO: fix client-side vite types
|
|
20
|
+
const components = import.meta.glob('/apps/*/index.tsx', {
|
|
21
|
+
import: 'default',
|
|
22
|
+
eager: true,
|
|
23
|
+
});
|
|
24
|
+
const app = fastify();
|
|
25
|
+
// Setup schema validation. We have to use our own ajv instance that
|
|
26
|
+
// we can use both to validate request bodies and examples against
|
|
27
|
+
// app schemas
|
|
28
|
+
app.setValidatorCompiler(({ schema }) => validator.compile(schema));
|
|
29
|
+
await setupSwagger(app);
|
|
30
|
+
for (const appEntry of Object.values(apps)) {
|
|
31
|
+
const { name, examples, schema, assets } = appEntry;
|
|
32
|
+
const component = components[`/apps/${name}/index.tsx`];
|
|
33
|
+
const routeOptions = {};
|
|
34
|
+
// TODO: report error if schema is missing, unless this app is client-only
|
|
35
|
+
// TODO: disallow apps without schemas in production build
|
|
36
|
+
if (schema) {
|
|
37
|
+
routeOptions.schema = {
|
|
38
|
+
// Use description as Swagger summary, since summary is visible
|
|
39
|
+
// even when the route is collapsed in the UI
|
|
40
|
+
summary: schema.description,
|
|
41
|
+
// TODO: do we need to mix in examples like in the development server?
|
|
42
|
+
body: schema,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// POST /api/{name} -> render app with request.body as props
|
|
46
|
+
app.post(`/api/${name}`, routeOptions, (request) => renderApp({ name, assets, component }, request.body));
|
|
47
|
+
for (const [exampleName, example] of Object.entries(examples)) {
|
|
48
|
+
// GET /api/{name}/examples/{example} -> render a preview page
|
|
49
|
+
// with a predefined example body
|
|
50
|
+
const exampleRoute = `/api/${name}/examples/${exampleName}`;
|
|
51
|
+
app.get(exampleRoute, {
|
|
52
|
+
schema: {
|
|
53
|
+
// Add a clickable link to the example route in route's Swagger
|
|
54
|
+
// description so it's easier to navigate to
|
|
55
|
+
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
56
|
+
},
|
|
57
|
+
}, async (_, reply) => {
|
|
58
|
+
const { html, assets: outAssets } = renderApp({
|
|
59
|
+
name,
|
|
60
|
+
assets,
|
|
61
|
+
component,
|
|
62
|
+
}, example);
|
|
63
|
+
reply.type('text/html');
|
|
64
|
+
return renderPreviewPage(html, outAssets);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Add graceful shutdown handler to prevent requests errors
|
|
69
|
+
await app.register(fastifyGracefulShutdown);
|
|
70
|
+
if (process.env.ENABLE_K8S_PROBES) {
|
|
71
|
+
await setupK8SProbes(app);
|
|
72
|
+
}
|
|
73
|
+
// Execute runtime hook in nerest-runtime.ts if it exists
|
|
74
|
+
await runRuntimeHook(app, async () => {
|
|
75
|
+
const glob = import.meta.glob('/nerest-runtime.ts', { eager: true });
|
|
76
|
+
return glob['/nerest-runtime.ts'];
|
|
77
|
+
});
|
|
78
|
+
// TODO: remove hardcoded port
|
|
79
|
+
await app.listen({
|
|
80
|
+
host: '0.0.0.0',
|
|
81
|
+
port: 3000,
|
|
82
|
+
});
|
|
83
|
+
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
84
|
+
}
|
|
85
|
+
runProductionServer();
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerest/nerest",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "React micro frontend framework",
|
|
5
5
|
"homepage": "https://github.com/nerestjs/nerest#readme",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/nerestjs/nerest.git"
|
|
9
9
|
},
|
|
10
|
+
"type": "module",
|
|
10
11
|
"bin": {
|
|
11
12
|
"nerest": "dist/bin/index.js"
|
|
12
13
|
},
|
|
@@ -52,16 +53,16 @@
|
|
|
52
53
|
]
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@fastify/static": "^6.
|
|
56
|
+
"@fastify/static": "^6.12.0",
|
|
56
57
|
"@fastify/swagger": "^8.12.0",
|
|
57
58
|
"@fastify/swagger-ui": "^1.10.1",
|
|
58
59
|
"ajv": "^8.12.0",
|
|
59
60
|
"ajv-formats": "^2.1.1",
|
|
60
61
|
"dotenv": "^16.3.1",
|
|
61
|
-
"fast-uri": "^2.
|
|
62
|
+
"fast-uri": "^2.3.0",
|
|
62
63
|
"fastify": "^4.24.3",
|
|
63
64
|
"fastify-graceful-shutdown": "^3.5.1",
|
|
64
|
-
"nanoid": "^
|
|
65
|
+
"nanoid": "^5.0.2",
|
|
65
66
|
"vite": "^4.5.0",
|
|
66
67
|
"vite-plugin-externals": "^0.6.2"
|
|
67
68
|
},
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
"@tinkoff/eslint-config": "^1.54.4",
|
|
70
71
|
"@tinkoff/eslint-config-react": "^1.54.4",
|
|
71
72
|
"@tinkoff/prettier-config": "^1.52.1",
|
|
72
|
-
"@types/react": "^18.2.
|
|
73
|
+
"@types/react": "^18.2.34",
|
|
73
74
|
"@types/react-dom": "^18.2.14",
|
|
74
75
|
"jest": "^29.7.0",
|
|
75
76
|
"json-schema-to-typescript": "^13.1.1",
|
package/server/development.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import type { ServerResponse } from 'http';
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { build, createServer } from 'vite';
|
|
6
6
|
import type { InlineConfig } from 'vite';
|
|
7
7
|
import type { RollupWatcher, RollupWatcherEvent } from 'rollup';
|
|
8
8
|
|
|
@@ -11,13 +11,15 @@ import fastify from 'fastify';
|
|
|
11
11
|
import fastifyStatic from '@fastify/static';
|
|
12
12
|
import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
|
|
13
13
|
|
|
14
|
-
import { loadApps } from './parts/apps';
|
|
15
|
-
import { renderApp } from './parts/render';
|
|
16
|
-
import { renderPreviewPage } from './parts/preview';
|
|
17
|
-
import { validator } from './parts/validator';
|
|
18
|
-
import { setupSwagger } from './parts/swagger';
|
|
19
|
-
import { setupK8SProbes } from './parts/k8s-probes';
|
|
14
|
+
import { loadApps } from './parts/apps.js';
|
|
15
|
+
import { renderApp } from './parts/render.js';
|
|
16
|
+
import { renderPreviewPage } from './parts/preview.js';
|
|
17
|
+
import { validator } from './parts/validator.js';
|
|
18
|
+
import { setupSwagger } from './parts/swagger.js';
|
|
19
|
+
import { setupK8SProbes } from './parts/k8s-probes.js';
|
|
20
|
+
import { runRuntimeHook } from './parts/runtime-hook.js';
|
|
20
21
|
|
|
22
|
+
// eslint-disable-next-line max-statements
|
|
21
23
|
export async function runDevelopmentServer() {
|
|
22
24
|
const root = process.cwd();
|
|
23
25
|
|
|
@@ -61,7 +63,7 @@ export async function runDevelopmentServer() {
|
|
|
61
63
|
const apps = await loadApps(root, 'http://0.0.0.0:3000/');
|
|
62
64
|
|
|
63
65
|
// Start vite server that will be rendering SSR components
|
|
64
|
-
const viteSsr = await
|
|
66
|
+
const viteSsr = await createServer(config);
|
|
65
67
|
const app = fastify();
|
|
66
68
|
|
|
67
69
|
// Setup schema validation. We have to use our own ajv instance that
|
|
@@ -171,6 +173,9 @@ export async function runDevelopmentServer() {
|
|
|
171
173
|
},
|
|
172
174
|
});
|
|
173
175
|
|
|
176
|
+
// Execute runtime hook in nerest-runtime.ts if it exists
|
|
177
|
+
await runRuntimeHook(app, () => viteSsr.ssrLoadModule('/nerest-runtime.ts'));
|
|
178
|
+
|
|
174
179
|
// TODO: remove hardcoded port
|
|
175
180
|
await app.listen({
|
|
176
181
|
host: '0.0.0.0',
|
|
@@ -182,7 +187,7 @@ export async function runDevelopmentServer() {
|
|
|
182
187
|
|
|
183
188
|
// TODO: this should probably be moved from here
|
|
184
189
|
async function startClientBuildWatcher(config: InlineConfig) {
|
|
185
|
-
const watcher = (await
|
|
190
|
+
const watcher = (await build(config)) as RollupWatcher;
|
|
186
191
|
return new Promise<void>((resolve) => {
|
|
187
192
|
// We need to have a built manifest.json to provide assets
|
|
188
193
|
// links in SSR. We will wait for rollup to report when it
|
package/server/parts/apps.ts
CHANGED
|
@@ -2,10 +2,10 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import type { Manifest } from 'vite';
|
|
4
4
|
|
|
5
|
-
import { loadAppAssets } from '../loaders/assets';
|
|
6
|
-
import { loadAppExamples } from '../loaders/examples';
|
|
7
|
-
import { loadAppSchema } from '../loaders/schema';
|
|
8
|
-
import { loadAppManifest } from '../loaders/manifest';
|
|
5
|
+
import { loadAppAssets } from '../loaders/assets.js';
|
|
6
|
+
import { loadAppExamples } from '../loaders/examples.js';
|
|
7
|
+
import { loadAppSchema } from '../loaders/schema.js';
|
|
8
|
+
import { loadAppManifest } from '../loaders/manifest.js';
|
|
9
9
|
|
|
10
10
|
export type AppEntry = {
|
|
11
11
|
name: string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
|
|
3
|
+
type RuntimeHookModule = {
|
|
4
|
+
default: (app: FastifyInstance) => unknown;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// Load the runtime hook module and run it if it exists, passing down our
|
|
8
|
+
// fastify instance. This hook can be used to modify fastify settings, add
|
|
9
|
+
// plugins or routes on an individual app level.
|
|
10
|
+
export async function runRuntimeHook(
|
|
11
|
+
app: FastifyInstance,
|
|
12
|
+
loader: () => Promise<unknown>
|
|
13
|
+
) {
|
|
14
|
+
let module: RuntimeHookModule | undefined;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
module = (await loader()) as RuntimeHookModule;
|
|
18
|
+
} catch {}
|
|
19
|
+
|
|
20
|
+
if (typeof module?.default === 'function') {
|
|
21
|
+
// If module exists and exports a default function, execute it and
|
|
22
|
+
// pass down the fastify instance
|
|
23
|
+
try {
|
|
24
|
+
await module.default(app);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error('Failed to execute runtime hook', e);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
} else if (module) {
|
|
30
|
+
console.error("Runtime hook found, but doesn't export default function!");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
} else {
|
|
33
|
+
console.log('Runtime hook not found, skipping...');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -2,7 +2,10 @@ import Ajv from 'ajv';
|
|
|
2
2
|
import fastUri from 'fast-uri';
|
|
3
3
|
import addFormats from 'ajv-formats';
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
// Ajv default export is broken, so we have to specify `.default`
|
|
6
|
+
// manually: https://github.com/ajv-validator/ajv/issues/2132
|
|
7
|
+
// eslint-disable-next-line new-cap
|
|
8
|
+
export const validator = new Ajv.default({
|
|
6
9
|
coerceTypes: 'array',
|
|
7
10
|
useDefaults: true,
|
|
8
11
|
removeAdditional: true,
|
|
@@ -16,4 +19,4 @@ export const validator = new Ajv({
|
|
|
16
19
|
// Support additional type formats in JSON schema like `date`,
|
|
17
20
|
// `email`, `url`, etc. Used by default in fastify
|
|
18
21
|
// https://www.npmjs.com/package/ajv-formats
|
|
19
|
-
addFormats(validator);
|
|
22
|
+
addFormats.default(validator);
|
package/server/production.ts
CHANGED
|
@@ -6,12 +6,13 @@ import fastify from 'fastify';
|
|
|
6
6
|
import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
|
|
7
7
|
import type { RouteShorthandOptions } from 'fastify';
|
|
8
8
|
|
|
9
|
-
import type { AppEntry } from './parts/apps';
|
|
10
|
-
import { renderApp } from './parts/render';
|
|
11
|
-
import { setupSwagger } from './parts/swagger';
|
|
12
|
-
import { validator } from './parts/validator';
|
|
13
|
-
import { renderPreviewPage } from './parts/preview';
|
|
14
|
-
import { setupK8SProbes } from './parts/k8s-probes';
|
|
9
|
+
import type { AppEntry } from './parts/apps.js';
|
|
10
|
+
import { renderApp } from './parts/render.js';
|
|
11
|
+
import { setupSwagger } from './parts/swagger.js';
|
|
12
|
+
import { validator } from './parts/validator.js';
|
|
13
|
+
import { renderPreviewPage } from './parts/preview.js';
|
|
14
|
+
import { setupK8SProbes } from './parts/k8s-probes.js';
|
|
15
|
+
import { runRuntimeHook } from './parts/runtime-hook.js';
|
|
15
16
|
|
|
16
17
|
// TODO: refactor to merge the similar parts between production and development server?
|
|
17
18
|
async function runProductionServer() {
|
|
@@ -103,6 +104,12 @@ async function runProductionServer() {
|
|
|
103
104
|
await setupK8SProbes(app);
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
// Execute runtime hook in nerest-runtime.ts if it exists
|
|
108
|
+
await runRuntimeHook(app, async () => {
|
|
109
|
+
const glob = import.meta.glob('/nerest-runtime.ts', { eager: true });
|
|
110
|
+
return glob['/nerest-runtime.ts'];
|
|
111
|
+
});
|
|
112
|
+
|
|
106
113
|
// TODO: remove hardcoded port
|
|
107
114
|
await app.listen({
|
|
108
115
|
host: '0.0.0.0',
|