@nerest/nerest 0.0.8 → 0.0.9

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,21 @@ 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 a single object as an argument -- the body of the request. 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
+ export default function (props: Props) {
46
+ return {
47
+ ...props,
48
+ greeting: `${props.greeting} (modified in props.ts)`,
49
+ };
50
+ }
51
+ ```
52
+
53
+ 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.
54
+
40
55
  ## Configuration
41
56
 
42
57
  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.
@@ -10,7 +10,8 @@ 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';
14
15
  // eslint-disable-next-line max-statements
15
16
  export async function runDevelopmentServer() {
16
17
  const root = process.cwd();
@@ -59,7 +60,7 @@ export async function runDevelopmentServer() {
59
60
  app.setValidatorCompiler(({ schema }) => validator.compile(schema));
60
61
  await setupSwagger(app);
61
62
  for (const appEntry of Object.values(apps)) {
62
- const { name, entry, examples, schema } = appEntry;
63
+ const { name, entry, propsHookEntry, examples, schema } = appEntry;
63
64
  const routeOptions = {};
64
65
  // TODO: report error if schema is missing, unless this app is client-only
65
66
  if (schema) {
@@ -82,11 +83,12 @@ export async function runDevelopmentServer() {
82
83
  const ssrComponent = await viteSsr.ssrLoadModule(entry, {
83
84
  fixStacktrace: true,
84
85
  });
86
+ const props = await runPropsHook(request.body, () => viteSsr.ssrLoadModule(propsHookEntry));
85
87
  return renderApp({
86
88
  name,
87
89
  assets: appEntry.assets,
88
90
  component: ssrComponent.default,
89
- }, request.body);
91
+ }, props);
90
92
  });
91
93
  for (const [exampleName, example] of Object.entries(examples)) {
92
94
  // Validate example against schema when specified
@@ -107,11 +109,12 @@ export async function runDevelopmentServer() {
107
109
  const ssrComponent = await viteSsr.ssrLoadModule(entry, {
108
110
  fixStacktrace: true,
109
111
  });
112
+ const props = await runPropsHook(example, () => viteSsr.ssrLoadModule(propsHookEntry));
110
113
  const { html, assets } = renderApp({
111
114
  name,
112
115
  assets: appEntry.assets,
113
116
  component: ssrComponent.default,
114
- }, example);
117
+ }, props);
115
118
  reply.type('text/html');
116
119
  return renderPreviewPage(html, assets);
117
120
  });
@@ -0,0 +1 @@
1
+ export declare function runPropsHook(props: unknown, loader: () => Promise<unknown>): Promise<Record<string, unknown>>;
@@ -0,0 +1,12 @@
1
+ // Load the props hook module and run it if it exists, passing down app's
2
+ // props object. This hook can be used to modify props before they are passed
3
+ // to the root app component.
4
+ export async function runPropsHook(props, 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' ? module.default(props) : props);
12
+ }
@@ -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,8 @@ 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';
12
13
  // TODO: refactor to merge the similar parts between production and development server?
13
14
  async function runProductionServer() {
14
15
  const root = process.cwd();
@@ -16,11 +17,13 @@ async function runProductionServer() {
16
17
  const apps = JSON.parse(await fs.readFile(path.join(root, 'build/nerest-manifest.json'), {
17
18
  encoding: 'utf-8',
18
19
  }));
19
- // TODO: fix client-side vite types
20
20
  const components = import.meta.glob('/apps/*/index.tsx', {
21
21
  import: 'default',
22
22
  eager: true,
23
23
  });
24
+ const propsHooks = import.meta.glob('/apps/*/props.ts', {
25
+ eager: true,
26
+ });
24
27
  const app = fastify();
25
28
  // Setup schema validation. We have to use our own ajv instance that
26
29
  // we can use both to validate request bodies and examples against
@@ -30,6 +33,7 @@ async function runProductionServer() {
30
33
  for (const appEntry of Object.values(apps)) {
31
34
  const { name, examples, schema, assets } = appEntry;
32
35
  const component = components[`/apps/${name}/index.tsx`];
36
+ const propsHook = async () => propsHooks[`/apps/${name}/props.ts`];
33
37
  const routeOptions = {};
34
38
  // TODO: report error if schema is missing, unless this app is client-only
35
39
  // TODO: disallow apps without schemas in production build
@@ -43,7 +47,10 @@ async function runProductionServer() {
43
47
  };
44
48
  }
45
49
  // POST /api/{name} -> render app with request.body as props
46
- app.post(`/api/${name}`, routeOptions, (request) => renderApp({ name, assets, component }, request.body));
50
+ app.post(`/api/${name}`, routeOptions, async (request) => {
51
+ const props = await runPropsHook(request.body, propsHook);
52
+ return renderApp({ name, assets, component }, props);
53
+ });
47
54
  for (const [exampleName, example] of Object.entries(examples)) {
48
55
  // GET /api/{name}/examples/{example} -> render a preview page
49
56
  // with a predefined example body
@@ -55,11 +62,12 @@ async function runProductionServer() {
55
62
  description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
56
63
  },
57
64
  }, async (_, reply) => {
65
+ const props = await runPropsHook(example, propsHook);
58
66
  const { html, assets: outAssets } = renderApp({
59
67
  name,
60
68
  assets,
61
69
  component,
62
- }, example);
70
+ }, props);
63
71
  reply.type('text/html');
64
72
  return renderPreviewPage(html, outAssets);
65
73
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerest/nerest",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "React micro frontend framework",
5
5
  "homepage": "https://github.com/nerestjs/nerest#readme",
6
6
  "repository": {
@@ -17,7 +17,8 @@ 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';
21
22
 
22
23
  // eslint-disable-next-line max-statements
23
24
  export async function runDevelopmentServer() {
@@ -74,7 +75,7 @@ export async function runDevelopmentServer() {
74
75
  await setupSwagger(app);
75
76
 
76
77
  for (const appEntry of Object.values(apps)) {
77
- const { name, entry, examples, schema } = appEntry;
78
+ const { name, entry, propsHookEntry, examples, schema } = appEntry;
78
79
 
79
80
  const routeOptions: RouteShorthandOptions = {};
80
81
 
@@ -100,13 +101,16 @@ export async function runDevelopmentServer() {
100
101
  const ssrComponent = await viteSsr.ssrLoadModule(entry, {
101
102
  fixStacktrace: true,
102
103
  });
104
+ const props = await runPropsHook(request.body, () =>
105
+ viteSsr.ssrLoadModule(propsHookEntry)
106
+ );
103
107
  return renderApp(
104
108
  {
105
109
  name,
106
110
  assets: appEntry.assets,
107
111
  component: ssrComponent.default,
108
112
  },
109
- request.body as Record<string, unknown>
113
+ props
110
114
  );
111
115
  });
112
116
 
@@ -135,13 +139,16 @@ export async function runDevelopmentServer() {
135
139
  const ssrComponent = await viteSsr.ssrLoadModule(entry, {
136
140
  fixStacktrace: true,
137
141
  });
142
+ const props = await runPropsHook(example, () =>
143
+ viteSsr.ssrLoadModule(propsHookEntry)
144
+ );
138
145
  const { html, assets } = renderApp(
139
146
  {
140
147
  name,
141
148
  assets: appEntry.assets,
142
149
  component: ssrComponent.default,
143
150
  },
144
- example as Record<string, unknown>
151
+ props
145
152
  );
146
153
 
147
154
  reply.type('text/html');
@@ -0,0 +1,22 @@
1
+ type PropsHookModule = {
2
+ default: (props: unknown) => unknown;
3
+ };
4
+
5
+ // Load the props hook module and run it if it exists, passing down app's
6
+ // props object. This hook can be used to modify props before they are passed
7
+ // to the root app component.
8
+ export async function runPropsHook(
9
+ props: unknown,
10
+ loader: () => Promise<unknown>
11
+ ) {
12
+ let module: PropsHookModule | undefined;
13
+
14
+ try {
15
+ module = (await loader()) as PropsHookModule;
16
+ } catch {}
17
+
18
+ // If module exists and exports a default function, run it to modify props
19
+ return (
20
+ typeof module?.default === 'function' ? module.default(props) : props
21
+ ) as Record<string, unknown>;
22
+ }
@@ -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,8 @@ 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';
16
17
 
17
18
  // TODO: refactor to merge the similar parts between production and development server?
18
19
  async function runProductionServer() {
@@ -25,12 +26,15 @@ async function runProductionServer() {
25
26
  })
26
27
  ) as Record<string, AppEntry>;
27
28
 
28
- // TODO: fix client-side vite types
29
29
  const components = import.meta.glob('/apps/*/index.tsx', {
30
30
  import: 'default',
31
31
  eager: true,
32
32
  }) as Record<string, React.ComponentType>;
33
33
 
34
+ const propsHooks = import.meta.glob('/apps/*/props.ts', {
35
+ eager: true,
36
+ });
37
+
34
38
  const app = fastify();
35
39
 
36
40
  // Setup schema validation. We have to use our own ajv instance that
@@ -43,6 +47,7 @@ async function runProductionServer() {
43
47
  for (const appEntry of Object.values(apps)) {
44
48
  const { name, examples, schema, assets } = appEntry;
45
49
  const component = components[`/apps/${name}/index.tsx`];
50
+ const propsHook = async () => propsHooks[`/apps/${name}/props.ts`];
46
51
 
47
52
  const routeOptions: RouteShorthandOptions = {};
48
53
 
@@ -59,12 +64,10 @@ async function runProductionServer() {
59
64
  }
60
65
 
61
66
  // 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
- );
67
+ app.post(`/api/${name}`, routeOptions, async (request) => {
68
+ const props = await runPropsHook(request.body, propsHook);
69
+ return renderApp({ name, assets, component }, props);
70
+ });
68
71
 
69
72
  for (const [exampleName, example] of Object.entries(examples)) {
70
73
  // GET /api/{name}/examples/{example} -> render a preview page
@@ -80,13 +83,14 @@ async function runProductionServer() {
80
83
  },
81
84
  },
82
85
  async (_, reply) => {
86
+ const props = await runPropsHook(example, propsHook);
83
87
  const { html, assets: outAssets } = renderApp(
84
88
  {
85
89
  name,
86
90
  assets,
87
91
  component,
88
92
  },
89
- example as Record<string, unknown>
93
+ props
90
94
  );
91
95
 
92
96
  reply.type('text/html');