@nerest/nerest 0.0.1 → 0.0.3
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 +16 -2
- package/dist/server/app-parts/schema.d.ts +1 -0
- package/dist/server/app-parts/schema.js +21 -0
- package/dist/server/apps.d.ts +2 -1
- package/dist/server/apps.js +2 -0
- package/dist/server/index.js +39 -4
- package/dist/server/swagger.d.ts +2 -0
- package/dist/server/swagger.js +50 -0
- package/dist/server/validator.d.ts +2 -0
- package/dist/server/validator.js +23 -0
- package/package.json +18 -13
- package/server/app-parts/schema.ts +18 -0
- package/server/apps.ts +3 -0
- package/server/index.ts +64 -17
- package/server/swagger.ts +58 -0
- package/server/validator.ts +19 -0
package/README.md
CHANGED
|
@@ -10,14 +10,28 @@ React micro frontend framework
|
|
|
10
10
|
npm i --save @nerest/nerest react react-dom
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Points of Interest
|
|
14
|
+
|
|
15
|
+
- SSR server entry: [server/index.ts](server/index.ts)
|
|
16
|
+
- Hydrating client entry: [client/index.ts](client/index.ts)
|
|
17
|
+
- CLI entry: [bin/index.ts](bin/index.ts)
|
|
18
|
+
|
|
13
19
|
## Conventions
|
|
14
20
|
|
|
15
21
|
The `apps` directory must contain all of the apps provided by the micro frontend. E.g. `/apps/foo/index.tsx` is the entrypoint component of the `foo` app. It becomes available as the `/api/foo` route of the micro frontend server.
|
|
16
22
|
|
|
17
|
-
The single app directory may contain an `examples` subdirectory with example JSON files which can be used as props for the app entrypoint component. E.g. `/apps/foo/examples/example-1.json` becomes available as the `/api/foo/examples/example-1` route of the micro frontend server.
|
|
18
|
-
|
|
19
23
|
See [nerest-harness](https://github.com/nerestjs/harness) for the minimal example of a nerest micro frontend.
|
|
20
24
|
|
|
25
|
+
### Examples (`/examples/*.json`)
|
|
26
|
+
|
|
27
|
+
The app directory may contain an `examples` subdirectory with example JSON files which can be used as props for the app entrypoint component. E.g. `/apps/foo/examples/example-1.json` becomes available as the `/api/foo/examples/example-1` route of the micro frontend server.
|
|
28
|
+
|
|
29
|
+
### Schema (`/schema.json`)
|
|
30
|
+
|
|
31
|
+
The app directory should contain a `schema.json` file that describes the schema of a request body for this specific app in the [JSON Schema language](https://json-schema.org/). All requests sent to this app, and app examples from the `examples` subdirectory will be validated against this schema. `ajv` and `ajv-formats` are used for validation, so all JSON Schema features implemented by them are supported.
|
|
32
|
+
|
|
33
|
+
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`.
|
|
34
|
+
|
|
21
35
|
## Development
|
|
22
36
|
|
|
23
37
|
Run the build script to build the framework.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadAppSchema(appRoot: string): Promise<any>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadAppSchema = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
|
+
// Loads and parses the schema file for a specific app
|
|
11
|
+
async function loadAppSchema(appRoot) {
|
|
12
|
+
const schemaPath = path_1.default.join(appRoot, 'schema.json');
|
|
13
|
+
let schema = null;
|
|
14
|
+
// TODO: error handling and reporting
|
|
15
|
+
if ((0, fs_1.existsSync)(schemaPath)) {
|
|
16
|
+
const file = await promises_1.default.readFile(schemaPath, { encoding: 'utf-8' });
|
|
17
|
+
schema = JSON.parse(file);
|
|
18
|
+
}
|
|
19
|
+
return schema;
|
|
20
|
+
}
|
|
21
|
+
exports.loadAppSchema = loadAppSchema;
|
package/dist/server/apps.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type AppEntry = {
|
|
2
2
|
name: string;
|
|
3
3
|
root: string;
|
|
4
4
|
entry: string;
|
|
5
5
|
assets: string[];
|
|
6
6
|
examples: Record<string, unknown>;
|
|
7
|
+
schema: Record<string, unknown> | null;
|
|
7
8
|
};
|
|
8
9
|
export declare function loadApps(root: string): Promise<{
|
|
9
10
|
[k: string]: AppEntry;
|
package/dist/server/apps.js
CHANGED
|
@@ -8,6 +8,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
9
|
const assets_1 = require("./app-parts/assets");
|
|
10
10
|
const examples_1 = require("./app-parts/examples");
|
|
11
|
+
const schema_1 = require("./app-parts/schema");
|
|
11
12
|
// Build the record of the available apps by convention
|
|
12
13
|
// apps -> /apps/{name}/index.tsx
|
|
13
14
|
// examples -> /apps/{name}/examples/{example}.json
|
|
@@ -35,6 +36,7 @@ async function loadApp(appsRoot, name, manifest) {
|
|
|
35
36
|
entry: path_1.default.join(appRoot, 'index.tsx'),
|
|
36
37
|
assets: (0, assets_1.loadAppAssets)(manifest, name),
|
|
37
38
|
examples: await (0, examples_1.loadAppExamples)(appRoot),
|
|
39
|
+
schema: await (0, schema_1.loadAppSchema)(appRoot),
|
|
38
40
|
},
|
|
39
41
|
];
|
|
40
42
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -12,6 +12,8 @@ const static_1 = __importDefault(require("@fastify/static"));
|
|
|
12
12
|
const apps_1 = require("./apps");
|
|
13
13
|
const render_1 = require("./render");
|
|
14
14
|
const preview_1 = require("./preview");
|
|
15
|
+
const validator_1 = require("./validator");
|
|
16
|
+
const swagger_1 = require("./swagger");
|
|
15
17
|
// TODO: this turned out to be a dev server, production server
|
|
16
18
|
// will most likely be implemented separately
|
|
17
19
|
async function createServer() {
|
|
@@ -47,10 +49,31 @@ async function createServer() {
|
|
|
47
49
|
// Start vite server that will be rendering SSR components
|
|
48
50
|
const viteSsr = await vite_1.default.createServer(config);
|
|
49
51
|
const app = (0, fastify_1.default)();
|
|
52
|
+
// Setup schema validation. We have to use our own ajv instance that
|
|
53
|
+
// we can use both to validate request bodies and examples against
|
|
54
|
+
// app schemas
|
|
55
|
+
app.setValidatorCompiler(({ schema }) => validator_1.validator.compile(schema));
|
|
56
|
+
await (0, swagger_1.setupSwagger)(app);
|
|
50
57
|
for (const appEntry of Object.values(apps)) {
|
|
51
|
-
const { name, entry, examples } = appEntry;
|
|
58
|
+
const { name, entry, examples, schema } = appEntry;
|
|
59
|
+
const routeOptions = {};
|
|
60
|
+
// TODO: report error if schema is missing, unless this app is client-only.
|
|
61
|
+
// TODO: disallow apps without schemas in production build
|
|
62
|
+
if (schema) {
|
|
63
|
+
routeOptions.schema = {
|
|
64
|
+
// Use description as Swagger summary, since summary is visible
|
|
65
|
+
// even when the route is collapsed in the UI
|
|
66
|
+
summary: schema.description,
|
|
67
|
+
body: {
|
|
68
|
+
...schema,
|
|
69
|
+
// Mix examples into the schema so they become accessible
|
|
70
|
+
// in the Swagger UI
|
|
71
|
+
examples: Object.values(examples),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
52
75
|
// POST /api/{name} -> render app with request.body as props
|
|
53
|
-
app.post(`/api/${name}`, async (request) => {
|
|
76
|
+
app.post(`/api/${name}`, routeOptions, async (request) => {
|
|
54
77
|
// ssrLoadModule drives the "hot-reload" logic, and allows
|
|
55
78
|
// picking up changes to the source without restarting the server
|
|
56
79
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
@@ -59,9 +82,21 @@ async function createServer() {
|
|
|
59
82
|
return (0, render_1.renderApp)(appEntry, ssrComponent.default, request.body);
|
|
60
83
|
});
|
|
61
84
|
for (const [exampleName, example] of Object.entries(examples)) {
|
|
85
|
+
// Validate example against schema when specified
|
|
86
|
+
if (schema && !validator_1.validator.validate(schema, example)) {
|
|
87
|
+
// TODO: use logger and display errors more prominently
|
|
88
|
+
console.error(`Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator_1.validator.errorsText()}`);
|
|
89
|
+
}
|
|
62
90
|
// GET /api/{name}/examples/{example} -> render a preview page
|
|
63
91
|
// with a predefined example body
|
|
64
|
-
|
|
92
|
+
const exampleRoute = `/api/${name}/examples/${exampleName}`;
|
|
93
|
+
app.get(exampleRoute, {
|
|
94
|
+
schema: {
|
|
95
|
+
// Add a clickable link to the example route in route's Swagger
|
|
96
|
+
// description so it's easier to navigate to
|
|
97
|
+
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
98
|
+
},
|
|
99
|
+
}, async (_, reply) => {
|
|
65
100
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
66
101
|
fixStacktrace: true,
|
|
67
102
|
});
|
|
@@ -72,7 +107,7 @@ async function createServer() {
|
|
|
72
107
|
}
|
|
73
108
|
}
|
|
74
109
|
// TODO: only do this locally, load from CDN in production
|
|
75
|
-
app.register(static_1.default, {
|
|
110
|
+
await app.register(static_1.default, {
|
|
76
111
|
root: path_1.default.join(root, 'dist'),
|
|
77
112
|
// TODO: maybe use @fastify/cors instead
|
|
78
113
|
setHeaders(res) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
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"));
|
|
11
|
+
// Setup automatic OpenAPI specification compilation and enable
|
|
12
|
+
// Swagger UI at the `/api` route
|
|
13
|
+
async function setupSwagger(app) {
|
|
14
|
+
let appInfo = {};
|
|
15
|
+
try {
|
|
16
|
+
const packageJson = fs_1.default.readFileSync(path_1.default.join(process.cwd(), 'package.json'), { encoding: 'utf-8' });
|
|
17
|
+
appInfo = JSON.parse(packageJson);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
// We only use package.json info to setup Swagger info and links,
|
|
21
|
+
// if we are unable to load them -- that's fine
|
|
22
|
+
}
|
|
23
|
+
const homepage = appInfo.homepage ||
|
|
24
|
+
(typeof appInfo.repository === 'string'
|
|
25
|
+
? appInfo.repository
|
|
26
|
+
: appInfo.repository?.url);
|
|
27
|
+
await app.register(swagger_1.default, {
|
|
28
|
+
openapi: {
|
|
29
|
+
info: {
|
|
30
|
+
title: appInfo.name ?? 'Nerest micro frontend',
|
|
31
|
+
description: appInfo.description,
|
|
32
|
+
version: appInfo.version ?? '',
|
|
33
|
+
contact: homepage
|
|
34
|
+
? {
|
|
35
|
+
name: 'Homepage',
|
|
36
|
+
url: homepage,
|
|
37
|
+
}
|
|
38
|
+
: undefined,
|
|
39
|
+
},
|
|
40
|
+
externalDocs: {
|
|
41
|
+
url: 'https://github.com/nerestjs/nerest',
|
|
42
|
+
description: 'Built with Nerest',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
await app.register(swagger_ui_1.default, {
|
|
47
|
+
routePrefix: '/api',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
exports.setupSwagger = setupSwagger;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.validator = void 0;
|
|
7
|
+
const ajv_1 = __importDefault(require("ajv"));
|
|
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({
|
|
11
|
+
coerceTypes: 'array',
|
|
12
|
+
useDefaults: true,
|
|
13
|
+
removeAdditional: true,
|
|
14
|
+
uriResolver: fast_uri_1.default,
|
|
15
|
+
addUsedSchema: false,
|
|
16
|
+
// Explicitly set allErrors to `false`.
|
|
17
|
+
// When set to `true`, a DoS attack is possible.
|
|
18
|
+
allErrors: false,
|
|
19
|
+
});
|
|
20
|
+
// Support additional type formats in JSON schema like `date`,
|
|
21
|
+
// `email`, `url`, etc. Used by default in fastify
|
|
22
|
+
// https://www.npmjs.com/package/ajv-formats
|
|
23
|
+
(0, ajv_formats_1.default)(exports.validator);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerest/nerest",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "React micro frontend framework",
|
|
5
5
|
"homepage": "https://github.com/nerestjs/nerest#readme",
|
|
6
6
|
"repository": {
|
|
@@ -46,24 +46,29 @@
|
|
|
46
46
|
]
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@fastify/static": "^6.
|
|
50
|
-
"fastify": "^
|
|
49
|
+
"@fastify/static": "^6.9.0",
|
|
50
|
+
"@fastify/swagger": "^8.3.1",
|
|
51
|
+
"@fastify/swagger-ui": "^1.4.0",
|
|
52
|
+
"ajv": "^8.12.0",
|
|
53
|
+
"ajv-formats": "^2.1.1",
|
|
54
|
+
"fast-uri": "^2.2.0",
|
|
55
|
+
"fastify": "^4.13.0",
|
|
51
56
|
"nanoid": "^3.3.4",
|
|
52
|
-
"vite": "^
|
|
57
|
+
"vite": "^4.1.4"
|
|
53
58
|
},
|
|
54
59
|
"devDependencies": {
|
|
55
|
-
"@tinkoff/eslint-config": "^1.
|
|
56
|
-
"@tinkoff/eslint-config-react": "^1.
|
|
57
|
-
"@tinkoff/prettier-config": "^1.
|
|
58
|
-
"@types/react": "^18.0.
|
|
59
|
-
"@types/react-dom": "^18.0.
|
|
60
|
-
"jest": "^29.3
|
|
61
|
-
"lint-staged": "^13.
|
|
60
|
+
"@tinkoff/eslint-config": "^1.50.1",
|
|
61
|
+
"@tinkoff/eslint-config-react": "^1.50.2",
|
|
62
|
+
"@tinkoff/prettier-config": "^1.47.1",
|
|
63
|
+
"@types/react": "^18.0.28",
|
|
64
|
+
"@types/react-dom": "^18.0.11",
|
|
65
|
+
"jest": "^29.4.3",
|
|
66
|
+
"lint-staged": "^13.1.2",
|
|
62
67
|
"react": "^18.2.0",
|
|
63
68
|
"react-dom": "^18.2.0",
|
|
64
69
|
"simple-git-hooks": "^2.8.1",
|
|
65
|
-
"sort-package-json": "^2.1
|
|
66
|
-
"typescript": "^4.
|
|
70
|
+
"sort-package-json": "^2.4.1",
|
|
71
|
+
"typescript": "^4.9.5"
|
|
67
72
|
},
|
|
68
73
|
"peerDependencies": {
|
|
69
74
|
"react": "^18.0.0",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
// Loads and parses the schema file for a specific app
|
|
6
|
+
export async function loadAppSchema(appRoot: string) {
|
|
7
|
+
const schemaPath = path.join(appRoot, 'schema.json');
|
|
8
|
+
|
|
9
|
+
let schema = null;
|
|
10
|
+
|
|
11
|
+
// TODO: error handling and reporting
|
|
12
|
+
if (existsSync(schemaPath)) {
|
|
13
|
+
const file = await fs.readFile(schemaPath, { encoding: 'utf-8' });
|
|
14
|
+
schema = JSON.parse(file);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return schema;
|
|
18
|
+
}
|
package/server/apps.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Manifest } from 'vite';
|
|
|
4
4
|
|
|
5
5
|
import { loadAppAssets } from './app-parts/assets';
|
|
6
6
|
import { loadAppExamples } from './app-parts/examples';
|
|
7
|
+
import { loadAppSchema } from './app-parts/schema';
|
|
7
8
|
|
|
8
9
|
export type AppEntry = {
|
|
9
10
|
name: string;
|
|
@@ -11,6 +12,7 @@ export type AppEntry = {
|
|
|
11
12
|
entry: string;
|
|
12
13
|
assets: string[];
|
|
13
14
|
examples: Record<string, unknown>;
|
|
15
|
+
schema: Record<string, unknown> | null;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
// Build the record of the available apps by convention
|
|
@@ -47,6 +49,7 @@ async function loadApp(
|
|
|
47
49
|
entry: path.join(appRoot, 'index.tsx'),
|
|
48
50
|
assets: loadAppAssets(manifest, name),
|
|
49
51
|
examples: await loadAppExamples(appRoot),
|
|
52
|
+
schema: await loadAppSchema(appRoot),
|
|
50
53
|
},
|
|
51
54
|
];
|
|
52
55
|
}
|
package/server/index.ts
CHANGED
|
@@ -6,12 +6,15 @@ import vite from 'vite';
|
|
|
6
6
|
import type { InlineConfig } from 'vite';
|
|
7
7
|
import type { RollupWatcher, RollupWatcherEvent } from 'rollup';
|
|
8
8
|
|
|
9
|
+
import type { RouteShorthandOptions } from 'fastify';
|
|
9
10
|
import fastify from 'fastify';
|
|
10
11
|
import fastifyStatic from '@fastify/static';
|
|
11
12
|
|
|
12
13
|
import { loadApps } from './apps';
|
|
13
14
|
import { renderApp } from './render';
|
|
14
15
|
import { renderPreviewPage } from './preview';
|
|
16
|
+
import { validator } from './validator';
|
|
17
|
+
import { setupSwagger } from './swagger';
|
|
15
18
|
|
|
16
19
|
// TODO: this turned out to be a dev server, production server
|
|
17
20
|
// will most likely be implemented separately
|
|
@@ -53,11 +56,36 @@ export async function createServer() {
|
|
|
53
56
|
const viteSsr = await vite.createServer(config);
|
|
54
57
|
const app = fastify();
|
|
55
58
|
|
|
59
|
+
// Setup schema validation. We have to use our own ajv instance that
|
|
60
|
+
// we can use both to validate request bodies and examples against
|
|
61
|
+
// app schemas
|
|
62
|
+
app.setValidatorCompiler(({ schema }) => validator.compile(schema));
|
|
63
|
+
|
|
64
|
+
await setupSwagger(app);
|
|
65
|
+
|
|
56
66
|
for (const appEntry of Object.values(apps)) {
|
|
57
|
-
const { name, entry, examples } = appEntry;
|
|
67
|
+
const { name, entry, examples, schema } = appEntry;
|
|
68
|
+
|
|
69
|
+
const routeOptions: RouteShorthandOptions = {};
|
|
70
|
+
|
|
71
|
+
// TODO: report error if schema is missing, unless this app is client-only.
|
|
72
|
+
// TODO: disallow apps without schemas in production build
|
|
73
|
+
if (schema) {
|
|
74
|
+
routeOptions.schema = {
|
|
75
|
+
// Use description as Swagger summary, since summary is visible
|
|
76
|
+
// even when the route is collapsed in the UI
|
|
77
|
+
summary: schema.description as string,
|
|
78
|
+
body: {
|
|
79
|
+
...schema,
|
|
80
|
+
// Mix examples into the schema so they become accessible
|
|
81
|
+
// in the Swagger UI
|
|
82
|
+
examples: Object.values(examples),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
58
86
|
|
|
59
87
|
// POST /api/{name} -> render app with request.body as props
|
|
60
|
-
app.post(`/api/${name}`, async (request) => {
|
|
88
|
+
app.post(`/api/${name}`, routeOptions, async (request) => {
|
|
61
89
|
// ssrLoadModule drives the "hot-reload" logic, and allows
|
|
62
90
|
// picking up changes to the source without restarting the server
|
|
63
91
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
@@ -71,27 +99,46 @@ export async function createServer() {
|
|
|
71
99
|
});
|
|
72
100
|
|
|
73
101
|
for (const [exampleName, example] of Object.entries(examples)) {
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
});
|
|
80
|
-
const { html, assets } = renderApp(
|
|
81
|
-
appEntry,
|
|
82
|
-
ssrComponent.default,
|
|
83
|
-
example as Record<string, unknown>
|
|
102
|
+
// Validate example against schema when specified
|
|
103
|
+
if (schema && !validator.validate(schema, example)) {
|
|
104
|
+
// TODO: use logger and display errors more prominently
|
|
105
|
+
console.error(
|
|
106
|
+
`Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator.errorsText()}`
|
|
84
107
|
);
|
|
108
|
+
}
|
|
85
109
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
// GET /api/{name}/examples/{example} -> render a preview page
|
|
111
|
+
// with a predefined example body
|
|
112
|
+
const exampleRoute = `/api/${name}/examples/${exampleName}`;
|
|
113
|
+
app.get(
|
|
114
|
+
exampleRoute,
|
|
115
|
+
{
|
|
116
|
+
schema: {
|
|
117
|
+
// Add a clickable link to the example route in route's Swagger
|
|
118
|
+
// description so it's easier to navigate to
|
|
119
|
+
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
async (_, reply) => {
|
|
123
|
+
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
124
|
+
fixStacktrace: true,
|
|
125
|
+
});
|
|
126
|
+
const { html, assets } = renderApp(
|
|
127
|
+
appEntry,
|
|
128
|
+
ssrComponent.default,
|
|
129
|
+
example as Record<string, unknown>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
reply.type('text/html');
|
|
133
|
+
|
|
134
|
+
return renderPreviewPage(html, assets);
|
|
135
|
+
}
|
|
136
|
+
);
|
|
90
137
|
}
|
|
91
138
|
}
|
|
92
139
|
|
|
93
140
|
// TODO: only do this locally, load from CDN in production
|
|
94
|
-
app.register(fastifyStatic, {
|
|
141
|
+
await app.register(fastifyStatic, {
|
|
95
142
|
root: path.join(root, 'dist'),
|
|
96
143
|
// TODO: maybe use @fastify/cors instead
|
|
97
144
|
setHeaders(res: ServerResponse) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import type { FastifyInstance } from 'fastify';
|
|
4
|
+
import fastifySwagger from '@fastify/swagger';
|
|
5
|
+
import fastifySwaggerUi from '@fastify/swagger-ui';
|
|
6
|
+
|
|
7
|
+
// Setup automatic OpenAPI specification compilation and enable
|
|
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
|
+
|
|
29
|
+
const homepage =
|
|
30
|
+
appInfo.homepage ||
|
|
31
|
+
(typeof appInfo.repository === 'string'
|
|
32
|
+
? appInfo.repository
|
|
33
|
+
: appInfo.repository?.url);
|
|
34
|
+
|
|
35
|
+
await app.register(fastifySwagger, {
|
|
36
|
+
openapi: {
|
|
37
|
+
info: {
|
|
38
|
+
title: appInfo.name ?? 'Nerest micro frontend',
|
|
39
|
+
description: appInfo.description,
|
|
40
|
+
version: appInfo.version ?? '',
|
|
41
|
+
contact: homepage
|
|
42
|
+
? {
|
|
43
|
+
name: 'Homepage',
|
|
44
|
+
url: homepage,
|
|
45
|
+
}
|
|
46
|
+
: undefined,
|
|
47
|
+
},
|
|
48
|
+
externalDocs: {
|
|
49
|
+
url: 'https://github.com/nerestjs/nerest',
|
|
50
|
+
description: 'Built with Nerest',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await app.register(fastifySwaggerUi, {
|
|
56
|
+
routePrefix: '/api',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import fastUri from 'fast-uri';
|
|
3
|
+
import addFormats from 'ajv-formats';
|
|
4
|
+
|
|
5
|
+
export const validator = new Ajv({
|
|
6
|
+
coerceTypes: 'array',
|
|
7
|
+
useDefaults: true,
|
|
8
|
+
removeAdditional: true,
|
|
9
|
+
uriResolver: fastUri,
|
|
10
|
+
addUsedSchema: false,
|
|
11
|
+
// Explicitly set allErrors to `false`.
|
|
12
|
+
// When set to `true`, a DoS attack is possible.
|
|
13
|
+
allErrors: false,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Support additional type formats in JSON schema like `date`,
|
|
17
|
+
// `email`, `url`, etc. Used by default in fastify
|
|
18
|
+
// https://www.npmjs.com/package/ajv-formats
|
|
19
|
+
addFormats(validator);
|