@nerest/nerest 0.0.2 → 0.0.4
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 +29 -2
- package/bin/build.ts +8 -0
- package/bin/index.ts +7 -1
- package/bin/watch.ts +2 -11
- package/build/index.ts +82 -0
- package/dist/bin/build.d.ts +1 -0
- package/dist/bin/build.js +11 -0
- package/dist/bin/index.d.ts +1 -1
- package/dist/bin/index.js +7 -1
- package/dist/bin/watch.js +2 -8
- package/dist/build/index.d.ts +1 -0
- package/dist/build/index.js +72 -0
- package/dist/server/development.d.ts +1 -0
- package/dist/server/{index.js → development.js} +58 -23
- package/dist/server/loaders/assets.d.ts +2 -0
- package/dist/server/loaders/assets.js +14 -0
- package/dist/server/loaders/manifest.d.ts +1 -0
- package/dist/server/loaders/manifest.js +17 -0
- package/dist/server/{apps.d.ts → parts/apps.d.ts} +1 -1
- package/dist/server/{apps.js → parts/apps.js} +9 -16
- package/dist/server/parts/render.d.ts +11 -0
- package/dist/server/{render.js → parts/render.js} +2 -3
- package/dist/server/parts/swagger.d.ts +2 -0
- package/dist/server/parts/swagger.js +50 -0
- package/package.json +17 -13
- package/server/{index.ts → development.ts} +73 -34
- package/server/loaders/assets.ts +19 -0
- package/server/loaders/manifest.ts +11 -0
- package/server/{apps.ts → parts/apps.ts} +10 -17
- package/server/{render.ts → parts/render.ts} +7 -5
- package/server/parts/swagger.ts +58 -0
- package/server/production.ts +105 -0
- package/dist/client/entry.d.ts +0 -1
- package/dist/client/entry.js +0 -3
- package/dist/server/app-parts/assets.d.ts +0 -2
- package/dist/server/app-parts/assets.js +0 -15
- package/dist/server/app-parts/preview.js +0 -27
- package/dist/server/assets.d.ts +0 -1
- package/dist/server/assets.js +0 -28
- package/dist/server/entry.d.ts +0 -2
- package/dist/server/entry.js +0 -17
- package/dist/server/index.d.ts +0 -5
- package/dist/server/preview.d.ts +0 -1
- package/dist/server/render.d.ts +0 -6
- package/server/app-parts/assets.ts +0 -17
- /package/dist/server/{app-parts → loaders}/examples.d.ts +0 -0
- /package/dist/server/{app-parts → loaders}/examples.js +0 -0
- /package/dist/server/{app-parts → loaders}/schema.d.ts +0 -0
- /package/dist/server/{app-parts → loaders}/schema.js +0 -0
- /package/dist/server/{app-parts → parts}/preview.d.ts +0 -0
- /package/dist/server/{preview.js → parts/preview.js} +0 -0
- /package/dist/server/{validator.d.ts → parts/validator.d.ts} +0 -0
- /package/dist/server/{validator.js → parts/validator.js} +0 -0
- /package/server/{app-parts → loaders}/examples.ts +0 -0
- /package/server/{app-parts → loaders}/schema.ts +0 -0
- /package/server/{preview.ts → parts/preview.ts} +0 -0
- /package/server/{validator.ts → parts/validator.ts} +0 -0
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerest/nerest",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "React micro frontend framework",
|
|
5
5
|
"homepage": "https://github.com/nerestjs/nerest#readme",
|
|
6
6
|
"repository": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"/dist",
|
|
15
15
|
"/bin",
|
|
16
|
+
"/build",
|
|
16
17
|
"/client",
|
|
17
18
|
"/server",
|
|
18
19
|
"/README.md"
|
|
@@ -46,27 +47,30 @@
|
|
|
46
47
|
]
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
|
-
"@fastify/static": "^6.
|
|
50
|
+
"@fastify/static": "^6.10.2",
|
|
51
|
+
"@fastify/swagger": "^8.5.1",
|
|
52
|
+
"@fastify/swagger-ui": "^1.8.1",
|
|
50
53
|
"ajv": "^8.12.0",
|
|
51
54
|
"ajv-formats": "^2.1.1",
|
|
55
|
+
"dotenv": "^16.1.4",
|
|
52
56
|
"fast-uri": "^2.2.0",
|
|
53
|
-
"fastify": "^4.
|
|
54
|
-
"nanoid": "^3.3.
|
|
55
|
-
"vite": "^4.
|
|
57
|
+
"fastify": "^4.17.0",
|
|
58
|
+
"nanoid": "^3.3.6",
|
|
59
|
+
"vite": "^4.3.9"
|
|
56
60
|
},
|
|
57
61
|
"devDependencies": {
|
|
58
|
-
"@tinkoff/eslint-config": "^1.
|
|
59
|
-
"@tinkoff/eslint-config-react": "^1.
|
|
60
|
-
"@tinkoff/prettier-config": "^1.
|
|
61
|
-
"@types/react": "^18.
|
|
62
|
-
"@types/react-dom": "^18.
|
|
63
|
-
"jest": "^29.
|
|
64
|
-
"lint-staged": "^13.
|
|
62
|
+
"@tinkoff/eslint-config": "^1.52.1",
|
|
63
|
+
"@tinkoff/eslint-config-react": "^1.52.1",
|
|
64
|
+
"@tinkoff/prettier-config": "^1.52.1",
|
|
65
|
+
"@types/react": "^18.2.7",
|
|
66
|
+
"@types/react-dom": "^18.2.4",
|
|
67
|
+
"jest": "^29.5.0",
|
|
68
|
+
"lint-staged": "^13.2.2",
|
|
65
69
|
"react": "^18.2.0",
|
|
66
70
|
"react-dom": "^18.2.0",
|
|
67
71
|
"simple-git-hooks": "^2.8.1",
|
|
68
72
|
"sort-package-json": "^2.4.1",
|
|
69
|
-
"typescript": "^
|
|
73
|
+
"typescript": "^5.0.4"
|
|
70
74
|
},
|
|
71
75
|
"peerDependencies": {
|
|
72
76
|
"react": "^18.0.0",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// This is the nerest server entrypoint
|
|
1
|
+
// This is the nerest development server entrypoint
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import type { ServerResponse } from 'http';
|
|
4
4
|
|
|
@@ -10,14 +10,13 @@ import type { RouteShorthandOptions } from 'fastify';
|
|
|
10
10
|
import fastify from 'fastify';
|
|
11
11
|
import fastifyStatic from '@fastify/static';
|
|
12
12
|
|
|
13
|
-
import { loadApps } from './apps';
|
|
14
|
-
import { renderApp } from './render';
|
|
15
|
-
import { renderPreviewPage } from './preview';
|
|
16
|
-
import { validator } from './validator';
|
|
13
|
+
import { loadApps } from './parts/apps';
|
|
14
|
+
import { renderApp } from './parts/render';
|
|
15
|
+
import { renderPreviewPage } from './parts/preview';
|
|
16
|
+
import { validator } from './parts/validator';
|
|
17
|
+
import { setupSwagger } from './parts/swagger';
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
// will most likely be implemented separately
|
|
20
|
-
export async function createServer() {
|
|
19
|
+
export async function runDevelopmentServer() {
|
|
21
20
|
const root = process.cwd();
|
|
22
21
|
|
|
23
22
|
// TODO: move build config into a separate file
|
|
@@ -26,6 +25,7 @@ export async function createServer() {
|
|
|
26
25
|
const config: InlineConfig = {
|
|
27
26
|
root,
|
|
28
27
|
appType: 'custom',
|
|
28
|
+
envPrefix: 'NEREST_',
|
|
29
29
|
server: { middlewareMode: true },
|
|
30
30
|
build: {
|
|
31
31
|
// Manifest is needed to report used assets in SSR handles
|
|
@@ -36,12 +36,18 @@ export async function createServer() {
|
|
|
36
36
|
rollupOptions: {
|
|
37
37
|
input: '/node_modules/@nerest/nerest/client/index.ts',
|
|
38
38
|
output: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
dir: 'build',
|
|
40
|
+
entryFileNames: `client/assets/[name].js`,
|
|
41
|
+
chunkFileNames: `client/assets/[name].js`,
|
|
42
|
+
assetFileNames: `client/assets/[name].[ext]`,
|
|
42
43
|
},
|
|
43
44
|
},
|
|
44
45
|
},
|
|
46
|
+
// TODO: this doesn't seem to work without the index.html file entry and
|
|
47
|
+
// produces warnings in dev mode. look into this maybe
|
|
48
|
+
optimizeDeps: {
|
|
49
|
+
disabled: true,
|
|
50
|
+
},
|
|
45
51
|
};
|
|
46
52
|
|
|
47
53
|
// Build the clientside assets and watch for changes
|
|
@@ -49,7 +55,8 @@ export async function createServer() {
|
|
|
49
55
|
await startClientBuildWatcher(config);
|
|
50
56
|
|
|
51
57
|
// Load app entries following the `apps/{name}/index.tsx` convention
|
|
52
|
-
|
|
58
|
+
// TODO: remove hardcoded port
|
|
59
|
+
const apps = await loadApps(root, 'http://0.0.0.0:3000/');
|
|
53
60
|
|
|
54
61
|
// Start vite server that will be rendering SSR components
|
|
55
62
|
const viteSsr = await vite.createServer(config);
|
|
@@ -60,16 +67,25 @@ export async function createServer() {
|
|
|
60
67
|
// app schemas
|
|
61
68
|
app.setValidatorCompiler(({ schema }) => validator.compile(schema));
|
|
62
69
|
|
|
70
|
+
await setupSwagger(app);
|
|
71
|
+
|
|
63
72
|
for (const appEntry of Object.values(apps)) {
|
|
64
73
|
const { name, entry, examples, schema } = appEntry;
|
|
65
74
|
|
|
66
75
|
const routeOptions: RouteShorthandOptions = {};
|
|
67
76
|
|
|
68
|
-
// TODO: report error if schema is missing, unless this app is client-only
|
|
69
|
-
// TODO: disallow apps without schemas in production build
|
|
77
|
+
// TODO: report error if schema is missing, unless this app is client-only
|
|
70
78
|
if (schema) {
|
|
71
79
|
routeOptions.schema = {
|
|
72
|
-
|
|
80
|
+
// Use description as Swagger summary, since summary is visible
|
|
81
|
+
// even when the route is collapsed in the UI
|
|
82
|
+
summary: schema.description as string,
|
|
83
|
+
body: {
|
|
84
|
+
...schema,
|
|
85
|
+
// Mix examples into the schema so they become accessible
|
|
86
|
+
// in the Swagger UI
|
|
87
|
+
examples: Object.values(examples),
|
|
88
|
+
},
|
|
73
89
|
};
|
|
74
90
|
}
|
|
75
91
|
|
|
@@ -81,8 +97,11 @@ export async function createServer() {
|
|
|
81
97
|
fixStacktrace: true,
|
|
82
98
|
});
|
|
83
99
|
return renderApp(
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
{
|
|
101
|
+
name,
|
|
102
|
+
assets: appEntry.assets,
|
|
103
|
+
component: ssrComponent.default,
|
|
104
|
+
},
|
|
86
105
|
request.body as Record<string, unknown>
|
|
87
106
|
);
|
|
88
107
|
});
|
|
@@ -98,26 +117,40 @@ export async function createServer() {
|
|
|
98
117
|
|
|
99
118
|
// GET /api/{name}/examples/{example} -> render a preview page
|
|
100
119
|
// with a predefined example body
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
reply
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
const exampleRoute = `/api/${name}/examples/${exampleName}`;
|
|
121
|
+
app.get(
|
|
122
|
+
exampleRoute,
|
|
123
|
+
{
|
|
124
|
+
schema: {
|
|
125
|
+
// Add a clickable link to the example route in route's Swagger
|
|
126
|
+
// description so it's easier to navigate to
|
|
127
|
+
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
async (_, reply) => {
|
|
131
|
+
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
132
|
+
fixStacktrace: true,
|
|
133
|
+
});
|
|
134
|
+
const { html, assets } = renderApp(
|
|
135
|
+
{
|
|
136
|
+
name,
|
|
137
|
+
assets: appEntry.assets,
|
|
138
|
+
component: ssrComponent.default,
|
|
139
|
+
},
|
|
140
|
+
example as Record<string, unknown>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
reply.type('text/html');
|
|
144
|
+
|
|
145
|
+
return renderPreviewPage(html, assets);
|
|
146
|
+
}
|
|
147
|
+
);
|
|
115
148
|
}
|
|
116
149
|
}
|
|
117
150
|
|
|
118
151
|
// TODO: only do this locally, load from CDN in production
|
|
119
|
-
app.register(fastifyStatic, {
|
|
120
|
-
root: path.join(root, '
|
|
152
|
+
await app.register(fastifyStatic, {
|
|
153
|
+
root: path.join(root, 'build'),
|
|
121
154
|
// TODO: maybe use @fastify/cors instead
|
|
122
155
|
setHeaders(res: ServerResponse) {
|
|
123
156
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -129,7 +162,13 @@ export async function createServer() {
|
|
|
129
162
|
},
|
|
130
163
|
});
|
|
131
164
|
|
|
132
|
-
|
|
165
|
+
// TODO: remove hardcoded port
|
|
166
|
+
await app.listen({
|
|
167
|
+
host: '0.0.0.0',
|
|
168
|
+
port: 3000,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
133
172
|
}
|
|
134
173
|
|
|
135
174
|
// TODO: this should probably be moved from here
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Manifest } from 'vite';
|
|
2
|
+
|
|
3
|
+
// Extracts the list of assets for a given app from the manifest file
|
|
4
|
+
export function loadAppAssets(
|
|
5
|
+
appName: string,
|
|
6
|
+
manifest: Manifest,
|
|
7
|
+
staticPath: string
|
|
8
|
+
) {
|
|
9
|
+
// TODO: handling errors and potentially missing entries
|
|
10
|
+
// All apps share the same JS entry that dynamically imports the chunks of the apps
|
|
11
|
+
// that are used on the page based on their name
|
|
12
|
+
const clientEntryJs =
|
|
13
|
+
manifest['node_modules/@nerest/nerest/client/index.ts'].file;
|
|
14
|
+
|
|
15
|
+
// Each app has its own CSS bundles, if it imports any CSS
|
|
16
|
+
const appCss = manifest[`apps/${appName}/index.tsx`].css ?? [];
|
|
17
|
+
|
|
18
|
+
return [clientEntryJs, ...appCss].map((x) => staticPath + x);
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
// Manifest is used to provide assets list for every app
|
|
5
|
+
// for use with SSR
|
|
6
|
+
export async function loadAppManifest(root: string) {
|
|
7
|
+
// TODO: error handling
|
|
8
|
+
const manifestPath = path.join(root, 'build', 'manifest.json');
|
|
9
|
+
const manifestData = await fs.readFile(manifestPath, { encoding: 'utf8' });
|
|
10
|
+
return JSON.parse(manifestData);
|
|
11
|
+
}
|
|
@@ -2,9 +2,10 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import type { Manifest } from 'vite';
|
|
4
4
|
|
|
5
|
-
import { loadAppAssets } from '
|
|
6
|
-
import { loadAppExamples } from '
|
|
7
|
-
import { loadAppSchema } from '
|
|
5
|
+
import { loadAppAssets } from '../loaders/assets';
|
|
6
|
+
import { loadAppExamples } from '../loaders/examples';
|
|
7
|
+
import { loadAppSchema } from '../loaders/schema';
|
|
8
|
+
import { loadAppManifest } from '../loaders/manifest';
|
|
8
9
|
|
|
9
10
|
export type AppEntry = {
|
|
10
11
|
name: string;
|
|
@@ -18,9 +19,9 @@ export type AppEntry = {
|
|
|
18
19
|
// Build the record of the available apps by convention
|
|
19
20
|
// apps -> /apps/{name}/index.tsx
|
|
20
21
|
// examples -> /apps/{name}/examples/{example}.json
|
|
21
|
-
export async function loadApps(root: string) {
|
|
22
|
+
export async function loadApps(root: string, deployedStaticPath: string) {
|
|
22
23
|
const appsRoot = path.join(root, 'apps');
|
|
23
|
-
const manifest = await
|
|
24
|
+
const manifest = await loadAppManifest(root);
|
|
24
25
|
|
|
25
26
|
const appsDirs = (await fs.readdir(appsRoot, { withFileTypes: true }))
|
|
26
27
|
.filter((d) => d.isDirectory())
|
|
@@ -28,7 +29,7 @@ export async function loadApps(root: string) {
|
|
|
28
29
|
|
|
29
30
|
const apps: Array<[name: string, entry: AppEntry]> = [];
|
|
30
31
|
for (const appDir of appsDirs) {
|
|
31
|
-
apps.push(await loadApp(appsRoot, appDir, manifest));
|
|
32
|
+
apps.push(await loadApp(appsRoot, appDir, manifest, deployedStaticPath));
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
return Object.fromEntries(apps);
|
|
@@ -37,7 +38,8 @@ export async function loadApps(root: string) {
|
|
|
37
38
|
async function loadApp(
|
|
38
39
|
appsRoot: string,
|
|
39
40
|
name: string,
|
|
40
|
-
manifest: Manifest
|
|
41
|
+
manifest: Manifest,
|
|
42
|
+
deployedStaticPath: string
|
|
41
43
|
): Promise<[name: string, entry: AppEntry]> {
|
|
42
44
|
// TODO: report problems with loading entries, assets and/or examples
|
|
43
45
|
const appRoot = path.join(appsRoot, name);
|
|
@@ -47,18 +49,9 @@ async function loadApp(
|
|
|
47
49
|
name,
|
|
48
50
|
root: appRoot,
|
|
49
51
|
entry: path.join(appRoot, 'index.tsx'),
|
|
50
|
-
assets: loadAppAssets(manifest,
|
|
52
|
+
assets: loadAppAssets(name, manifest, deployedStaticPath),
|
|
51
53
|
examples: await loadAppExamples(appRoot),
|
|
52
54
|
schema: await loadAppSchema(appRoot),
|
|
53
55
|
},
|
|
54
56
|
];
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
// Manifest is used to provide assets list for every app
|
|
58
|
-
// for use with SSR
|
|
59
|
-
async function loadManifest(root: string) {
|
|
60
|
-
// TODO: error handling
|
|
61
|
-
const manifestPath = path.join(root, 'dist', 'manifest.json');
|
|
62
|
-
const manifestData = await fs.readFile(manifestPath, { encoding: 'utf8' });
|
|
63
|
-
return JSON.parse(manifestData);
|
|
64
|
-
}
|
|
@@ -2,15 +2,17 @@ import React from 'react';
|
|
|
2
2
|
import { renderToString } from 'react-dom/server';
|
|
3
3
|
import { nanoid } from 'nanoid';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type RenderProps = {
|
|
6
|
+
name: string;
|
|
7
|
+
assets: string[];
|
|
8
|
+
component: React.ComponentType;
|
|
9
|
+
};
|
|
6
10
|
|
|
7
11
|
export function renderApp(
|
|
8
|
-
|
|
9
|
-
appComponent: React.ComponentType,
|
|
12
|
+
{ name, assets, component }: RenderProps,
|
|
10
13
|
props: Record<string, unknown> = {}
|
|
11
14
|
) {
|
|
12
|
-
const
|
|
13
|
-
const html = renderSsrComponent(name, appComponent, props);
|
|
15
|
+
const html = renderSsrComponent(name, component, props);
|
|
14
16
|
return { html, assets };
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -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,105 @@
|
|
|
1
|
+
// This is the nerest production server entrypoint
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
import fastify from 'fastify';
|
|
6
|
+
import type { RouteShorthandOptions } from 'fastify';
|
|
7
|
+
|
|
8
|
+
import type { AppEntry } from './parts/apps';
|
|
9
|
+
import { renderApp } from './parts/render';
|
|
10
|
+
import { setupSwagger } from './parts/swagger';
|
|
11
|
+
import { validator } from './parts/validator';
|
|
12
|
+
import { renderPreviewPage } from './parts/preview';
|
|
13
|
+
|
|
14
|
+
// TODO: refactor to merge the similar parts between production and development server?
|
|
15
|
+
async function runProductionServer() {
|
|
16
|
+
const root = process.cwd();
|
|
17
|
+
|
|
18
|
+
// TODO: error handling for file reading
|
|
19
|
+
const apps = JSON.parse(
|
|
20
|
+
await fs.readFile(path.join(root, 'build/nerest-manifest.json'), {
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
})
|
|
23
|
+
) as Record<string, AppEntry>;
|
|
24
|
+
|
|
25
|
+
const components = import.meta.glob('/apps/*/index.tsx', {
|
|
26
|
+
import: 'default',
|
|
27
|
+
eager: true,
|
|
28
|
+
}) as Record<string, React.ComponentType>;
|
|
29
|
+
|
|
30
|
+
const app = fastify();
|
|
31
|
+
|
|
32
|
+
// Setup schema validation. We have to use our own ajv instance that
|
|
33
|
+
// we can use both to validate request bodies and examples against
|
|
34
|
+
// app schemas
|
|
35
|
+
app.setValidatorCompiler(({ schema }) => validator.compile(schema));
|
|
36
|
+
|
|
37
|
+
await setupSwagger(app);
|
|
38
|
+
|
|
39
|
+
for (const appEntry of Object.values(apps)) {
|
|
40
|
+
const { name, examples, schema, assets } = appEntry;
|
|
41
|
+
const component = components[`/apps/${name}/index.tsx`];
|
|
42
|
+
|
|
43
|
+
const routeOptions: RouteShorthandOptions = {};
|
|
44
|
+
|
|
45
|
+
// TODO: report error if schema is missing, unless this app is client-only
|
|
46
|
+
// TODO: disallow apps without schemas in production build
|
|
47
|
+
if (schema) {
|
|
48
|
+
routeOptions.schema = {
|
|
49
|
+
// Use description as Swagger summary, since summary is visible
|
|
50
|
+
// even when the route is collapsed in the UI
|
|
51
|
+
summary: schema.description as string,
|
|
52
|
+
// TODO: do we need to mix in examples like in the development server?
|
|
53
|
+
body: schema,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// POST /api/{name} -> render app with request.body as props
|
|
58
|
+
app.post(`/api/${name}`, routeOptions, (request) =>
|
|
59
|
+
renderApp(
|
|
60
|
+
{ name, assets, component },
|
|
61
|
+
request.body as Record<string, unknown>
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
for (const [exampleName, example] of Object.entries(examples)) {
|
|
66
|
+
// GET /api/{name}/examples/{example} -> render a preview page
|
|
67
|
+
// with a predefined example body
|
|
68
|
+
const exampleRoute = `/api/${name}/examples/${exampleName}`;
|
|
69
|
+
app.get(
|
|
70
|
+
exampleRoute,
|
|
71
|
+
{
|
|
72
|
+
schema: {
|
|
73
|
+
// Add a clickable link to the example route in route's Swagger
|
|
74
|
+
// description so it's easier to navigate to
|
|
75
|
+
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
async (_, reply) => {
|
|
79
|
+
const { html, assets: outAssets } = renderApp(
|
|
80
|
+
{
|
|
81
|
+
name,
|
|
82
|
+
assets,
|
|
83
|
+
component,
|
|
84
|
+
},
|
|
85
|
+
example as Record<string, unknown>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
reply.type('text/html');
|
|
89
|
+
|
|
90
|
+
return renderPreviewPage(html, outAssets);
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// TODO: remove hardcoded port
|
|
97
|
+
await app.listen({
|
|
98
|
+
host: '0.0.0.0',
|
|
99
|
+
port: 3000,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
runProductionServer();
|
package/dist/client/entry.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/client/entry.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.loadAppAssets = void 0;
|
|
4
|
-
// TODO: this is not as simple in the real world
|
|
5
|
-
const publicPath = process.env.PUBLIC_PATH ?? 'http://0.0.0.0:3000/';
|
|
6
|
-
// Extracts the list of assets for a given app from the manifest file
|
|
7
|
-
function loadAppAssets(manifest, appName) {
|
|
8
|
-
const entries = Object.entries(manifest);
|
|
9
|
-
// TODO: handling errors and potentially missing entries
|
|
10
|
-
const clientEntryJs = entries.find(([_, entry]) => entry.isEntry)?.[1].file;
|
|
11
|
-
const appCss = entries.find(([name, _]) => name.includes(`/${appName}/index.tsx`))?.[1]
|
|
12
|
-
.css ?? [];
|
|
13
|
-
return [clientEntryJs, ...appCss].map((x) => publicPath + x);
|
|
14
|
-
}
|
|
15
|
-
exports.loadAppAssets = loadAppAssets;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.renderPreviewPage = void 0;
|
|
4
|
-
function renderPreviewPage(html, assets) {
|
|
5
|
-
const { scripts, styles } = mapAssets(assets);
|
|
6
|
-
return `
|
|
7
|
-
<html>
|
|
8
|
-
<head>
|
|
9
|
-
${styles.join('\n')}
|
|
10
|
-
</head>
|
|
11
|
-
<body>
|
|
12
|
-
${html}
|
|
13
|
-
${scripts.join('\n')}
|
|
14
|
-
</body>
|
|
15
|
-
</html>
|
|
16
|
-
`;
|
|
17
|
-
}
|
|
18
|
-
exports.renderPreviewPage = renderPreviewPage;
|
|
19
|
-
function mapAssets(assets) {
|
|
20
|
-
const scripts = assets
|
|
21
|
-
.filter((src) => src.endsWith('.js'))
|
|
22
|
-
.map((src) => `<script type="module" src="${src}"></script>`);
|
|
23
|
-
const styles = assets
|
|
24
|
-
.filter((src) => src.endsWith('.css'))
|
|
25
|
-
.map((src) => `<link rel="stylesheet" href="${src}">`);
|
|
26
|
-
return { scripts, styles };
|
|
27
|
-
}
|
package/dist/server/assets.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getAppAssets(appName: string, root: string): Promise<string[]>;
|
package/dist/server/assets.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
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.getAppAssets = void 0;
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
-
// TODO: way more complicated in the real world
|
|
10
|
-
const publicPath = process.env.PUBLIC_PATH ?? 'http://127.0.0.1:3000/';
|
|
11
|
-
async function getAppAssets(appName, root) {
|
|
12
|
-
const manifest = await loadManifest(root);
|
|
13
|
-
const assets = extractAssetsFromManifest(manifest, appName);
|
|
14
|
-
return assets.map((x) => publicPath + x);
|
|
15
|
-
}
|
|
16
|
-
exports.getAppAssets = getAppAssets;
|
|
17
|
-
async function loadManifest(root) {
|
|
18
|
-
const manifestPath = path_1.default.join(root, 'dist', 'manifest.json');
|
|
19
|
-
const manifestData = await promises_1.default.readFile(manifestPath, { encoding: 'utf8' });
|
|
20
|
-
return JSON.parse(manifestData);
|
|
21
|
-
}
|
|
22
|
-
function extractAssetsFromManifest(manifest, appName) {
|
|
23
|
-
const entries = Object.entries(manifest);
|
|
24
|
-
const clientEntryJs = entries.find(([_, entry]) => entry.isEntry)?.[1].file;
|
|
25
|
-
const appCss = entries.find(([name, _]) => name.includes(`/${appName}/index.tsx`))?.[1]
|
|
26
|
-
.css ?? [];
|
|
27
|
-
return [clientEntryJs, ...appCss];
|
|
28
|
-
}
|
package/dist/server/entry.d.ts
DELETED