@nerest/nerest 0.0.1 → 0.0.2
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 +14 -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 +20 -2
- package/dist/server/validator.d.ts +2 -0
- package/dist/server/validator.js +23 -0
- package/package.json +16 -13
- package/server/app-parts/schema.ts +18 -0
- package/server/apps.ts +3 -0
- package/server/index.ts +27 -2
- package/server/validator.ts +19 -0
package/README.md
CHANGED
|
@@ -10,14 +10,26 @@ 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
|
+
|
|
21
33
|
## Development
|
|
22
34
|
|
|
23
35
|
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,7 @@ 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");
|
|
15
16
|
// TODO: this turned out to be a dev server, production server
|
|
16
17
|
// will most likely be implemented separately
|
|
17
18
|
async function createServer() {
|
|
@@ -47,10 +48,22 @@ async function createServer() {
|
|
|
47
48
|
// Start vite server that will be rendering SSR components
|
|
48
49
|
const viteSsr = await vite_1.default.createServer(config);
|
|
49
50
|
const app = (0, fastify_1.default)();
|
|
51
|
+
// Setup schema validation. We have to use our own ajv instance that
|
|
52
|
+
// we can use both to validate request bodies and examples against
|
|
53
|
+
// app schemas
|
|
54
|
+
app.setValidatorCompiler(({ schema }) => validator_1.validator.compile(schema));
|
|
50
55
|
for (const appEntry of Object.values(apps)) {
|
|
51
|
-
const { name, entry, examples } = appEntry;
|
|
56
|
+
const { name, entry, examples, schema } = appEntry;
|
|
57
|
+
const routeOptions = {};
|
|
58
|
+
// TODO: report error if schema is missing, unless this app is client-only.
|
|
59
|
+
// TODO: disallow apps without schemas in production build
|
|
60
|
+
if (schema) {
|
|
61
|
+
routeOptions.schema = {
|
|
62
|
+
body: schema,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
52
65
|
// POST /api/{name} -> render app with request.body as props
|
|
53
|
-
app.post(`/api/${name}`, async (request) => {
|
|
66
|
+
app.post(`/api/${name}`, routeOptions, async (request) => {
|
|
54
67
|
// ssrLoadModule drives the "hot-reload" logic, and allows
|
|
55
68
|
// picking up changes to the source without restarting the server
|
|
56
69
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
@@ -59,6 +72,11 @@ async function createServer() {
|
|
|
59
72
|
return (0, render_1.renderApp)(appEntry, ssrComponent.default, request.body);
|
|
60
73
|
});
|
|
61
74
|
for (const [exampleName, example] of Object.entries(examples)) {
|
|
75
|
+
// Validate example against schema when specified
|
|
76
|
+
if (schema && !validator_1.validator.validate(schema, example)) {
|
|
77
|
+
// TODO: use logger and display errors more prominently
|
|
78
|
+
console.error(`Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator_1.validator.errorsText()}`);
|
|
79
|
+
}
|
|
62
80
|
// GET /api/{name}/examples/{example} -> render a preview page
|
|
63
81
|
// with a predefined example body
|
|
64
82
|
app.get(`/api/${name}/examples/${exampleName}`, async (_, reply) => {
|
|
@@ -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.2",
|
|
4
4
|
"description": "React micro frontend framework",
|
|
5
5
|
"homepage": "https://github.com/nerestjs/nerest#readme",
|
|
6
6
|
"repository": {
|
|
@@ -46,24 +46,27 @@
|
|
|
46
46
|
]
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@fastify/static": "^6.
|
|
50
|
-
"
|
|
49
|
+
"@fastify/static": "^6.9.0",
|
|
50
|
+
"ajv": "^8.12.0",
|
|
51
|
+
"ajv-formats": "^2.1.1",
|
|
52
|
+
"fast-uri": "^2.2.0",
|
|
53
|
+
"fastify": "^4.13.0",
|
|
51
54
|
"nanoid": "^3.3.4",
|
|
52
|
-
"vite": "^
|
|
55
|
+
"vite": "^4.1.4"
|
|
53
56
|
},
|
|
54
57
|
"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.
|
|
58
|
+
"@tinkoff/eslint-config": "^1.50.1",
|
|
59
|
+
"@tinkoff/eslint-config-react": "^1.50.2",
|
|
60
|
+
"@tinkoff/prettier-config": "^1.47.1",
|
|
61
|
+
"@types/react": "^18.0.28",
|
|
62
|
+
"@types/react-dom": "^18.0.11",
|
|
63
|
+
"jest": "^29.4.3",
|
|
64
|
+
"lint-staged": "^13.1.2",
|
|
62
65
|
"react": "^18.2.0",
|
|
63
66
|
"react-dom": "^18.2.0",
|
|
64
67
|
"simple-git-hooks": "^2.8.1",
|
|
65
|
-
"sort-package-json": "^2.1
|
|
66
|
-
"typescript": "^4.
|
|
68
|
+
"sort-package-json": "^2.4.1",
|
|
69
|
+
"typescript": "^4.9.5"
|
|
67
70
|
},
|
|
68
71
|
"peerDependencies": {
|
|
69
72
|
"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,14 @@ 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';
|
|
15
17
|
|
|
16
18
|
// TODO: this turned out to be a dev server, production server
|
|
17
19
|
// will most likely be implemented separately
|
|
@@ -53,11 +55,26 @@ export async function createServer() {
|
|
|
53
55
|
const viteSsr = await vite.createServer(config);
|
|
54
56
|
const app = fastify();
|
|
55
57
|
|
|
58
|
+
// Setup schema validation. We have to use our own ajv instance that
|
|
59
|
+
// we can use both to validate request bodies and examples against
|
|
60
|
+
// app schemas
|
|
61
|
+
app.setValidatorCompiler(({ schema }) => validator.compile(schema));
|
|
62
|
+
|
|
56
63
|
for (const appEntry of Object.values(apps)) {
|
|
57
|
-
const { name, entry, examples } = appEntry;
|
|
64
|
+
const { name, entry, examples, schema } = appEntry;
|
|
65
|
+
|
|
66
|
+
const routeOptions: RouteShorthandOptions = {};
|
|
67
|
+
|
|
68
|
+
// TODO: report error if schema is missing, unless this app is client-only.
|
|
69
|
+
// TODO: disallow apps without schemas in production build
|
|
70
|
+
if (schema) {
|
|
71
|
+
routeOptions.schema = {
|
|
72
|
+
body: schema,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
58
75
|
|
|
59
76
|
// POST /api/{name} -> render app with request.body as props
|
|
60
|
-
app.post(`/api/${name}`, async (request) => {
|
|
77
|
+
app.post(`/api/${name}`, routeOptions, async (request) => {
|
|
61
78
|
// ssrLoadModule drives the "hot-reload" logic, and allows
|
|
62
79
|
// picking up changes to the source without restarting the server
|
|
63
80
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
@@ -71,6 +88,14 @@ export async function createServer() {
|
|
|
71
88
|
});
|
|
72
89
|
|
|
73
90
|
for (const [exampleName, example] of Object.entries(examples)) {
|
|
91
|
+
// Validate example against schema when specified
|
|
92
|
+
if (schema && !validator.validate(schema, example)) {
|
|
93
|
+
// TODO: use logger and display errors more prominently
|
|
94
|
+
console.error(
|
|
95
|
+
`Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator.errorsText()}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
74
99
|
// GET /api/{name}/examples/{example} -> render a preview page
|
|
75
100
|
// with a predefined example body
|
|
76
101
|
app.get(`/api/${name}/examples/${exampleName}`, async (_, reply) => {
|
|
@@ -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);
|