@nerest/nerest 0.0.7 → 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/bin/build.ts +1 -1
- package/bin/index.ts +2 -2
- package/bin/watch.ts +1 -1
- package/build/index.ts +6 -6
- package/client/index.ts +5 -1
- package/dist/bin/build.js +3 -7
- package/dist/bin/index.js +5 -7
- package/dist/bin/watch.js +3 -7
- package/dist/build/excludes/index.js +1 -5
- package/dist/build/index.js +17 -24
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +32 -0
- package/dist/server/development.js +35 -39
- 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/loaders/assets.js +1 -5
- package/dist/server/loaders/examples.js +10 -17
- package/dist/server/loaders/manifest.js +5 -12
- package/dist/server/loaders/schema.js +7 -14
- package/dist/server/parts/apps.d.ts +1 -0
- package/dist/server/parts/apps.js +16 -22
- package/dist/server/parts/k8s-probes.js +1 -5
- package/dist/server/parts/preview.js +1 -5
- package/dist/server/parts/props-hook.d.ts +1 -0
- package/dist/server/parts/props-hook.js +8 -0
- package/dist/server/parts/render.js +6 -13
- package/dist/server/parts/runtime-hook.d.ts +1 -1
- package/dist/server/parts/runtime-hook.js +1 -5
- package/dist/server/parts/swagger.js +8 -15
- package/dist/server/parts/validator.d.ts +1 -1
- package/dist/server/parts/validator.js +9 -12
- package/dist/server/production.d.ts +1 -0
- package/dist/server/production.js +93 -0
- package/package.json +4 -3
- package/server/development.ts +20 -13
- package/server/hooks/props.ts +22 -0
- package/server/{parts/runtime-hook.ts → hooks/runtime.ts} +1 -1
- package/server/parts/apps.ts +6 -4
- package/server/parts/validator.ts +5 -2
- package/server/production.ts +19 -15
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
-
const assets_1 = require("../loaders/assets");
|
|
10
|
-
const examples_1 = require("../loaders/examples");
|
|
11
|
-
const schema_1 = require("../loaders/schema");
|
|
12
|
-
const manifest_1 = require("../loaders/manifest");
|
|
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';
|
|
13
7
|
// Build the record of the available apps by convention
|
|
14
8
|
// apps -> /apps/{name}/index.tsx
|
|
15
9
|
// examples -> /apps/{name}/examples/{example}.json
|
|
16
|
-
async function loadApps(root, deployedStaticPath) {
|
|
17
|
-
const appsRoot =
|
|
18
|
-
const manifest = await
|
|
19
|
-
const appsDirs = (await
|
|
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 }))
|
|
20
14
|
.filter((d) => d.isDirectory())
|
|
21
15
|
.map((d) => d.name);
|
|
22
16
|
const apps = [];
|
|
@@ -25,19 +19,19 @@ async function loadApps(root, deployedStaticPath) {
|
|
|
25
19
|
}
|
|
26
20
|
return Object.fromEntries(apps);
|
|
27
21
|
}
|
|
28
|
-
exports.loadApps = loadApps;
|
|
29
22
|
async function loadApp(appsRoot, name, manifest, deployedStaticPath) {
|
|
30
23
|
// TODO: report problems with loading entries, assets and/or examples
|
|
31
|
-
const appRoot =
|
|
24
|
+
const appRoot = path.join(appsRoot, name);
|
|
32
25
|
return [
|
|
33
26
|
name,
|
|
34
27
|
{
|
|
35
28
|
name,
|
|
36
29
|
root: appRoot,
|
|
37
|
-
entry:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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),
|
|
41
35
|
},
|
|
42
36
|
];
|
|
43
37
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setupK8SProbes = void 0;
|
|
4
1
|
// Setup routes for k8s probes to check if application is live
|
|
5
|
-
async function setupK8SProbes(app) {
|
|
2
|
+
export async function setupK8SProbes(app) {
|
|
6
3
|
// Handler for graceful shutdowns
|
|
7
4
|
// K8s can initiate shutdown at any moment: on pods restart or on deploy.
|
|
8
5
|
// So, if we receive shutdown request, we:
|
|
@@ -28,4 +25,3 @@ async function setupK8SProbes(app) {
|
|
|
28
25
|
}
|
|
29
26
|
});
|
|
30
27
|
}
|
|
31
|
-
exports.setupK8SProbes = setupK8SProbes;
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.renderPreviewPage = void 0;
|
|
4
1
|
// Renders the preview page available by convention at /api/{name}/examples/{example}
|
|
5
|
-
function renderPreviewPage(html, assets) {
|
|
2
|
+
export function renderPreviewPage(html, assets) {
|
|
6
3
|
const { scripts, styles } = mapAssets(assets);
|
|
7
4
|
return `
|
|
8
5
|
<html>
|
|
@@ -16,7 +13,6 @@ function renderPreviewPage(html, assets) {
|
|
|
16
13
|
</html>
|
|
17
14
|
`;
|
|
18
15
|
}
|
|
19
|
-
exports.renderPreviewPage = renderPreviewPage;
|
|
20
16
|
function mapAssets(assets) {
|
|
21
17
|
// TODO: script type="module" is not supported by older browsers
|
|
22
18
|
// but vite doesn't provide `nomodule` fallback by default
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runPropsHook(props: unknown, loader: () => Promise<unknown>): Promise<Record<string, unknown>>;
|
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.renderApp = void 0;
|
|
7
|
-
const react_1 = __importDefault(require("react"));
|
|
8
|
-
const server_1 = require("react-dom/server");
|
|
9
|
-
const nanoid_1 = require("nanoid");
|
|
10
|
-
function renderApp({ name, assets, component }, props = {}) {
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderToString } from 'react-dom/server';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
export function renderApp({ name, assets, component }, props = {}) {
|
|
11
5
|
const html = renderSsrComponent(name, component, props);
|
|
12
6
|
return { html, assets };
|
|
13
7
|
}
|
|
14
|
-
exports.renderApp = renderApp;
|
|
15
8
|
function renderSsrComponent(appName, appComponent, props) {
|
|
16
|
-
const html =
|
|
9
|
+
const html = renderToString(React.createElement(appComponent, props));
|
|
17
10
|
// There may be multiple instances of the same app on the page,
|
|
18
11
|
// so we will use a randomized id to avoid collisions
|
|
19
|
-
const appId =
|
|
12
|
+
const appId = nanoid();
|
|
20
13
|
// data-app-name and data-app-id are used by client entrypoint to hydrate
|
|
21
14
|
// apps using correct serialized props
|
|
22
15
|
const container = `<div data-app-name="${appName}" data-app-id="${appId}">${html}</div>`;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { FastifyInstance } from 'fastify';
|
|
2
|
-
export declare function runRuntimeHook(app: FastifyInstance, loader: () => Promise<
|
|
2
|
+
export declare function runRuntimeHook(app: FastifyInstance, loader: () => Promise<unknown>): Promise<void>;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runRuntimeHook = void 0;
|
|
4
1
|
// Load the runtime hook module and run it if it exists, passing down our
|
|
5
2
|
// fastify instance. This hook can be used to modify fastify settings, add
|
|
6
3
|
// plugins or routes on an individual app level.
|
|
7
|
-
async function runRuntimeHook(app, loader) {
|
|
4
|
+
export async function runRuntimeHook(app, loader) {
|
|
8
5
|
let module;
|
|
9
6
|
try {
|
|
10
7
|
module = (await loader());
|
|
@@ -29,4 +26,3 @@ async function runRuntimeHook(app, loader) {
|
|
|
29
26
|
console.log('Runtime hook not found, skipping...');
|
|
30
27
|
}
|
|
31
28
|
}
|
|
32
|
-
exports.runRuntimeHook = runRuntimeHook;
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.setupSwagger = void 0;
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const fs_1 = __importDefault(require("fs"));
|
|
9
|
-
const swagger_1 = __importDefault(require("@fastify/swagger"));
|
|
10
|
-
const swagger_ui_1 = __importDefault(require("@fastify/swagger-ui"));
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import fastifySwagger from '@fastify/swagger';
|
|
4
|
+
import fastifySwaggerUi from '@fastify/swagger-ui';
|
|
11
5
|
// Setup automatic OpenAPI specification compilation and enable
|
|
12
6
|
// Swagger UI at the `/api` route
|
|
13
|
-
async function setupSwagger(app) {
|
|
7
|
+
export async function setupSwagger(app) {
|
|
14
8
|
let appInfo = {};
|
|
15
9
|
try {
|
|
16
|
-
const packageJson =
|
|
10
|
+
const packageJson = fs.readFileSync(path.join(process.cwd(), 'package.json'), { encoding: 'utf-8' });
|
|
17
11
|
appInfo = JSON.parse(packageJson);
|
|
18
12
|
}
|
|
19
13
|
catch (e) {
|
|
@@ -24,7 +18,7 @@ async function setupSwagger(app) {
|
|
|
24
18
|
(typeof appInfo.repository === 'string'
|
|
25
19
|
? appInfo.repository
|
|
26
20
|
: appInfo.repository?.url);
|
|
27
|
-
await app.register(
|
|
21
|
+
await app.register(fastifySwagger, {
|
|
28
22
|
openapi: {
|
|
29
23
|
info: {
|
|
30
24
|
title: appInfo.name ?? 'Nerest micro frontend',
|
|
@@ -43,8 +37,7 @@ async function setupSwagger(app) {
|
|
|
43
37
|
},
|
|
44
38
|
},
|
|
45
39
|
});
|
|
46
|
-
await app.register(
|
|
40
|
+
await app.register(fastifySwaggerUi, {
|
|
47
41
|
routePrefix: '/api',
|
|
48
42
|
});
|
|
49
43
|
}
|
|
50
|
-
exports.setupSwagger = setupSwagger;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import Ajv from 'ajv';
|
|
2
|
-
export declare const validator: Ajv;
|
|
2
|
+
export declare const validator: Ajv.default;
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const fast_uri_1 = __importDefault(require("fast-uri"));
|
|
9
|
-
const ajv_formats_1 = __importDefault(require("ajv-formats"));
|
|
10
|
-
exports.validator = new ajv_1.default({
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import fastUri from 'fast-uri';
|
|
3
|
+
import addFormats from 'ajv-formats';
|
|
4
|
+
// Ajv default export is broken, so we have to specify `.default`
|
|
5
|
+
// manually: https://github.com/ajv-validator/ajv/issues/2132
|
|
6
|
+
// eslint-disable-next-line new-cap
|
|
7
|
+
export const validator = new Ajv.default({
|
|
11
8
|
coerceTypes: 'array',
|
|
12
9
|
useDefaults: true,
|
|
13
10
|
removeAdditional: true,
|
|
14
|
-
uriResolver:
|
|
11
|
+
uriResolver: fastUri,
|
|
15
12
|
addUsedSchema: false,
|
|
16
13
|
// Explicitly set allErrors to `false`.
|
|
17
14
|
// When set to `true`, a DoS attack is possible.
|
|
@@ -20,4 +17,4 @@ exports.validator = new ajv_1.default({
|
|
|
20
17
|
// Support additional type formats in JSON schema like `date`,
|
|
21
18
|
// `email`, `url`, etc. Used by default in fastify
|
|
22
19
|
// https://www.npmjs.com/package/ajv-formats
|
|
23
|
-
|
|
20
|
+
addFormats.default(validator);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// This is the nerest production server entrypoint
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import fastify from 'fastify';
|
|
5
|
+
import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
|
|
6
|
+
import { renderApp } from './parts/render.js';
|
|
7
|
+
import { setupSwagger } from './parts/swagger.js';
|
|
8
|
+
import { 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
|
+
// TODO: refactor to merge the similar parts between production and development server?
|
|
14
|
+
async function runProductionServer() {
|
|
15
|
+
const root = process.cwd();
|
|
16
|
+
// TODO: error handling for file reading
|
|
17
|
+
const apps = JSON.parse(await fs.readFile(path.join(root, 'build/nerest-manifest.json'), {
|
|
18
|
+
encoding: 'utf-8',
|
|
19
|
+
}));
|
|
20
|
+
const components = import.meta.glob('/apps/*/index.tsx', {
|
|
21
|
+
import: 'default',
|
|
22
|
+
eager: true,
|
|
23
|
+
});
|
|
24
|
+
const propsHooks = import.meta.glob('/apps/*/props.ts', {
|
|
25
|
+
eager: true,
|
|
26
|
+
});
|
|
27
|
+
const app = fastify();
|
|
28
|
+
// Setup schema validation. We have to use our own ajv instance that
|
|
29
|
+
// we can use both to validate request bodies and examples against
|
|
30
|
+
// app schemas
|
|
31
|
+
app.setValidatorCompiler(({ schema }) => validator.compile(schema));
|
|
32
|
+
await setupSwagger(app);
|
|
33
|
+
for (const appEntry of Object.values(apps)) {
|
|
34
|
+
const { name, examples, schema, assets } = appEntry;
|
|
35
|
+
const component = components[`/apps/${name}/index.tsx`];
|
|
36
|
+
const propsHook = async () => propsHooks[`/apps/${name}/props.ts`];
|
|
37
|
+
const routeOptions = {};
|
|
38
|
+
// TODO: report error if schema is missing, unless this app is client-only
|
|
39
|
+
// TODO: disallow apps without schemas in production build
|
|
40
|
+
if (schema) {
|
|
41
|
+
routeOptions.schema = {
|
|
42
|
+
// Use description as Swagger summary, since summary is visible
|
|
43
|
+
// even when the route is collapsed in the UI
|
|
44
|
+
summary: schema.description,
|
|
45
|
+
// TODO: do we need to mix in examples like in the development server?
|
|
46
|
+
body: schema,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// POST /api/{name} -> render app with request.body as props
|
|
50
|
+
app.post(`/api/${name}`, routeOptions, async (request) => {
|
|
51
|
+
const props = await runPropsHook(request.body, propsHook);
|
|
52
|
+
return renderApp({ name, assets, component }, props);
|
|
53
|
+
});
|
|
54
|
+
for (const [exampleName, example] of Object.entries(examples)) {
|
|
55
|
+
// GET /api/{name}/examples/{example} -> render a preview page
|
|
56
|
+
// with a predefined example body
|
|
57
|
+
const exampleRoute = `/api/${name}/examples/${exampleName}`;
|
|
58
|
+
app.get(exampleRoute, {
|
|
59
|
+
schema: {
|
|
60
|
+
// Add a clickable link to the example route in route's Swagger
|
|
61
|
+
// description so it's easier to navigate to
|
|
62
|
+
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
63
|
+
},
|
|
64
|
+
}, async (_, reply) => {
|
|
65
|
+
const props = await runPropsHook(example, propsHook);
|
|
66
|
+
const { html, assets: outAssets } = renderApp({
|
|
67
|
+
name,
|
|
68
|
+
assets,
|
|
69
|
+
component,
|
|
70
|
+
}, props);
|
|
71
|
+
reply.type('text/html');
|
|
72
|
+
return renderPreviewPage(html, outAssets);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Add graceful shutdown handler to prevent requests errors
|
|
77
|
+
await app.register(fastifyGracefulShutdown);
|
|
78
|
+
if (process.env.ENABLE_K8S_PROBES) {
|
|
79
|
+
await setupK8SProbes(app);
|
|
80
|
+
}
|
|
81
|
+
// Execute runtime hook in nerest-runtime.ts if it exists
|
|
82
|
+
await runRuntimeHook(app, async () => {
|
|
83
|
+
const glob = import.meta.glob('/nerest-runtime.ts', { eager: true });
|
|
84
|
+
return glob['/nerest-runtime.ts'];
|
|
85
|
+
});
|
|
86
|
+
// TODO: remove hardcoded port
|
|
87
|
+
await app.listen({
|
|
88
|
+
host: '0.0.0.0',
|
|
89
|
+
port: 3000,
|
|
90
|
+
});
|
|
91
|
+
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
92
|
+
}
|
|
93
|
+
runProductionServer();
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerest/nerest",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "React micro frontend framework",
|
|
5
5
|
"homepage": "https://github.com/nerestjs/nerest#readme",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/nerestjs/nerest.git"
|
|
9
9
|
},
|
|
10
|
+
"type": "module",
|
|
10
11
|
"bin": {
|
|
11
12
|
"nerest": "dist/bin/index.js"
|
|
12
13
|
},
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
"fast-uri": "^2.3.0",
|
|
62
63
|
"fastify": "^4.24.3",
|
|
63
64
|
"fastify-graceful-shutdown": "^3.5.1",
|
|
64
|
-
"nanoid": "^
|
|
65
|
+
"nanoid": "^5.0.2",
|
|
65
66
|
"vite": "^4.5.0",
|
|
66
67
|
"vite-plugin-externals": "^0.6.2"
|
|
67
68
|
},
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
"@tinkoff/eslint-config": "^1.54.4",
|
|
70
71
|
"@tinkoff/eslint-config-react": "^1.54.4",
|
|
71
72
|
"@tinkoff/prettier-config": "^1.52.1",
|
|
72
|
-
"@types/react": "^18.2.
|
|
73
|
+
"@types/react": "^18.2.34",
|
|
73
74
|
"@types/react-dom": "^18.2.14",
|
|
74
75
|
"jest": "^29.7.0",
|
|
75
76
|
"json-schema-to-typescript": "^13.1.1",
|
package/server/development.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import type { ServerResponse } from 'http';
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { build, createServer } from 'vite';
|
|
6
6
|
import type { InlineConfig } from 'vite';
|
|
7
7
|
import type { RollupWatcher, RollupWatcherEvent } from 'rollup';
|
|
8
8
|
|
|
@@ -11,13 +11,14 @@ import fastify from 'fastify';
|
|
|
11
11
|
import fastifyStatic from '@fastify/static';
|
|
12
12
|
import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
|
|
13
13
|
|
|
14
|
-
import { loadApps } from './parts/apps';
|
|
15
|
-
import { renderApp } from './parts/render';
|
|
16
|
-
import { renderPreviewPage } from './parts/preview';
|
|
17
|
-
import { validator } from './parts/validator';
|
|
18
|
-
import { setupSwagger } from './parts/swagger';
|
|
19
|
-
import { setupK8SProbes } from './parts/k8s-probes';
|
|
20
|
-
import { runRuntimeHook } from './
|
|
14
|
+
import { loadApps } from './parts/apps.js';
|
|
15
|
+
import { renderApp } from './parts/render.js';
|
|
16
|
+
import { renderPreviewPage } from './parts/preview.js';
|
|
17
|
+
import { validator } from './parts/validator.js';
|
|
18
|
+
import { setupSwagger } from './parts/swagger.js';
|
|
19
|
+
import { setupK8SProbes } from './parts/k8s-probes.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() {
|
|
@@ -63,7 +64,7 @@ export async function runDevelopmentServer() {
|
|
|
63
64
|
const apps = await loadApps(root, 'http://0.0.0.0:3000/');
|
|
64
65
|
|
|
65
66
|
// Start vite server that will be rendering SSR components
|
|
66
|
-
const viteSsr = await
|
|
67
|
+
const viteSsr = await createServer(config);
|
|
67
68
|
const app = fastify();
|
|
68
69
|
|
|
69
70
|
// Setup schema validation. We have to use our own ajv instance that
|
|
@@ -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');
|
|
@@ -187,7 +194,7 @@ export async function runDevelopmentServer() {
|
|
|
187
194
|
|
|
188
195
|
// TODO: this should probably be moved from here
|
|
189
196
|
async function startClientBuildWatcher(config: InlineConfig) {
|
|
190
|
-
const watcher = (await
|
|
197
|
+
const watcher = (await build(config)) as RollupWatcher;
|
|
191
198
|
return new Promise<void>((resolve) => {
|
|
192
199
|
// We need to have a built manifest.json to provide assets
|
|
193
200
|
// links in SSR. We will wait for rollup to report when it
|
|
@@ -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
|
+
}
|
|
@@ -9,7 +9,7 @@ type RuntimeHookModule = {
|
|
|
9
9
|
// plugins or routes on an individual app level.
|
|
10
10
|
export async function runRuntimeHook(
|
|
11
11
|
app: FastifyInstance,
|
|
12
|
-
loader: () => Promise<
|
|
12
|
+
loader: () => Promise<unknown>
|
|
13
13
|
) {
|
|
14
14
|
let module: RuntimeHookModule | undefined;
|
|
15
15
|
|
package/server/parts/apps.ts
CHANGED
|
@@ -2,15 +2,16 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import type { Manifest } from 'vite';
|
|
4
4
|
|
|
5
|
-
import { loadAppAssets } from '../loaders/assets';
|
|
6
|
-
import { loadAppExamples } from '../loaders/examples';
|
|
7
|
-
import { loadAppSchema } from '../loaders/schema';
|
|
8
|
-
import { loadAppManifest } from '../loaders/manifest';
|
|
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
9
|
|
|
10
10
|
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),
|
|
@@ -2,7 +2,10 @@ import Ajv from 'ajv';
|
|
|
2
2
|
import fastUri from 'fast-uri';
|
|
3
3
|
import addFormats from 'ajv-formats';
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
// Ajv default export is broken, so we have to specify `.default`
|
|
6
|
+
// manually: https://github.com/ajv-validator/ajv/issues/2132
|
|
7
|
+
// eslint-disable-next-line new-cap
|
|
8
|
+
export const validator = new Ajv.default({
|
|
6
9
|
coerceTypes: 'array',
|
|
7
10
|
useDefaults: true,
|
|
8
11
|
removeAdditional: true,
|
|
@@ -16,4 +19,4 @@ export const validator = new Ajv({
|
|
|
16
19
|
// Support additional type formats in JSON schema like `date`,
|
|
17
20
|
// `email`, `url`, etc. Used by default in fastify
|
|
18
21
|
// https://www.npmjs.com/package/ajv-formats
|
|
19
|
-
addFormats(validator);
|
|
22
|
+
addFormats.default(validator);
|
package/server/production.ts
CHANGED
|
@@ -6,13 +6,14 @@ import fastify from 'fastify';
|
|
|
6
6
|
import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
|
|
7
7
|
import type { RouteShorthandOptions } from 'fastify';
|
|
8
8
|
|
|
9
|
-
import type { AppEntry } from './parts/apps';
|
|
10
|
-
import { renderApp } from './parts/render';
|
|
11
|
-
import { setupSwagger } from './parts/swagger';
|
|
12
|
-
import { validator } from './parts/validator';
|
|
13
|
-
import { renderPreviewPage } from './parts/preview';
|
|
14
|
-
import { setupK8SProbes } from './parts/k8s-probes';
|
|
15
|
-
import { runRuntimeHook } from './
|
|
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';
|
|
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');
|