@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 +15 -0
- package/dist/server/development.js +7 -4
- package/dist/server/hooks/props.d.ts +1 -0
- package/dist/server/hooks/props.js +12 -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 +12 -4
- package/package.json +1 -1
- package/server/development.ts +11 -4
- package/server/hooks/props.ts +22 -0
- package/server/parts/apps.ts +2 -0
- package/server/production.ts +13 -9
- /package/server/{parts/runtime-hook.ts → hooks/runtime.ts} +0 -0
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 './
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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,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,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 './
|
|
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) =>
|
|
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
|
-
},
|
|
70
|
+
}, props);
|
|
63
71
|
reply.type('text/html');
|
|
64
72
|
return renderPreviewPage(html, outAssets);
|
|
65
73
|
});
|
package/package.json
CHANGED
package/server/development.ts
CHANGED
|
@@ -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 './
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
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,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 './
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
93
|
+
props
|
|
90
94
|
);
|
|
91
95
|
|
|
92
96
|
reply.type('text/html');
|
|
File without changes
|