@nerest/nerest 0.0.9 → 1.5.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/LICENSE.md +178 -0
- package/README.md +148 -39
- package/bin/index.ts +3 -0
- package/bin/typegen.ts +35 -0
- package/bin/watch.ts +3 -2
- package/build/configs/development.ts +63 -0
- package/build/configs/production.ts +59 -0
- package/build/configs/shared.ts +51 -0
- package/build/configs/vite-logger.development.ts +30 -0
- package/build/index.ts +53 -76
- package/client/index.ts +14 -2
- package/dist/bin/index.js +4 -0
- package/dist/bin/typegen.d.ts +1 -0
- package/dist/bin/typegen.js +31 -0
- package/dist/bin/watch.js +1 -2
- package/dist/build/configs/development.d.ts +4 -0
- package/dist/build/configs/development.js +53 -0
- package/dist/build/configs/production.d.ts +4 -0
- package/dist/build/configs/production.js +50 -0
- package/dist/build/configs/shared.d.ts +10 -0
- package/dist/build/configs/shared.js +24 -0
- package/dist/build/configs/vite-logger.development.d.ts +2 -0
- package/dist/build/configs/vite-logger.development.js +25 -0
- package/dist/build/index.js +39 -69
- package/dist/client/index.js +9 -2
- package/dist/server/development.d.ts +1 -1
- package/dist/server/development.js +52 -130
- package/dist/server/hooks/logger.d.ts +1 -0
- package/dist/server/hooks/logger.js +24 -0
- package/dist/server/hooks/props.d.ts +2 -1
- package/dist/server/hooks/props.js +6 -4
- package/dist/server/hooks/runtime.js +3 -3
- package/dist/server/{parts → loaders}/apps.d.ts +2 -1
- package/dist/server/loaders/apps.js +36 -0
- package/dist/server/loaders/assets.js +25 -2
- package/dist/server/loaders/build.d.ts +2 -0
- package/dist/server/loaders/build.js +11 -0
- package/dist/server/loaders/examples.js +7 -13
- package/dist/server/loaders/manifest.d.ts +8 -1
- package/dist/server/loaders/manifest.js +12 -4
- package/dist/server/loaders/preview.d.ts +4 -0
- package/dist/server/loaders/preview.js +11 -0
- package/dist/server/loaders/project.d.ts +16 -0
- package/dist/server/loaders/project.js +11 -0
- package/dist/server/loaders/schema.d.ts +2 -1
- package/dist/server/loaders/schema.js +12 -8
- package/dist/server/parts/k8s-probes.js +10 -6
- package/dist/server/parts/preview.d.ts +2 -1
- package/dist/server/parts/preview.js +5 -4
- package/dist/server/parts/render.d.ts +5 -3
- package/dist/server/parts/render.js +8 -8
- package/dist/server/parts/swagger.d.ts +2 -1
- package/dist/server/parts/swagger.js +8 -19
- package/dist/server/parts/validator.d.ts +3 -1
- package/dist/server/parts/validator.js +13 -0
- package/dist/server/production.js +17 -80
- package/dist/server/shared.d.ts +14 -0
- package/dist/server/shared.js +94 -0
- package/dist/server/utils.d.ts +1 -0
- package/dist/server/utils.js +5 -0
- package/package.json +46 -43
- package/schemas/nerest-build.schema.d.ts +19 -1
- package/schemas/nerest-build.schema.json +21 -1
- package/server/development.ts +67 -164
- package/server/hooks/logger.ts +31 -0
- package/server/hooks/props.ts +11 -6
- package/server/hooks/runtime.ts +3 -3
- package/server/loaders/apps.ts +58 -0
- package/server/loaders/assets.ts +30 -2
- package/server/loaders/build.ts +14 -0
- package/server/loaders/examples.ts +7 -15
- package/server/loaders/manifest.ts +23 -4
- package/server/loaders/preview.ts +17 -0
- package/server/loaders/project.ts +33 -0
- package/server/loaders/schema.ts +12 -10
- package/server/parts/k8s-probes.ts +26 -13
- package/server/parts/preview.ts +11 -4
- package/server/parts/render.ts +13 -9
- package/server/parts/swagger.ts +10 -29
- package/server/parts/validator.ts +14 -0
- package/server/production.ts +22 -106
- package/server/shared.ts +150 -0
- package/server/utils.ts +6 -0
- package/dist/server/parts/apps.js +0 -37
- package/dist/server/parts/props-hook.d.ts +0 -1
- package/dist/server/parts/props-hook.js +0 -8
- package/dist/server/parts/runtime-hook.d.ts +0 -2
- package/dist/server/parts/runtime-hook.js +0 -28
- package/server/parts/apps.ts +0 -59
package/server/parts/preview.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import type { PreviewParts } from '../loaders/preview.js';
|
|
2
|
+
|
|
1
3
|
// Renders the preview page available by convention at /api/{name}/examples/{example}
|
|
2
|
-
export function renderPreviewPage(
|
|
4
|
+
export function renderPreviewPage(
|
|
5
|
+
html: string,
|
|
6
|
+
assets: string[],
|
|
7
|
+
parts: PreviewParts
|
|
8
|
+
) {
|
|
3
9
|
const { scripts, styles } = mapAssets(assets);
|
|
4
10
|
return `
|
|
11
|
+
<!DOCTYPE html>
|
|
5
12
|
<html>
|
|
6
13
|
<head>
|
|
14
|
+
<meta charset="utf-8">
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
16
|
${styles.join('\n')}
|
|
17
|
+
${parts.head}
|
|
8
18
|
</head>
|
|
9
19
|
<body>
|
|
10
20
|
${html}
|
|
@@ -15,9 +25,6 @@ export function renderPreviewPage(html: string, assets: string[]) {
|
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
function mapAssets(assets: string[]) {
|
|
18
|
-
// TODO: script type="module" is not supported by older browsers
|
|
19
|
-
// but vite doesn't provide `nomodule` fallback by default
|
|
20
|
-
// see @vitejs/plugin-legacy
|
|
21
28
|
const scripts = assets
|
|
22
29
|
.filter((src) => src.endsWith('.js'))
|
|
23
30
|
.map((src) => `<script type="module" src="${src}"></script>`);
|
package/server/parts/render.ts
CHANGED
|
@@ -1,35 +1,39 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import { createElement } from 'react';
|
|
2
3
|
import { renderToString } from 'react-dom/server';
|
|
3
|
-
import {
|
|
4
|
+
import type { Project } from '../loaders/project.js';
|
|
5
|
+
import { randomId } from '../utils.js';
|
|
4
6
|
|
|
5
7
|
type RenderProps = {
|
|
6
8
|
name: string;
|
|
7
9
|
assets: string[];
|
|
8
|
-
component:
|
|
10
|
+
component: ComponentType;
|
|
11
|
+
project: Project;
|
|
9
12
|
};
|
|
10
13
|
|
|
11
14
|
export function renderApp(
|
|
12
|
-
{ name, assets, component }: RenderProps,
|
|
15
|
+
{ name, assets, component, project }: RenderProps,
|
|
13
16
|
props: Record<string, unknown> = {}
|
|
14
17
|
) {
|
|
15
|
-
const html = renderSsrComponent(name, component, props);
|
|
18
|
+
const html = renderSsrComponent(name, component, project, props);
|
|
16
19
|
return { html, assets };
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
function renderSsrComponent(
|
|
20
23
|
appName: string,
|
|
21
|
-
appComponent:
|
|
24
|
+
appComponent: ComponentType,
|
|
25
|
+
project: Project,
|
|
22
26
|
props: Record<string, unknown>
|
|
23
27
|
) {
|
|
24
|
-
const html = renderToString(
|
|
28
|
+
const html = renderToString(createElement(appComponent, props));
|
|
25
29
|
|
|
26
30
|
// There may be multiple instances of the same app on the page,
|
|
27
31
|
// so we will use a randomized id to avoid collisions
|
|
28
|
-
const appId =
|
|
32
|
+
const appId = randomId();
|
|
29
33
|
|
|
30
34
|
// data-app-name and data-app-id are used by client entrypoint to hydrate
|
|
31
35
|
// apps using correct serialized props
|
|
32
|
-
const container = `<div data-app-name="${appName}" data-app-id="${appId}">${html}</div>`;
|
|
36
|
+
const container = `<div data-project-name="${project.name}" data-app-name="${appName}" data-app-id="${appId}">${html}</div>`;
|
|
33
37
|
const script = `<script type="application/json" data-app-id="${appId}">${JSON.stringify(
|
|
34
38
|
props
|
|
35
39
|
)}</script>`;
|
package/server/parts/swagger.ts
CHANGED
|
@@ -1,43 +1,24 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import fs from 'fs';
|
|
3
1
|
import type { FastifyInstance } from 'fastify';
|
|
4
2
|
import fastifySwagger from '@fastify/swagger';
|
|
5
3
|
import fastifySwaggerUi from '@fastify/swagger-ui';
|
|
6
4
|
|
|
5
|
+
import type { Project } from '../loaders/project.js';
|
|
6
|
+
|
|
7
7
|
// Setup automatic OpenAPI specification compilation and enable
|
|
8
8
|
// Swagger UI at the `/api` route
|
|
9
|
-
export async function setupSwagger(app: FastifyInstance) {
|
|
10
|
-
let appInfo: {
|
|
11
|
-
name?: string;
|
|
12
|
-
description?: string;
|
|
13
|
-
version?: string;
|
|
14
|
-
homepage?: string;
|
|
15
|
-
repository?: string | { url?: string };
|
|
16
|
-
} = {};
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const packageJson = fs.readFileSync(
|
|
20
|
-
path.join(process.cwd(), 'package.json'),
|
|
21
|
-
{ encoding: 'utf-8' }
|
|
22
|
-
);
|
|
23
|
-
appInfo = JSON.parse(packageJson);
|
|
24
|
-
} catch (e) {
|
|
25
|
-
// We only use package.json info to setup Swagger info and links,
|
|
26
|
-
// if we are unable to load them -- that's fine
|
|
27
|
-
}
|
|
28
|
-
|
|
9
|
+
export async function setupSwagger(app: FastifyInstance, project: Project) {
|
|
29
10
|
const homepage =
|
|
30
|
-
|
|
31
|
-
(typeof
|
|
32
|
-
?
|
|
33
|
-
:
|
|
11
|
+
project.homepage ||
|
|
12
|
+
(typeof project.repository === 'string'
|
|
13
|
+
? project.repository
|
|
14
|
+
: project.repository?.url);
|
|
34
15
|
|
|
35
16
|
await app.register(fastifySwagger, {
|
|
36
17
|
openapi: {
|
|
37
18
|
info: {
|
|
38
|
-
title:
|
|
39
|
-
description:
|
|
40
|
-
version:
|
|
19
|
+
title: project.name || 'Nerest micro frontend',
|
|
20
|
+
description: project.description,
|
|
21
|
+
version: project.version ?? '',
|
|
41
22
|
contact: homepage
|
|
42
23
|
? {
|
|
43
24
|
name: 'Homepage',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Ajv from 'ajv';
|
|
2
2
|
import fastUri from 'fast-uri';
|
|
3
3
|
import addFormats from 'ajv-formats';
|
|
4
|
+
import type { FastifyInstance } from 'fastify';
|
|
4
5
|
|
|
5
6
|
// Ajv default export is broken, so we have to specify `.default`
|
|
6
7
|
// manually: https://github.com/ajv-validator/ajv/issues/2132
|
|
@@ -20,3 +21,16 @@ export const validator = new Ajv.default({
|
|
|
20
21
|
// `email`, `url`, etc. Used by default in fastify
|
|
21
22
|
// https://www.npmjs.com/package/ajv-formats
|
|
22
23
|
addFormats.default(validator);
|
|
24
|
+
|
|
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
|
+
export function setupValidator(app: FastifyInstance) {
|
|
29
|
+
if (process.env.DISABLE_SCHEMA_VALIDATION) {
|
|
30
|
+
// If schema validation is disabled, return data as is without any checks
|
|
31
|
+
app.setValidatorCompiler(() => (data) => ({ value: data }));
|
|
32
|
+
} else {
|
|
33
|
+
// If schema validation is enabled, validate and coerce data via ajv
|
|
34
|
+
app.setValidatorCompiler(({ schema }) => validator.compile(schema));
|
|
35
|
+
}
|
|
36
|
+
}
|
package/server/production.ts
CHANGED
|
@@ -1,126 +1,42 @@
|
|
|
1
1
|
// This is the nerest production server entrypoint
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import type { ComponentType } from 'react';
|
|
3
|
+
import { createServer } from './shared.js';
|
|
4
|
+
import { loadNerestManifest } from './loaders/manifest.js';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
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 './hooks/runtime.js';
|
|
16
|
-
import { runPropsHook } from './hooks/props.js';
|
|
17
|
-
|
|
18
|
-
// TODO: refactor to merge the similar parts between production and development server?
|
|
19
|
-
async function runProductionServer() {
|
|
6
|
+
// Important: this file is the server entrypoint that will be built by vite
|
|
7
|
+
// in `build/index.ts`. All of the import.meta.glob's will be resolved at build time
|
|
8
|
+
async function runProductionServer(port: number) {
|
|
20
9
|
const root = process.cwd();
|
|
21
10
|
|
|
22
|
-
//
|
|
23
|
-
const apps =
|
|
24
|
-
await fs.readFile(path.join(root, 'build/nerest-manifest.json'), {
|
|
25
|
-
encoding: 'utf-8',
|
|
26
|
-
})
|
|
27
|
-
) as Record<string, AppEntry>;
|
|
11
|
+
// Load project information from the manifest generated during production build
|
|
12
|
+
const { project, apps } = await loadNerestManifest(root);
|
|
28
13
|
|
|
29
14
|
const components = import.meta.glob('/apps/*/index.tsx', {
|
|
30
15
|
import: 'default',
|
|
31
16
|
eager: true,
|
|
32
|
-
}) as Record<string,
|
|
17
|
+
}) as Record<string, ComponentType>;
|
|
33
18
|
|
|
34
19
|
const propsHooks = import.meta.glob('/apps/*/props.ts', {
|
|
35
20
|
eager: true,
|
|
36
21
|
});
|
|
37
22
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const component = components[`/apps/${name}/index.tsx`];
|
|
50
|
-
const propsHook = async () => propsHooks[`/apps/${name}/props.ts`];
|
|
51
|
-
|
|
52
|
-
const routeOptions: RouteShorthandOptions = {};
|
|
53
|
-
|
|
54
|
-
// TODO: report error if schema is missing, unless this app is client-only
|
|
55
|
-
// TODO: disallow apps without schemas in production build
|
|
56
|
-
if (schema) {
|
|
57
|
-
routeOptions.schema = {
|
|
58
|
-
// Use description as Swagger summary, since summary is visible
|
|
59
|
-
// even when the route is collapsed in the UI
|
|
60
|
-
summary: schema.description as string,
|
|
61
|
-
// TODO: do we need to mix in examples like in the development server?
|
|
62
|
-
body: schema,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// POST /api/{name} -> render app with request.body as props
|
|
67
|
-
app.post(`/api/${name}`, routeOptions, async (request) => {
|
|
68
|
-
const props = await runPropsHook(request.body, propsHook);
|
|
69
|
-
return renderApp({ name, assets, component }, props);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
for (const [exampleName, example] of Object.entries(examples)) {
|
|
73
|
-
// GET /api/{name}/examples/{example} -> render a preview page
|
|
74
|
-
// with a predefined example body
|
|
75
|
-
const exampleRoute = `/api/${name}/examples/${exampleName}`;
|
|
76
|
-
app.get(
|
|
77
|
-
exampleRoute,
|
|
78
|
-
{
|
|
79
|
-
schema: {
|
|
80
|
-
// Add a clickable link to the example route in route's Swagger
|
|
81
|
-
// description so it's easier to navigate to
|
|
82
|
-
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
async (_, reply) => {
|
|
86
|
-
const props = await runPropsHook(example, propsHook);
|
|
87
|
-
const { html, assets: outAssets } = renderApp(
|
|
88
|
-
{
|
|
89
|
-
name,
|
|
90
|
-
assets,
|
|
91
|
-
component,
|
|
92
|
-
},
|
|
93
|
-
props
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
reply.type('text/html');
|
|
97
|
-
|
|
98
|
-
return renderPreviewPage(html, outAssets);
|
|
99
|
-
}
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Add graceful shutdown handler to prevent requests errors
|
|
105
|
-
await app.register(fastifyGracefulShutdown);
|
|
106
|
-
|
|
107
|
-
if (process.env.ENABLE_K8S_PROBES) {
|
|
108
|
-
await setupK8SProbes(app);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Execute runtime hook in nerest-runtime.ts if it exists
|
|
112
|
-
await runRuntimeHook(app, async () => {
|
|
113
|
-
const glob = import.meta.glob('/nerest-runtime.ts', { eager: true });
|
|
114
|
-
return glob['/nerest-runtime.ts'];
|
|
23
|
+
const runtimeHook = import.meta.glob('/nerest/runtime.ts', { eager: true });
|
|
24
|
+
|
|
25
|
+
const app = await createServer({
|
|
26
|
+
root,
|
|
27
|
+
project,
|
|
28
|
+
apps,
|
|
29
|
+
loadComponent: async (entry: string) =>
|
|
30
|
+
components[`/apps/${entry}/index.tsx`],
|
|
31
|
+
loadPropsHook: async (entry: string) =>
|
|
32
|
+
propsHooks[`/apps/${entry}/props.ts`],
|
|
33
|
+
loadRuntimeHook: async () => runtimeHook['/nerest/runtime.ts'],
|
|
115
34
|
});
|
|
116
35
|
|
|
117
|
-
// TODO: remove hardcoded port
|
|
118
36
|
await app.listen({
|
|
119
37
|
host: '0.0.0.0',
|
|
120
|
-
port
|
|
38
|
+
port,
|
|
121
39
|
});
|
|
122
|
-
|
|
123
|
-
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
124
40
|
}
|
|
125
41
|
|
|
126
|
-
runProductionServer();
|
|
42
|
+
runProductionServer(process.env.PORT ? Number(process.env.PORT) : 3000);
|
package/server/shared.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fastify from 'fastify';
|
|
2
|
+
import type { FastifyInstance } from 'fastify';
|
|
3
|
+
import type { RouteShorthandOptions } from 'fastify';
|
|
4
|
+
import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
|
|
5
|
+
import type { ComponentType } from 'react';
|
|
6
|
+
import { renderApp } from './parts/render.js';
|
|
7
|
+
import { setupSwagger } from './parts/swagger.js';
|
|
8
|
+
import { setupValidator, validator } from './parts/validator.js';
|
|
9
|
+
import { renderPreviewPage } from './parts/preview.js';
|
|
10
|
+
import { setupK8SProbes } from './parts/k8s-probes.js';
|
|
11
|
+
import { runRuntimeHook } from './hooks/runtime.js';
|
|
12
|
+
import { runPropsHook } from './hooks/props.js';
|
|
13
|
+
import { runLoggerHook } from './hooks/logger.js';
|
|
14
|
+
import type { Project } from './loaders/project.js';
|
|
15
|
+
import { loadPreviewParts } from './loaders/preview.js';
|
|
16
|
+
import type { PreviewParts } from './loaders/preview.js';
|
|
17
|
+
import type { AppEntry } from './loaders/apps.js';
|
|
18
|
+
import { randomId } from './utils.js';
|
|
19
|
+
|
|
20
|
+
type ServerOptions = {
|
|
21
|
+
root: string;
|
|
22
|
+
project: Project;
|
|
23
|
+
apps: Record<string, AppEntry>;
|
|
24
|
+
loadComponent: (entry: string) => Promise<ComponentType>;
|
|
25
|
+
loadPropsHook: (entry: string) => Promise<unknown>;
|
|
26
|
+
loadRuntimeHook: () => Promise<unknown>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type ServerOptionsWithPreview = ServerOptions & {
|
|
30
|
+
previewParts: PreviewParts;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export async function createServer(options: ServerOptions) {
|
|
34
|
+
const { project, root, loadRuntimeHook } = options;
|
|
35
|
+
|
|
36
|
+
const app = fastify({
|
|
37
|
+
logger: (await runLoggerHook(loadRuntimeHook)) ?? true,
|
|
38
|
+
ignoreTrailingSlash: true,
|
|
39
|
+
useSemicolonDelimiter: false,
|
|
40
|
+
// JSON parsing can take a long time and blocks the event loop,
|
|
41
|
+
// so we need to limit the size of the body. 10MB is a good compromise
|
|
42
|
+
// baseline that was chosen by experimenting with real world usage
|
|
43
|
+
bodyLimit: 10 * 1024 * 1024,
|
|
44
|
+
genReqId(req): string {
|
|
45
|
+
return String(req.headers['x-request-id'] || randomId());
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Setup payload validation and Swagger based on apps' JSON Schema
|
|
50
|
+
setupValidator(app);
|
|
51
|
+
await setupSwagger(app, project);
|
|
52
|
+
|
|
53
|
+
// Load preview parts from `nerest/preview-{part}.html` files
|
|
54
|
+
const previewParts = await loadPreviewParts(root);
|
|
55
|
+
|
|
56
|
+
await setupRoutes(app, { ...options, previewParts });
|
|
57
|
+
|
|
58
|
+
// Add graceful shutdown handler to prevent requests errors
|
|
59
|
+
await app.register(fastifyGracefulShutdown);
|
|
60
|
+
|
|
61
|
+
if (process.env.ENABLE_K8S_PROBES) {
|
|
62
|
+
await setupK8SProbes(app);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Execute runtime hook in nerest/runtime.ts if it exists
|
|
66
|
+
await runRuntimeHook(app, loadRuntimeHook);
|
|
67
|
+
|
|
68
|
+
return app;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function setupRoutes(
|
|
72
|
+
app: FastifyInstance,
|
|
73
|
+
options: ServerOptionsWithPreview
|
|
74
|
+
) {
|
|
75
|
+
const { project, apps, previewParts, loadComponent, loadPropsHook } = options;
|
|
76
|
+
|
|
77
|
+
for (const appEntry of Object.values(apps)) {
|
|
78
|
+
const { name, examples, schema, assets } = appEntry;
|
|
79
|
+
|
|
80
|
+
const routeOptions: RouteShorthandOptions = {};
|
|
81
|
+
|
|
82
|
+
// TODO: report error if schema is missing, making it mandatory
|
|
83
|
+
if (schema) {
|
|
84
|
+
routeOptions.schema = {
|
|
85
|
+
summary: schema.description as string,
|
|
86
|
+
// Tags are used to group routes in Swagger UI
|
|
87
|
+
tags: [name],
|
|
88
|
+
body: {
|
|
89
|
+
...schema,
|
|
90
|
+
// Examples are also displayed in Swagger UI
|
|
91
|
+
examples: Object.values(examples),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// POST /api/{name} -> render app with request.body as props
|
|
97
|
+
app.post(`/api/${name}`, routeOptions, async (request) => {
|
|
98
|
+
const component = await loadComponent(name);
|
|
99
|
+
const props = await runPropsHook(
|
|
100
|
+
app,
|
|
101
|
+
() => loadPropsHook(name),
|
|
102
|
+
request.body
|
|
103
|
+
);
|
|
104
|
+
return renderApp({ name, assets, component, project }, props);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
for (const [exampleName, example] of Object.entries(examples)) {
|
|
108
|
+
// Validate examples against schema
|
|
109
|
+
if (schema && !validator.validate(schema, example)) {
|
|
110
|
+
app.log.error(
|
|
111
|
+
`Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator.errorsText()}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// GET /api/{name}/examples/{example} -> render a preview page
|
|
116
|
+
const exampleRoute = `/api/${name}/examples/${exampleName}`;
|
|
117
|
+
app.get(
|
|
118
|
+
exampleRoute,
|
|
119
|
+
{
|
|
120
|
+
schema: {
|
|
121
|
+
// Add clickable link to go to the example in Swagger UI
|
|
122
|
+
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
123
|
+
// Place examples under the same tag as the app
|
|
124
|
+
tags: [name],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
async (request, reply) => {
|
|
128
|
+
const component = await loadComponent(name);
|
|
129
|
+
const props = await runPropsHook(
|
|
130
|
+
app,
|
|
131
|
+
() => loadPropsHook(name),
|
|
132
|
+
example
|
|
133
|
+
);
|
|
134
|
+
const { html, assets: outAssets } = renderApp(
|
|
135
|
+
{
|
|
136
|
+
name,
|
|
137
|
+
assets,
|
|
138
|
+
component,
|
|
139
|
+
project,
|
|
140
|
+
},
|
|
141
|
+
props
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
reply.type('text/html');
|
|
145
|
+
return renderPreviewPage(html, outAssets, previewParts);
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
package/server/utils.ts
ADDED
|
@@ -1,37 +0,0 @@
|
|
|
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';
|
|
7
|
-
// Build the record of the available apps by convention
|
|
8
|
-
// apps -> /apps/{name}/index.tsx
|
|
9
|
-
// examples -> /apps/{name}/examples/{example}.json
|
|
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 }))
|
|
14
|
-
.filter((d) => d.isDirectory())
|
|
15
|
-
.map((d) => d.name);
|
|
16
|
-
const apps = [];
|
|
17
|
-
for (const appDir of appsDirs) {
|
|
18
|
-
apps.push(await loadApp(appsRoot, appDir, manifest, deployedStaticPath));
|
|
19
|
-
}
|
|
20
|
-
return Object.fromEntries(apps);
|
|
21
|
-
}
|
|
22
|
-
async function loadApp(appsRoot, name, manifest, deployedStaticPath) {
|
|
23
|
-
// TODO: report problems with loading entries, assets and/or examples
|
|
24
|
-
const appRoot = path.join(appsRoot, name);
|
|
25
|
-
return [
|
|
26
|
-
name,
|
|
27
|
-
{
|
|
28
|
-
name,
|
|
29
|
-
root: appRoot,
|
|
30
|
-
entry: path.join(appRoot, 'index.tsx'),
|
|
31
|
-
propsHookEntry: path.join(appRoot, 'props.ts'),
|
|
32
|
-
assets: loadAppAssets(name, manifest, deployedStaticPath),
|
|
33
|
-
examples: await loadAppExamples(appRoot),
|
|
34
|
-
schema: await loadAppSchema(appRoot),
|
|
35
|
-
},
|
|
36
|
-
];
|
|
37
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function runPropsHook(props: unknown, loader: () => Promise<unknown>): Promise<Record<string, unknown>>;
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
}
|
package/server/parts/apps.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
import type { Manifest } from 'vite';
|
|
4
|
-
|
|
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
|
-
|
|
10
|
-
export type AppEntry = {
|
|
11
|
-
name: string;
|
|
12
|
-
root: string;
|
|
13
|
-
entry: string;
|
|
14
|
-
propsHookEntry: string;
|
|
15
|
-
assets: string[];
|
|
16
|
-
examples: Record<string, unknown>;
|
|
17
|
-
schema: Record<string, unknown> | null;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// Build the record of the available apps by convention
|
|
21
|
-
// apps -> /apps/{name}/index.tsx
|
|
22
|
-
// examples -> /apps/{name}/examples/{example}.json
|
|
23
|
-
export async function loadApps(root: string, deployedStaticPath: string) {
|
|
24
|
-
const appsRoot = path.join(root, 'apps');
|
|
25
|
-
const manifest = await loadAppManifest(root);
|
|
26
|
-
|
|
27
|
-
const appsDirs = (await fs.readdir(appsRoot, { withFileTypes: true }))
|
|
28
|
-
.filter((d) => d.isDirectory())
|
|
29
|
-
.map((d) => d.name);
|
|
30
|
-
|
|
31
|
-
const apps: Array<[name: string, entry: AppEntry]> = [];
|
|
32
|
-
for (const appDir of appsDirs) {
|
|
33
|
-
apps.push(await loadApp(appsRoot, appDir, manifest, deployedStaticPath));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return Object.fromEntries(apps);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function loadApp(
|
|
40
|
-
appsRoot: string,
|
|
41
|
-
name: string,
|
|
42
|
-
manifest: Manifest,
|
|
43
|
-
deployedStaticPath: string
|
|
44
|
-
): Promise<[name: string, entry: AppEntry]> {
|
|
45
|
-
// TODO: report problems with loading entries, assets and/or examples
|
|
46
|
-
const appRoot = path.join(appsRoot, name);
|
|
47
|
-
return [
|
|
48
|
-
name,
|
|
49
|
-
{
|
|
50
|
-
name,
|
|
51
|
-
root: appRoot,
|
|
52
|
-
entry: path.join(appRoot, 'index.tsx'),
|
|
53
|
-
propsHookEntry: path.join(appRoot, 'props.ts'),
|
|
54
|
-
assets: loadAppAssets(name, manifest, deployedStaticPath),
|
|
55
|
-
examples: await loadAppExamples(appRoot),
|
|
56
|
-
schema: await loadAppSchema(appRoot),
|
|
57
|
-
},
|
|
58
|
-
];
|
|
59
|
-
}
|