@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 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 Hooks
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 './parts/runtime-hook.js';
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
- const app = fastify();
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
- }, request.body);
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 (_, reply) => {
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
- }, example);
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,2 @@
1
+ import type { FastifyBaseLogger } from 'fastify';
2
+ export declare function runPropsHook(props: unknown, logger: FastifyBaseLogger, loader: () => Promise<unknown>): Promise<Record<string, unknown>>;
@@ -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,2 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export declare function runRuntimeHook(app: FastifyInstance, loader: () => Promise<unknown>): Promise<void>;
@@ -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
+ }
@@ -2,6 +2,7 @@ export type AppEntry = {
2
2
  name: string;
3
3
  root: string;
4
4
  entry: string;
5
+ propsHookEntry: string;
5
6
  assets: string[];
6
7
  examples: Record<string, unknown>;
7
8
  schema: Record<string, unknown> | null;
@@ -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>>;
@@ -0,0 +1,8 @@
1
+ export async function runPropsHook(props, loader) {
2
+ let module;
3
+ try {
4
+ module = (await loader());
5
+ }
6
+ catch { }
7
+ return (typeof module?.default === 'function' ? module.default(props) : props);
8
+ }
@@ -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 './parts/runtime-hook.js';
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 app = fastify();
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) => renderApp({ name, assets, component }, request.body));
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 (_, reply) => {
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
- }, example);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerest/nerest",
3
- "version": "0.0.8",
3
+ "version": "0.1.0",
4
4
  "description": "React micro frontend framework",
5
5
  "homepage": "https://github.com/nerestjs/nerest#readme",
6
6
  "repository": {
@@ -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 './parts/runtime-hook.js';
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
- const app = fastify();
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
- request.body as Record<string, unknown>
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 (_, reply) => {
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
- example as Record<string, unknown>
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
+ }
@@ -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),
@@ -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 './parts/runtime-hook.js';
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 app = fastify();
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
- renderApp(
64
- { name, assets, component },
65
- request.body as Record<string, unknown>
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 (_, reply) => {
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
- example as Record<string, unknown>
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();