@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 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;
@@ -1,9 +1,10 @@
1
- export declare type AppEntry = {
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;
@@ -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
  }
@@ -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,2 @@
1
+ import Ajv from 'ajv';
2
+ export declare const validator: Ajv;
@@ -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.1",
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.5.0",
50
- "fastify": "^4.9.2",
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": "^3.2.3"
55
+ "vite": "^4.1.4"
53
56
  },
54
57
  "devDependencies": {
55
- "@tinkoff/eslint-config": "^1.41.0",
56
- "@tinkoff/eslint-config-react": "^1.41.0",
57
- "@tinkoff/prettier-config": "^1.32.1",
58
- "@types/react": "^18.0.25",
59
- "@types/react-dom": "^18.0.8",
60
- "jest": "^29.3.1",
61
- "lint-staged": "^13.0.3",
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.0",
66
- "typescript": "^4.8.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);