@nerest/nerest 0.0.8 → 0.1.0
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 +36 -1
- package/dist/server/development.js +15 -7
- package/dist/server/hooks/logger.d.ts +2 -0
- package/dist/server/hooks/logger.js +21 -0
- package/dist/server/hooks/props.d.ts +2 -0
- package/dist/server/hooks/props.js +14 -0
- package/dist/server/hooks/runtime.d.ts +2 -0
- package/dist/server/hooks/runtime.js +28 -0
- package/dist/server/parts/apps.d.ts +1 -0
- package/dist/server/parts/apps.js +1 -0
- package/dist/server/parts/props-hook.d.ts +1 -0
- package/dist/server/parts/props-hook.js +8 -0
- package/dist/server/production.js +22 -11
- package/package.json +1 -1
- package/server/development.ts +23 -8
- package/server/hooks/logger.ts +28 -0
- package/server/hooks/props.ts +27 -0
- package/server/parts/apps.ts +2 -0
- package/server/production.ts +25 -17
- /package/server/{parts/runtime-hook.ts → hooks/runtime.ts} +0 -0
package/README.md
CHANGED
|
@@ -37,6 +37,25 @@ The app directory should contain a `schema.json` file that describes the schema
|
|
|
37
37
|
|
|
38
38
|
OpenAPI specification is compiled automatically based on the provided schemas and becomes available at `/api/json`. It can also be explored through Swagger UI that becomes available at `/api`.
|
|
39
39
|
|
|
40
|
+
### Props Hook (`/props.ts`)
|
|
41
|
+
|
|
42
|
+
The app directory may contain a `props.ts` module that exports a default function. If it exists, this function will be executed on the server for every incoming request, receiving the body of the request and a logger as parameters. You can use this hook to modify and return a new object, which will be passed down to the `index.tsx` entrypoint component instead. For example:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import type { FastifyBaseLogger } from 'fastify';
|
|
46
|
+
|
|
47
|
+
export default function (props: Props, logger: FastifyBaseLogger) {
|
|
48
|
+
logger.info('Hello from props.ts!');
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...props,
|
|
52
|
+
greeting: `${props.greeting} (modified in props.ts)`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The exported function may be async, in which case nerest will wait for the Promise to resolve, then pass the result object to the entrypoint component.
|
|
58
|
+
|
|
40
59
|
## Configuration
|
|
41
60
|
|
|
42
61
|
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.
|
|
@@ -84,7 +103,7 @@ Excludes modules from the client build and maps them to globally available const
|
|
|
84
103
|
|
|
85
104
|
Here `react` and `react-dom` imports will be replaced with accessing the respective `window` constants.
|
|
86
105
|
|
|
87
|
-
### Runtime
|
|
106
|
+
### Runtime Hook
|
|
88
107
|
|
|
89
108
|
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
109
|
|
|
@@ -98,6 +117,22 @@ export default function (app: FastifyInstance) {
|
|
|
98
117
|
|
|
99
118
|
This runtime hook can be used to adjust fastify settings, register additional plugins or add custom routes.
|
|
100
119
|
|
|
120
|
+
#### Logger Configuration
|
|
121
|
+
|
|
122
|
+
Nerest uses the default server-side [fastify logger](https://fastify.dev/docs/latest/Reference/Logging/#logging), enabled by default. To configure or disable it, export a `logger` function from the `nerest-runtime.ts` module. It will be called on server start, and the return value will be passed to fastify as logger configuration.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import type { FastifyServerOptions } from 'fastify';
|
|
126
|
+
|
|
127
|
+
export function logger(): FastifyServerOptions['logger'] {
|
|
128
|
+
return { prettyPrint: true };
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
To disable the logger, return `false` from the function.
|
|
133
|
+
|
|
134
|
+
You can also supply your own logger instance. Instead of passing configuration options, pass the instance. The logger you supply must conform to the [Pino](https://github.com/pinojs/pino) interface; that is, it must have the following methods: `info`, `error`, `debug`, `fatal`, `warn`, `trace`, `silent`, `child` and a string property `level`.
|
|
135
|
+
|
|
101
136
|
## Development
|
|
102
137
|
|
|
103
138
|
Run the build script to build the framework.
|
|
@@ -10,7 +10,9 @@ import { renderPreviewPage } from './parts/preview.js';
|
|
|
10
10
|
import { validator } from './parts/validator.js';
|
|
11
11
|
import { setupSwagger } from './parts/swagger.js';
|
|
12
12
|
import { setupK8SProbes } from './parts/k8s-probes.js';
|
|
13
|
-
import { runRuntimeHook } from './
|
|
13
|
+
import { runRuntimeHook } from './hooks/runtime.js';
|
|
14
|
+
import { runPropsHook } from './hooks/props.js';
|
|
15
|
+
import { runLoggerHook } from './hooks/logger.js';
|
|
14
16
|
// eslint-disable-next-line max-statements
|
|
15
17
|
export async function runDevelopmentServer() {
|
|
16
18
|
const root = process.cwd();
|
|
@@ -52,14 +54,19 @@ export async function runDevelopmentServer() {
|
|
|
52
54
|
const apps = await loadApps(root, 'http://0.0.0.0:3000/');
|
|
53
55
|
// Start vite server that will be rendering SSR components
|
|
54
56
|
const viteSsr = await createServer(config);
|
|
55
|
-
|
|
57
|
+
// Start fastify server that will be processing HTTP requests
|
|
58
|
+
const app = fastify({
|
|
59
|
+
// Receive logger configuration from `nerest-runtime.logger()` or
|
|
60
|
+
// use the default fastify one (`true`)
|
|
61
|
+
logger: (await runLoggerHook(() => viteSsr.ssrLoadModule('/nerest-runtime.ts'))) ?? true,
|
|
62
|
+
});
|
|
56
63
|
// Setup schema validation. We have to use our own ajv instance that
|
|
57
64
|
// we can use both to validate request bodies and examples against
|
|
58
65
|
// app schemas
|
|
59
66
|
app.setValidatorCompiler(({ schema }) => validator.compile(schema));
|
|
60
67
|
await setupSwagger(app);
|
|
61
68
|
for (const appEntry of Object.values(apps)) {
|
|
62
|
-
const { name, entry, examples, schema } = appEntry;
|
|
69
|
+
const { name, entry, propsHookEntry, examples, schema } = appEntry;
|
|
63
70
|
const routeOptions = {};
|
|
64
71
|
// TODO: report error if schema is missing, unless this app is client-only
|
|
65
72
|
if (schema) {
|
|
@@ -82,11 +89,12 @@ export async function runDevelopmentServer() {
|
|
|
82
89
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
83
90
|
fixStacktrace: true,
|
|
84
91
|
});
|
|
92
|
+
const props = await runPropsHook(request.body, request.log, () => viteSsr.ssrLoadModule(propsHookEntry));
|
|
85
93
|
return renderApp({
|
|
86
94
|
name,
|
|
87
95
|
assets: appEntry.assets,
|
|
88
96
|
component: ssrComponent.default,
|
|
89
|
-
},
|
|
97
|
+
}, props);
|
|
90
98
|
});
|
|
91
99
|
for (const [exampleName, example] of Object.entries(examples)) {
|
|
92
100
|
// Validate example against schema when specified
|
|
@@ -103,15 +111,16 @@ export async function runDevelopmentServer() {
|
|
|
103
111
|
// description so it's easier to navigate to
|
|
104
112
|
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
105
113
|
},
|
|
106
|
-
}, async (
|
|
114
|
+
}, async (request, reply) => {
|
|
107
115
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
108
116
|
fixStacktrace: true,
|
|
109
117
|
});
|
|
118
|
+
const props = await runPropsHook(example, request.log, () => viteSsr.ssrLoadModule(propsHookEntry));
|
|
110
119
|
const { html, assets } = renderApp({
|
|
111
120
|
name,
|
|
112
121
|
assets: appEntry.assets,
|
|
113
122
|
component: ssrComponent.default,
|
|
114
|
-
},
|
|
123
|
+
}, props);
|
|
115
124
|
reply.type('text/html');
|
|
116
125
|
return renderPreviewPage(html, assets);
|
|
117
126
|
});
|
|
@@ -139,7 +148,6 @@ export async function runDevelopmentServer() {
|
|
|
139
148
|
host: '0.0.0.0',
|
|
140
149
|
port: 3000,
|
|
141
150
|
});
|
|
142
|
-
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
143
151
|
}
|
|
144
152
|
// TODO: this should probably be moved from here
|
|
145
153
|
async function startClientBuildWatcher(config) {
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
export declare function runLoggerHook(loader: () => Promise<unknown>): Promise<boolean | import("fastify").FastifyBaseLogger | (import("fastify").FastifyLoggerOptions<import("fastify").RawServerDefault, import("fastify").FastifyRequest<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown, import("fastify").FastifyBaseLogger, import("fastify/types/type-provider.js").ResolveFastifyRequestType<import("fastify").FastifyTypeProviderDefault, import("fastify").FastifySchema, import("fastify").RouteGenericInterface>>, import("fastify").FastifyReply<import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, import("fastify").RouteGenericInterface, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>> & import("pino").default.LoggerOptions) | null | undefined>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Load the runtime hook module and run it if it exists, to receive
|
|
2
|
+
// logger configuration. If the `logger` function does not exist
|
|
3
|
+
// in the module, ignore it and use default configuration.
|
|
4
|
+
export async function runLoggerHook(loader) {
|
|
5
|
+
let module;
|
|
6
|
+
try {
|
|
7
|
+
module = (await loader());
|
|
8
|
+
}
|
|
9
|
+
catch { }
|
|
10
|
+
if (typeof module?.logger === 'function') {
|
|
11
|
+
// If module exists and exports a logger function, execute it
|
|
12
|
+
try {
|
|
13
|
+
return module.logger();
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
console.error('Failed to load logger configuration', e);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Load the props hook module and run it if it exists, passing down app's
|
|
2
|
+
// props object and logger. This hook can be used to modify props before
|
|
3
|
+
// they are passed to the root app component.
|
|
4
|
+
export async function runPropsHook(props, logger, loader) {
|
|
5
|
+
let module;
|
|
6
|
+
try {
|
|
7
|
+
module = (await loader());
|
|
8
|
+
}
|
|
9
|
+
catch { }
|
|
10
|
+
// If module exists and exports a default function, run it to modify props
|
|
11
|
+
return (typeof module?.default === 'function'
|
|
12
|
+
? module.default(props, logger)
|
|
13
|
+
: props);
|
|
14
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -28,6 +28,7 @@ async function loadApp(appsRoot, name, manifest, deployedStaticPath) {
|
|
|
28
28
|
name,
|
|
29
29
|
root: appRoot,
|
|
30
30
|
entry: path.join(appRoot, 'index.tsx'),
|
|
31
|
+
propsHookEntry: path.join(appRoot, 'props.ts'),
|
|
31
32
|
assets: loadAppAssets(name, manifest, deployedStaticPath),
|
|
32
33
|
examples: await loadAppExamples(appRoot),
|
|
33
34
|
schema: await loadAppSchema(appRoot),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runPropsHook(props: unknown, loader: () => Promise<unknown>): Promise<Record<string, unknown>>;
|
|
@@ -8,7 +8,9 @@ import { setupSwagger } from './parts/swagger.js';
|
|
|
8
8
|
import { validator } from './parts/validator.js';
|
|
9
9
|
import { renderPreviewPage } from './parts/preview.js';
|
|
10
10
|
import { setupK8SProbes } from './parts/k8s-probes.js';
|
|
11
|
-
import { runRuntimeHook } from './
|
|
11
|
+
import { runRuntimeHook } from './hooks/runtime.js';
|
|
12
|
+
import { runPropsHook } from './hooks/props.js';
|
|
13
|
+
import { runLoggerHook } from './hooks/logger.js';
|
|
12
14
|
// TODO: refactor to merge the similar parts between production and development server?
|
|
13
15
|
async function runProductionServer() {
|
|
14
16
|
const root = process.cwd();
|
|
@@ -16,12 +18,20 @@ async function runProductionServer() {
|
|
|
16
18
|
const apps = JSON.parse(await fs.readFile(path.join(root, 'build/nerest-manifest.json'), {
|
|
17
19
|
encoding: 'utf-8',
|
|
18
20
|
}));
|
|
19
|
-
// TODO: fix client-side vite types
|
|
20
21
|
const components = import.meta.glob('/apps/*/index.tsx', {
|
|
21
22
|
import: 'default',
|
|
22
23
|
eager: true,
|
|
23
24
|
});
|
|
24
|
-
const
|
|
25
|
+
const propsHooks = import.meta.glob('/apps/*/props.ts', {
|
|
26
|
+
eager: true,
|
|
27
|
+
});
|
|
28
|
+
const runtimeHook = import.meta.glob('/nerest-runtime.ts', { eager: true });
|
|
29
|
+
const app = fastify({
|
|
30
|
+
// Receive logger configuration from `nerest-runtime.logger()` or
|
|
31
|
+
// use the default fastify one (`true`)
|
|
32
|
+
logger: (await runLoggerHook(async () => runtimeHook['/nerest-runtime.ts'])) ??
|
|
33
|
+
true,
|
|
34
|
+
});
|
|
25
35
|
// Setup schema validation. We have to use our own ajv instance that
|
|
26
36
|
// we can use both to validate request bodies and examples against
|
|
27
37
|
// app schemas
|
|
@@ -30,6 +40,7 @@ async function runProductionServer() {
|
|
|
30
40
|
for (const appEntry of Object.values(apps)) {
|
|
31
41
|
const { name, examples, schema, assets } = appEntry;
|
|
32
42
|
const component = components[`/apps/${name}/index.tsx`];
|
|
43
|
+
const propsHook = async () => propsHooks[`/apps/${name}/props.ts`];
|
|
33
44
|
const routeOptions = {};
|
|
34
45
|
// TODO: report error if schema is missing, unless this app is client-only
|
|
35
46
|
// TODO: disallow apps without schemas in production build
|
|
@@ -43,7 +54,10 @@ async function runProductionServer() {
|
|
|
43
54
|
};
|
|
44
55
|
}
|
|
45
56
|
// POST /api/{name} -> render app with request.body as props
|
|
46
|
-
app.post(`/api/${name}`, routeOptions, (request) =>
|
|
57
|
+
app.post(`/api/${name}`, routeOptions, async (request) => {
|
|
58
|
+
const props = await runPropsHook(request.body, request.log, propsHook);
|
|
59
|
+
return renderApp({ name, assets, component }, props);
|
|
60
|
+
});
|
|
47
61
|
for (const [exampleName, example] of Object.entries(examples)) {
|
|
48
62
|
// GET /api/{name}/examples/{example} -> render a preview page
|
|
49
63
|
// with a predefined example body
|
|
@@ -54,12 +68,13 @@ async function runProductionServer() {
|
|
|
54
68
|
// description so it's easier to navigate to
|
|
55
69
|
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
56
70
|
},
|
|
57
|
-
}, async (
|
|
71
|
+
}, async (request, reply) => {
|
|
72
|
+
const props = await runPropsHook(example, request.log, propsHook);
|
|
58
73
|
const { html, assets: outAssets } = renderApp({
|
|
59
74
|
name,
|
|
60
75
|
assets,
|
|
61
76
|
component,
|
|
62
|
-
},
|
|
77
|
+
}, props);
|
|
63
78
|
reply.type('text/html');
|
|
64
79
|
return renderPreviewPage(html, outAssets);
|
|
65
80
|
});
|
|
@@ -71,15 +86,11 @@ async function runProductionServer() {
|
|
|
71
86
|
await setupK8SProbes(app);
|
|
72
87
|
}
|
|
73
88
|
// 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
|
-
});
|
|
89
|
+
await runRuntimeHook(app, async () => runtimeHook['/nerest-runtime.ts']);
|
|
78
90
|
// TODO: remove hardcoded port
|
|
79
91
|
await app.listen({
|
|
80
92
|
host: '0.0.0.0',
|
|
81
93
|
port: 3000,
|
|
82
94
|
});
|
|
83
|
-
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
84
95
|
}
|
|
85
96
|
runProductionServer();
|
package/package.json
CHANGED
package/server/development.ts
CHANGED
|
@@ -17,7 +17,9 @@ import { renderPreviewPage } from './parts/preview.js';
|
|
|
17
17
|
import { validator } from './parts/validator.js';
|
|
18
18
|
import { setupSwagger } from './parts/swagger.js';
|
|
19
19
|
import { setupK8SProbes } from './parts/k8s-probes.js';
|
|
20
|
-
import { runRuntimeHook } from './
|
|
20
|
+
import { runRuntimeHook } from './hooks/runtime.js';
|
|
21
|
+
import { runPropsHook } from './hooks/props.js';
|
|
22
|
+
import { runLoggerHook } from './hooks/logger.js';
|
|
21
23
|
|
|
22
24
|
// eslint-disable-next-line max-statements
|
|
23
25
|
export async function runDevelopmentServer() {
|
|
@@ -64,7 +66,16 @@ export async function runDevelopmentServer() {
|
|
|
64
66
|
|
|
65
67
|
// Start vite server that will be rendering SSR components
|
|
66
68
|
const viteSsr = await createServer(config);
|
|
67
|
-
|
|
69
|
+
|
|
70
|
+
// Start fastify server that will be processing HTTP requests
|
|
71
|
+
const app = fastify({
|
|
72
|
+
// Receive logger configuration from `nerest-runtime.logger()` or
|
|
73
|
+
// use the default fastify one (`true`)
|
|
74
|
+
logger:
|
|
75
|
+
(await runLoggerHook(() =>
|
|
76
|
+
viteSsr.ssrLoadModule('/nerest-runtime.ts')
|
|
77
|
+
)) ?? true,
|
|
78
|
+
});
|
|
68
79
|
|
|
69
80
|
// Setup schema validation. We have to use our own ajv instance that
|
|
70
81
|
// we can use both to validate request bodies and examples against
|
|
@@ -74,7 +85,7 @@ export async function runDevelopmentServer() {
|
|
|
74
85
|
await setupSwagger(app);
|
|
75
86
|
|
|
76
87
|
for (const appEntry of Object.values(apps)) {
|
|
77
|
-
const { name, entry, examples, schema } = appEntry;
|
|
88
|
+
const { name, entry, propsHookEntry, examples, schema } = appEntry;
|
|
78
89
|
|
|
79
90
|
const routeOptions: RouteShorthandOptions = {};
|
|
80
91
|
|
|
@@ -100,13 +111,16 @@ export async function runDevelopmentServer() {
|
|
|
100
111
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
101
112
|
fixStacktrace: true,
|
|
102
113
|
});
|
|
114
|
+
const props = await runPropsHook(request.body, request.log, () =>
|
|
115
|
+
viteSsr.ssrLoadModule(propsHookEntry)
|
|
116
|
+
);
|
|
103
117
|
return renderApp(
|
|
104
118
|
{
|
|
105
119
|
name,
|
|
106
120
|
assets: appEntry.assets,
|
|
107
121
|
component: ssrComponent.default,
|
|
108
122
|
},
|
|
109
|
-
|
|
123
|
+
props
|
|
110
124
|
);
|
|
111
125
|
});
|
|
112
126
|
|
|
@@ -131,17 +145,20 @@ export async function runDevelopmentServer() {
|
|
|
131
145
|
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
132
146
|
},
|
|
133
147
|
},
|
|
134
|
-
async (
|
|
148
|
+
async (request, reply) => {
|
|
135
149
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
136
150
|
fixStacktrace: true,
|
|
137
151
|
});
|
|
152
|
+
const props = await runPropsHook(example, request.log, () =>
|
|
153
|
+
viteSsr.ssrLoadModule(propsHookEntry)
|
|
154
|
+
);
|
|
138
155
|
const { html, assets } = renderApp(
|
|
139
156
|
{
|
|
140
157
|
name,
|
|
141
158
|
assets: appEntry.assets,
|
|
142
159
|
component: ssrComponent.default,
|
|
143
160
|
},
|
|
144
|
-
|
|
161
|
+
props
|
|
145
162
|
);
|
|
146
163
|
|
|
147
164
|
reply.type('text/html');
|
|
@@ -181,8 +198,6 @@ export async function runDevelopmentServer() {
|
|
|
181
198
|
host: '0.0.0.0',
|
|
182
199
|
port: 3000,
|
|
183
200
|
});
|
|
184
|
-
|
|
185
|
-
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
186
201
|
}
|
|
187
202
|
|
|
188
203
|
// TODO: this should probably be moved from here
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FastifyServerOptions } from 'fastify';
|
|
2
|
+
|
|
3
|
+
type LoggerHookModule = {
|
|
4
|
+
logger?: () => FastifyServerOptions['logger'];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// Load the runtime hook module and run it if it exists, to receive
|
|
8
|
+
// logger configuration. If the `logger` function does not exist
|
|
9
|
+
// in the module, ignore it and use default configuration.
|
|
10
|
+
export async function runLoggerHook(loader: () => Promise<unknown>) {
|
|
11
|
+
let module: LoggerHookModule | undefined;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
module = (await loader()) as LoggerHookModule;
|
|
15
|
+
} catch {}
|
|
16
|
+
|
|
17
|
+
if (typeof module?.logger === 'function') {
|
|
18
|
+
// If module exists and exports a logger function, execute it
|
|
19
|
+
try {
|
|
20
|
+
return module.logger();
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error('Failed to load logger configuration', e);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FastifyBaseLogger } from 'fastify';
|
|
2
|
+
|
|
3
|
+
type PropsHookModule = {
|
|
4
|
+
default: (props: unknown, logger: FastifyBaseLogger) => unknown;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// Load the props hook module and run it if it exists, passing down app's
|
|
8
|
+
// props object and logger. This hook can be used to modify props before
|
|
9
|
+
// they are passed to the root app component.
|
|
10
|
+
export async function runPropsHook(
|
|
11
|
+
props: unknown,
|
|
12
|
+
logger: FastifyBaseLogger,
|
|
13
|
+
loader: () => Promise<unknown>
|
|
14
|
+
) {
|
|
15
|
+
let module: PropsHookModule | undefined;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
module = (await loader()) as PropsHookModule;
|
|
19
|
+
} catch {}
|
|
20
|
+
|
|
21
|
+
// If module exists and exports a default function, run it to modify props
|
|
22
|
+
return (
|
|
23
|
+
typeof module?.default === 'function'
|
|
24
|
+
? module.default(props, logger)
|
|
25
|
+
: props
|
|
26
|
+
) as Record<string, unknown>;
|
|
27
|
+
}
|
package/server/parts/apps.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type AppEntry = {
|
|
|
11
11
|
name: string;
|
|
12
12
|
root: string;
|
|
13
13
|
entry: string;
|
|
14
|
+
propsHookEntry: string;
|
|
14
15
|
assets: string[];
|
|
15
16
|
examples: Record<string, unknown>;
|
|
16
17
|
schema: Record<string, unknown> | null;
|
|
@@ -49,6 +50,7 @@ async function loadApp(
|
|
|
49
50
|
name,
|
|
50
51
|
root: appRoot,
|
|
51
52
|
entry: path.join(appRoot, 'index.tsx'),
|
|
53
|
+
propsHookEntry: path.join(appRoot, 'props.ts'),
|
|
52
54
|
assets: loadAppAssets(name, manifest, deployedStaticPath),
|
|
53
55
|
examples: await loadAppExamples(appRoot),
|
|
54
56
|
schema: await loadAppSchema(appRoot),
|
package/server/production.ts
CHANGED
|
@@ -12,7 +12,9 @@ import { setupSwagger } from './parts/swagger.js';
|
|
|
12
12
|
import { validator } from './parts/validator.js';
|
|
13
13
|
import { renderPreviewPage } from './parts/preview.js';
|
|
14
14
|
import { setupK8SProbes } from './parts/k8s-probes.js';
|
|
15
|
-
import { runRuntimeHook } from './
|
|
15
|
+
import { runRuntimeHook } from './hooks/runtime.js';
|
|
16
|
+
import { runPropsHook } from './hooks/props.js';
|
|
17
|
+
import { runLoggerHook } from './hooks/logger.js';
|
|
16
18
|
|
|
17
19
|
// TODO: refactor to merge the similar parts between production and development server?
|
|
18
20
|
async function runProductionServer() {
|
|
@@ -25,13 +27,24 @@ async function runProductionServer() {
|
|
|
25
27
|
})
|
|
26
28
|
) as Record<string, AppEntry>;
|
|
27
29
|
|
|
28
|
-
// TODO: fix client-side vite types
|
|
29
30
|
const components = import.meta.glob('/apps/*/index.tsx', {
|
|
30
31
|
import: 'default',
|
|
31
32
|
eager: true,
|
|
32
33
|
}) as Record<string, React.ComponentType>;
|
|
33
34
|
|
|
34
|
-
const
|
|
35
|
+
const propsHooks = import.meta.glob('/apps/*/props.ts', {
|
|
36
|
+
eager: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const runtimeHook = import.meta.glob('/nerest-runtime.ts', { eager: true });
|
|
40
|
+
|
|
41
|
+
const app = fastify({
|
|
42
|
+
// Receive logger configuration from `nerest-runtime.logger()` or
|
|
43
|
+
// use the default fastify one (`true`)
|
|
44
|
+
logger:
|
|
45
|
+
(await runLoggerHook(async () => runtimeHook['/nerest-runtime.ts'])) ??
|
|
46
|
+
true,
|
|
47
|
+
});
|
|
35
48
|
|
|
36
49
|
// Setup schema validation. We have to use our own ajv instance that
|
|
37
50
|
// we can use both to validate request bodies and examples against
|
|
@@ -43,6 +56,7 @@ async function runProductionServer() {
|
|
|
43
56
|
for (const appEntry of Object.values(apps)) {
|
|
44
57
|
const { name, examples, schema, assets } = appEntry;
|
|
45
58
|
const component = components[`/apps/${name}/index.tsx`];
|
|
59
|
+
const propsHook = async () => propsHooks[`/apps/${name}/props.ts`];
|
|
46
60
|
|
|
47
61
|
const routeOptions: RouteShorthandOptions = {};
|
|
48
62
|
|
|
@@ -59,12 +73,10 @@ async function runProductionServer() {
|
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
// POST /api/{name} -> render app with request.body as props
|
|
62
|
-
app.post(`/api/${name}`, routeOptions, (request) =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
);
|
|
76
|
+
app.post(`/api/${name}`, routeOptions, async (request) => {
|
|
77
|
+
const props = await runPropsHook(request.body, request.log, propsHook);
|
|
78
|
+
return renderApp({ name, assets, component }, props);
|
|
79
|
+
});
|
|
68
80
|
|
|
69
81
|
for (const [exampleName, example] of Object.entries(examples)) {
|
|
70
82
|
// GET /api/{name}/examples/{example} -> render a preview page
|
|
@@ -79,14 +91,15 @@ async function runProductionServer() {
|
|
|
79
91
|
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
80
92
|
},
|
|
81
93
|
},
|
|
82
|
-
async (
|
|
94
|
+
async (request, reply) => {
|
|
95
|
+
const props = await runPropsHook(example, request.log, propsHook);
|
|
83
96
|
const { html, assets: outAssets } = renderApp(
|
|
84
97
|
{
|
|
85
98
|
name,
|
|
86
99
|
assets,
|
|
87
100
|
component,
|
|
88
101
|
},
|
|
89
|
-
|
|
102
|
+
props
|
|
90
103
|
);
|
|
91
104
|
|
|
92
105
|
reply.type('text/html');
|
|
@@ -105,18 +118,13 @@ async function runProductionServer() {
|
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
// 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
|
-
});
|
|
121
|
+
await runRuntimeHook(app, async () => runtimeHook['/nerest-runtime.ts']);
|
|
112
122
|
|
|
113
123
|
// TODO: remove hardcoded port
|
|
114
124
|
await app.listen({
|
|
115
125
|
host: '0.0.0.0',
|
|
116
126
|
port: 3000,
|
|
117
127
|
});
|
|
118
|
-
|
|
119
|
-
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
runProductionServer();
|
|
File without changes
|