@nerest/nerest 0.0.3 → 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 +27 -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} +39 -21
- 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/package.json +17 -15
- package/server/{index.ts → development.ts} +38 -21
- 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/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/{swagger.d.ts → parts/swagger.d.ts} +0 -0
- /package/dist/server/{swagger.js → parts/swagger.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/{swagger.ts → parts/swagger.ts} +0 -0
- /package/server/{validator.ts → parts/validator.ts} +0 -0
package/README.md
CHANGED
|
@@ -12,9 +12,12 @@ npm i --save @nerest/nerest react react-dom
|
|
|
12
12
|
|
|
13
13
|
## Points of Interest
|
|
14
14
|
|
|
15
|
-
- SSR server entry:
|
|
15
|
+
- SSR server entry:
|
|
16
|
+
- Development: [server/development.ts](server/development.ts)
|
|
17
|
+
- Production: [server/production.ts](server/production.ts)
|
|
16
18
|
- Hydrating client entry: [client/index.ts](client/index.ts)
|
|
17
19
|
- CLI entry: [bin/index.ts](bin/index.ts)
|
|
20
|
+
- Production build script: [build/index.ts](build/index.ts)
|
|
18
21
|
|
|
19
22
|
## Conventions
|
|
20
23
|
|
|
@@ -32,6 +35,28 @@ The app directory should contain a `schema.json` file that describes the schema
|
|
|
32
35
|
|
|
33
36
|
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
37
|
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
Different aspects of Nerest apps can be configured via environment variables, JSON configuration and runtime hooks written in TypeScript. Examples of all kinds of configuration can be viewed in the [nerest-harness](https://github.com/nerestjs/harness) repository.
|
|
41
|
+
|
|
42
|
+
### Environment Variables
|
|
43
|
+
|
|
44
|
+
#### Buildtime
|
|
45
|
+
|
|
46
|
+
- `NEREST_STATIC_PATH` is required for production build and must contain the URL of where the client static assets will be deployed. It lets the server-side renderer return their paths in the assets field of the response.
|
|
47
|
+
|
|
48
|
+
#### Runtime
|
|
49
|
+
|
|
50
|
+
All environment variables prefixed with `NEREST_` will be bundled with your app during buildtime. You can access them in the code using the special `import.meta.env` object. For example, `import.meta.env.NEREST_SOMEVAR` will be statically replaced during buildtime with the value of this environment variable on the build machine.
|
|
51
|
+
|
|
52
|
+
### JSON Configuration
|
|
53
|
+
|
|
54
|
+
TODO
|
|
55
|
+
|
|
56
|
+
### Runtime Hooks
|
|
57
|
+
|
|
58
|
+
TODO
|
|
59
|
+
|
|
35
60
|
## Development
|
|
36
61
|
|
|
37
62
|
Run the build script to build the framework.
|
|
@@ -41,4 +66,4 @@ npm install
|
|
|
41
66
|
npm run build
|
|
42
67
|
```
|
|
43
68
|
|
|
44
|
-
Use [nerest-harness](https://github.com/nerestjs/harness)
|
|
69
|
+
Use [nerest-harness](https://github.com/nerestjs/harness) to test changes locally.
|
package/bin/build.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { buildMicroFrontend } from '../build';
|
|
2
|
+
|
|
3
|
+
// Produce the production build of the Nerest micro frontend
|
|
4
|
+
export async function build() {
|
|
5
|
+
console.log('Nerest is preparing production build...');
|
|
6
|
+
await buildMicroFrontend();
|
|
7
|
+
console.log('Nerest build is finished');
|
|
8
|
+
}
|
package/bin/index.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// All executions of `nerest <command>` get routed through here
|
|
3
|
+
import 'dotenv/config';
|
|
4
|
+
|
|
5
|
+
import { build } from './build';
|
|
3
6
|
import { watch } from './watch';
|
|
4
7
|
|
|
8
|
+
// TODO: add CLI help and manual, maybe use a CLI framework like oclif
|
|
5
9
|
async function cliEntry(args: string[]) {
|
|
6
|
-
if (args[0] === '
|
|
10
|
+
if (args[0] === 'build') {
|
|
11
|
+
await build();
|
|
12
|
+
} else if (args[0] === 'watch') {
|
|
7
13
|
await watch();
|
|
8
14
|
}
|
|
9
15
|
}
|
package/bin/watch.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runDevelopmentServer } from '../server/development';
|
|
2
2
|
|
|
3
3
|
// Start dev server in watch mode, that restarts on file change
|
|
4
4
|
// and rebuilds the client static files
|
|
5
5
|
export async function watch() {
|
|
6
6
|
// TODO: will be replaced with nerest logger
|
|
7
7
|
console.log('Starting Nerest watch...');
|
|
8
|
-
|
|
9
|
-
const { app } = await createServer();
|
|
10
|
-
|
|
11
|
-
// TODO: remove hardcoded port
|
|
12
|
-
await app.listen({
|
|
13
|
-
host: '0.0.0.0',
|
|
14
|
-
port: 3000,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
8
|
+
await runDevelopmentServer();
|
|
18
9
|
}
|
package/build/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
import vite from 'vite';
|
|
5
|
+
import type { InlineConfig } from 'vite';
|
|
6
|
+
|
|
7
|
+
import { loadApps } from '../server/parts/apps';
|
|
8
|
+
|
|
9
|
+
export async function buildMicroFrontend() {
|
|
10
|
+
const root = process.cwd();
|
|
11
|
+
const staticPath = process.env.NEREST_STATIC_PATH;
|
|
12
|
+
|
|
13
|
+
// TODO: The path where the client files are deployed is built-in during
|
|
14
|
+
// the initial build, but the client scripts aren't using it, so maybe it should
|
|
15
|
+
// be a runtime env variable for the server instead?
|
|
16
|
+
if (!staticPath) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'NEREST_STATIC_PATH environment variable is not set but is required for the production build'
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Build client
|
|
23
|
+
// TODO: extract shared parts between build/index.ts and server/index.ts
|
|
24
|
+
// into a shared config
|
|
25
|
+
const clientConfig: InlineConfig = {
|
|
26
|
+
root,
|
|
27
|
+
appType: 'custom',
|
|
28
|
+
envPrefix: 'NEREST_',
|
|
29
|
+
build: {
|
|
30
|
+
// Manifest is needed to report used assets in SSR handles
|
|
31
|
+
manifest: true,
|
|
32
|
+
modulePreload: false,
|
|
33
|
+
rollupOptions: {
|
|
34
|
+
input: '/node_modules/@nerest/nerest/client/index.ts',
|
|
35
|
+
output: {
|
|
36
|
+
dir: 'build',
|
|
37
|
+
entryFileNames: `client/assets/[name].js`,
|
|
38
|
+
chunkFileNames: `client/assets/[name].js`,
|
|
39
|
+
assetFileNames: `client/assets/[name].[ext]`,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
console.log('Producing production client build...');
|
|
46
|
+
await vite.build(clientConfig);
|
|
47
|
+
|
|
48
|
+
console.log('Producing Nerest manifest file...');
|
|
49
|
+
await buildAppsManifest(root, staticPath);
|
|
50
|
+
|
|
51
|
+
// Build server using the client manifest
|
|
52
|
+
const serverConfig: InlineConfig = {
|
|
53
|
+
root,
|
|
54
|
+
appType: 'custom',
|
|
55
|
+
envPrefix: 'NEREST_',
|
|
56
|
+
build: {
|
|
57
|
+
emptyOutDir: false,
|
|
58
|
+
modulePreload: false,
|
|
59
|
+
// This is an important setting for producing a server build
|
|
60
|
+
ssr: true,
|
|
61
|
+
rollupOptions: {
|
|
62
|
+
input: '/node_modules/@nerest/nerest/server/production.ts',
|
|
63
|
+
output: {
|
|
64
|
+
dir: 'build',
|
|
65
|
+
entryFileNames: `server.mjs`,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
console.log('Producing production server build...');
|
|
72
|
+
await vite.build(serverConfig);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function buildAppsManifest(root: string, staticPath: string) {
|
|
76
|
+
const apps = await loadApps(root, staticPath);
|
|
77
|
+
await fs.writeFile(
|
|
78
|
+
path.join(root, 'build/nerest-manifest.json'),
|
|
79
|
+
JSON.stringify(apps),
|
|
80
|
+
{ encoding: 'utf-8' }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function build(): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.build = void 0;
|
|
4
|
+
const build_1 = require("../build");
|
|
5
|
+
// Produce the production build of the Nerest micro frontend
|
|
6
|
+
async function build() {
|
|
7
|
+
console.log('Nerest is preparing production build...');
|
|
8
|
+
await (0, build_1.buildMicroFrontend)();
|
|
9
|
+
console.log('Nerest build is finished');
|
|
10
|
+
}
|
|
11
|
+
exports.build = build;
|
package/dist/bin/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import 'dotenv/config';
|
package/dist/bin/index.js
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
// All executions of `nerest <command>` get routed through here
|
|
5
|
+
require("dotenv/config");
|
|
6
|
+
const build_1 = require("./build");
|
|
5
7
|
const watch_1 = require("./watch");
|
|
8
|
+
// TODO: add CLI help and manual, maybe use a CLI framework like oclif
|
|
6
9
|
async function cliEntry(args) {
|
|
7
|
-
if (args[0] === '
|
|
10
|
+
if (args[0] === 'build') {
|
|
11
|
+
await (0, build_1.build)();
|
|
12
|
+
}
|
|
13
|
+
else if (args[0] === 'watch') {
|
|
8
14
|
await (0, watch_1.watch)();
|
|
9
15
|
}
|
|
10
16
|
}
|
package/dist/bin/watch.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.watch = void 0;
|
|
4
|
-
const
|
|
4
|
+
const development_1 = require("../server/development");
|
|
5
5
|
// Start dev server in watch mode, that restarts on file change
|
|
6
6
|
// and rebuilds the client static files
|
|
7
7
|
async function watch() {
|
|
8
8
|
// TODO: will be replaced with nerest logger
|
|
9
9
|
console.log('Starting Nerest watch...');
|
|
10
|
-
|
|
11
|
-
// TODO: remove hardcoded port
|
|
12
|
-
await app.listen({
|
|
13
|
-
host: '0.0.0.0',
|
|
14
|
-
port: 3000,
|
|
15
|
-
});
|
|
16
|
-
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
10
|
+
await (0, development_1.runDevelopmentServer)();
|
|
17
11
|
}
|
|
18
12
|
exports.watch = watch;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildMicroFrontend(): Promise<void>;
|
|
@@ -0,0 +1,72 @@
|
|
|
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.buildMicroFrontend = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const vite_1 = __importDefault(require("vite"));
|
|
10
|
+
const apps_1 = require("../server/parts/apps");
|
|
11
|
+
async function buildMicroFrontend() {
|
|
12
|
+
const root = process.cwd();
|
|
13
|
+
const staticPath = process.env.NEREST_STATIC_PATH;
|
|
14
|
+
// TODO: The path where the client files are deployed is built-in during
|
|
15
|
+
// the initial build, but the client scripts aren't using it, so maybe it should
|
|
16
|
+
// be a runtime env variable for the server instead?
|
|
17
|
+
if (!staticPath) {
|
|
18
|
+
throw new Error('NEREST_STATIC_PATH environment variable is not set but is required for the production build');
|
|
19
|
+
}
|
|
20
|
+
// Build client
|
|
21
|
+
// TODO: extract shared parts between build/index.ts and server/index.ts
|
|
22
|
+
// into a shared config
|
|
23
|
+
const clientConfig = {
|
|
24
|
+
root,
|
|
25
|
+
appType: 'custom',
|
|
26
|
+
envPrefix: 'NEREST_',
|
|
27
|
+
build: {
|
|
28
|
+
// Manifest is needed to report used assets in SSR handles
|
|
29
|
+
manifest: true,
|
|
30
|
+
modulePreload: false,
|
|
31
|
+
rollupOptions: {
|
|
32
|
+
input: '/node_modules/@nerest/nerest/client/index.ts',
|
|
33
|
+
output: {
|
|
34
|
+
dir: 'build',
|
|
35
|
+
entryFileNames: `client/assets/[name].js`,
|
|
36
|
+
chunkFileNames: `client/assets/[name].js`,
|
|
37
|
+
assetFileNames: `client/assets/[name].[ext]`,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
console.log('Producing production client build...');
|
|
43
|
+
await vite_1.default.build(clientConfig);
|
|
44
|
+
console.log('Producing Nerest manifest file...');
|
|
45
|
+
await buildAppsManifest(root, staticPath);
|
|
46
|
+
// Build server using the client manifest
|
|
47
|
+
const serverConfig = {
|
|
48
|
+
root,
|
|
49
|
+
appType: 'custom',
|
|
50
|
+
envPrefix: 'NEREST_',
|
|
51
|
+
build: {
|
|
52
|
+
emptyOutDir: false,
|
|
53
|
+
modulePreload: false,
|
|
54
|
+
// This is an important setting for producing a server build
|
|
55
|
+
ssr: true,
|
|
56
|
+
rollupOptions: {
|
|
57
|
+
input: '/node_modules/@nerest/nerest/server/production.ts',
|
|
58
|
+
output: {
|
|
59
|
+
dir: 'build',
|
|
60
|
+
entryFileNames: `server.mjs`,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
console.log('Producing production server build...');
|
|
66
|
+
await vite_1.default.build(serverConfig);
|
|
67
|
+
}
|
|
68
|
+
exports.buildMicroFrontend = buildMicroFrontend;
|
|
69
|
+
async function buildAppsManifest(root, staticPath) {
|
|
70
|
+
const apps = await (0, apps_1.loadApps)(root, staticPath);
|
|
71
|
+
await promises_1.default.writeFile(path_1.default.join(root, 'build/nerest-manifest.json'), JSON.stringify(apps), { encoding: 'utf-8' });
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDevelopmentServer(): Promise<void>;
|
|
@@ -3,20 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
// This is the nerest server entrypoint
|
|
6
|
+
exports.runDevelopmentServer = void 0;
|
|
7
|
+
// This is the nerest development server entrypoint
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const vite_1 = __importDefault(require("vite"));
|
|
10
10
|
const fastify_1 = __importDefault(require("fastify"));
|
|
11
11
|
const static_1 = __importDefault(require("@fastify/static"));
|
|
12
|
-
const apps_1 = require("./apps");
|
|
13
|
-
const render_1 = require("./render");
|
|
14
|
-
const preview_1 = require("./preview");
|
|
15
|
-
const validator_1 = require("./validator");
|
|
16
|
-
const swagger_1 = require("./swagger");
|
|
17
|
-
|
|
18
|
-
// will most likely be implemented separately
|
|
19
|
-
async function createServer() {
|
|
12
|
+
const apps_1 = require("./parts/apps");
|
|
13
|
+
const render_1 = require("./parts/render");
|
|
14
|
+
const preview_1 = require("./parts/preview");
|
|
15
|
+
const validator_1 = require("./parts/validator");
|
|
16
|
+
const swagger_1 = require("./parts/swagger");
|
|
17
|
+
async function runDevelopmentServer() {
|
|
20
18
|
const root = process.cwd();
|
|
21
19
|
// TODO: move build config into a separate file
|
|
22
20
|
// TODO: look at @vitejs/plugin-react (everything seems fine without it though)
|
|
@@ -24,6 +22,7 @@ async function createServer() {
|
|
|
24
22
|
const config = {
|
|
25
23
|
root,
|
|
26
24
|
appType: 'custom',
|
|
25
|
+
envPrefix: 'NEREST_',
|
|
27
26
|
server: { middlewareMode: true },
|
|
28
27
|
build: {
|
|
29
28
|
// Manifest is needed to report used assets in SSR handles
|
|
@@ -34,18 +33,25 @@ async function createServer() {
|
|
|
34
33
|
rollupOptions: {
|
|
35
34
|
input: '/node_modules/@nerest/nerest/client/index.ts',
|
|
36
35
|
output: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
dir: 'build',
|
|
37
|
+
entryFileNames: `client/assets/[name].js`,
|
|
38
|
+
chunkFileNames: `client/assets/[name].js`,
|
|
39
|
+
assetFileNames: `client/assets/[name].[ext]`,
|
|
40
40
|
},
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
|
+
// TODO: this doesn't seem to work without the index.html file entry and
|
|
44
|
+
// produces warnings in dev mode. look into this maybe
|
|
45
|
+
optimizeDeps: {
|
|
46
|
+
disabled: true,
|
|
47
|
+
},
|
|
43
48
|
};
|
|
44
49
|
// Build the clientside assets and watch for changes
|
|
45
50
|
// TODO: this should probably be moved from here
|
|
46
51
|
await startClientBuildWatcher(config);
|
|
47
52
|
// Load app entries following the `apps/{name}/index.tsx` convention
|
|
48
|
-
|
|
53
|
+
// TODO: remove hardcoded port
|
|
54
|
+
const apps = await (0, apps_1.loadApps)(root, 'http://0.0.0.0:3000/');
|
|
49
55
|
// Start vite server that will be rendering SSR components
|
|
50
56
|
const viteSsr = await vite_1.default.createServer(config);
|
|
51
57
|
const app = (0, fastify_1.default)();
|
|
@@ -57,8 +63,7 @@ async function createServer() {
|
|
|
57
63
|
for (const appEntry of Object.values(apps)) {
|
|
58
64
|
const { name, entry, examples, schema } = appEntry;
|
|
59
65
|
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
|
|
66
|
+
// TODO: report error if schema is missing, unless this app is client-only
|
|
62
67
|
if (schema) {
|
|
63
68
|
routeOptions.schema = {
|
|
64
69
|
// Use description as Swagger summary, since summary is visible
|
|
@@ -79,7 +84,11 @@ async function createServer() {
|
|
|
79
84
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
80
85
|
fixStacktrace: true,
|
|
81
86
|
});
|
|
82
|
-
return (0, render_1.renderApp)(
|
|
87
|
+
return (0, render_1.renderApp)({
|
|
88
|
+
name,
|
|
89
|
+
assets: appEntry.assets,
|
|
90
|
+
component: ssrComponent.default,
|
|
91
|
+
}, request.body);
|
|
83
92
|
});
|
|
84
93
|
for (const [exampleName, example] of Object.entries(examples)) {
|
|
85
94
|
// Validate example against schema when specified
|
|
@@ -100,7 +109,11 @@ async function createServer() {
|
|
|
100
109
|
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
101
110
|
fixStacktrace: true,
|
|
102
111
|
});
|
|
103
|
-
const { html, assets } = (0, render_1.renderApp)(
|
|
112
|
+
const { html, assets } = (0, render_1.renderApp)({
|
|
113
|
+
name,
|
|
114
|
+
assets: appEntry.assets,
|
|
115
|
+
component: ssrComponent.default,
|
|
116
|
+
}, example);
|
|
104
117
|
reply.type('text/html');
|
|
105
118
|
return (0, preview_1.renderPreviewPage)(html, assets);
|
|
106
119
|
});
|
|
@@ -108,7 +121,7 @@ async function createServer() {
|
|
|
108
121
|
}
|
|
109
122
|
// TODO: only do this locally, load from CDN in production
|
|
110
123
|
await app.register(static_1.default, {
|
|
111
|
-
root: path_1.default.join(root, '
|
|
124
|
+
root: path_1.default.join(root, 'build'),
|
|
112
125
|
// TODO: maybe use @fastify/cors instead
|
|
113
126
|
setHeaders(res) {
|
|
114
127
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -116,9 +129,14 @@ async function createServer() {
|
|
|
116
129
|
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, content-type, Authorization');
|
|
117
130
|
},
|
|
118
131
|
});
|
|
119
|
-
|
|
132
|
+
// TODO: remove hardcoded port
|
|
133
|
+
await app.listen({
|
|
134
|
+
host: '0.0.0.0',
|
|
135
|
+
port: 3000,
|
|
136
|
+
});
|
|
137
|
+
console.log('Nerest is listening on 0.0.0.0:3000');
|
|
120
138
|
}
|
|
121
|
-
exports.
|
|
139
|
+
exports.runDevelopmentServer = runDevelopmentServer;
|
|
122
140
|
// TODO: this should probably be moved from here
|
|
123
141
|
async function startClientBuildWatcher(config) {
|
|
124
142
|
const watcher = (await vite_1.default.build(config));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadAppAssets = void 0;
|
|
4
|
+
// Extracts the list of assets for a given app from the manifest file
|
|
5
|
+
function loadAppAssets(appName, manifest, staticPath) {
|
|
6
|
+
// TODO: handling errors and potentially missing entries
|
|
7
|
+
// All apps share the same JS entry that dynamically imports the chunks of the apps
|
|
8
|
+
// that are used on the page based on their name
|
|
9
|
+
const clientEntryJs = manifest['node_modules/@nerest/nerest/client/index.ts'].file;
|
|
10
|
+
// Each app has its own CSS bundles, if it imports any CSS
|
|
11
|
+
const appCss = manifest[`apps/${appName}/index.tsx`].css ?? [];
|
|
12
|
+
return [clientEntryJs, ...appCss].map((x) => staticPath + x);
|
|
13
|
+
}
|
|
14
|
+
exports.loadAppAssets = loadAppAssets;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadAppManifest(root: string): Promise<any>;
|
|
@@ -0,0 +1,17 @@
|
|
|
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.loadAppManifest = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
// Manifest is used to provide assets list for every app
|
|
10
|
+
// for use with SSR
|
|
11
|
+
async function loadAppManifest(root) {
|
|
12
|
+
// TODO: error handling
|
|
13
|
+
const manifestPath = path_1.default.join(root, 'build', 'manifest.json');
|
|
14
|
+
const manifestData = await promises_1.default.readFile(manifestPath, { encoding: 'utf8' });
|
|
15
|
+
return JSON.parse(manifestData);
|
|
16
|
+
}
|
|
17
|
+
exports.loadAppManifest = loadAppManifest;
|
|
@@ -6,6 +6,6 @@ export type AppEntry = {
|
|
|
6
6
|
examples: Record<string, unknown>;
|
|
7
7
|
schema: Record<string, unknown> | null;
|
|
8
8
|
};
|
|
9
|
-
export declare function loadApps(root: string): Promise<{
|
|
9
|
+
export declare function loadApps(root: string, deployedStaticPath: string): Promise<{
|
|
10
10
|
[k: string]: AppEntry;
|
|
11
11
|
}>;
|
|
@@ -6,26 +6,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.loadApps = void 0;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
-
const assets_1 = require("
|
|
10
|
-
const examples_1 = require("
|
|
11
|
-
const schema_1 = require("
|
|
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");
|
|
12
13
|
// Build the record of the available apps by convention
|
|
13
14
|
// apps -> /apps/{name}/index.tsx
|
|
14
15
|
// examples -> /apps/{name}/examples/{example}.json
|
|
15
|
-
async function loadApps(root) {
|
|
16
|
+
async function loadApps(root, deployedStaticPath) {
|
|
16
17
|
const appsRoot = path_1.default.join(root, 'apps');
|
|
17
|
-
const manifest = await
|
|
18
|
+
const manifest = await (0, manifest_1.loadAppManifest)(root);
|
|
18
19
|
const appsDirs = (await promises_1.default.readdir(appsRoot, { withFileTypes: true }))
|
|
19
20
|
.filter((d) => d.isDirectory())
|
|
20
21
|
.map((d) => d.name);
|
|
21
22
|
const apps = [];
|
|
22
23
|
for (const appDir of appsDirs) {
|
|
23
|
-
apps.push(await loadApp(appsRoot, appDir, manifest));
|
|
24
|
+
apps.push(await loadApp(appsRoot, appDir, manifest, deployedStaticPath));
|
|
24
25
|
}
|
|
25
26
|
return Object.fromEntries(apps);
|
|
26
27
|
}
|
|
27
28
|
exports.loadApps = loadApps;
|
|
28
|
-
async function loadApp(appsRoot, name, manifest) {
|
|
29
|
+
async function loadApp(appsRoot, name, manifest, deployedStaticPath) {
|
|
29
30
|
// TODO: report problems with loading entries, assets and/or examples
|
|
30
31
|
const appRoot = path_1.default.join(appsRoot, name);
|
|
31
32
|
return [
|
|
@@ -34,17 +35,9 @@ async function loadApp(appsRoot, name, manifest) {
|
|
|
34
35
|
name,
|
|
35
36
|
root: appRoot,
|
|
36
37
|
entry: path_1.default.join(appRoot, 'index.tsx'),
|
|
37
|
-
assets: (0, assets_1.loadAppAssets)(manifest,
|
|
38
|
+
assets: (0, assets_1.loadAppAssets)(name, manifest, deployedStaticPath),
|
|
38
39
|
examples: await (0, examples_1.loadAppExamples)(appRoot),
|
|
39
40
|
schema: await (0, schema_1.loadAppSchema)(appRoot),
|
|
40
41
|
},
|
|
41
42
|
];
|
|
42
43
|
}
|
|
43
|
-
// Manifest is used to provide assets list for every app
|
|
44
|
-
// for use with SSR
|
|
45
|
-
async function loadManifest(root) {
|
|
46
|
-
// TODO: error handling
|
|
47
|
-
const manifestPath = path_1.default.join(root, 'dist', 'manifest.json');
|
|
48
|
-
const manifestData = await promises_1.default.readFile(manifestPath, { encoding: 'utf8' });
|
|
49
|
-
return JSON.parse(manifestData);
|
|
50
|
-
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type RenderProps = {
|
|
3
|
+
name: string;
|
|
4
|
+
assets: string[];
|
|
5
|
+
component: React.ComponentType;
|
|
6
|
+
};
|
|
7
|
+
export declare function renderApp({ name, assets, component }: RenderProps, props?: Record<string, unknown>): {
|
|
8
|
+
html: string;
|
|
9
|
+
assets: string[];
|
|
10
|
+
};
|
|
11
|
+
export {};
|
|
@@ -7,9 +7,8 @@ exports.renderApp = void 0;
|
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const server_1 = require("react-dom/server");
|
|
9
9
|
const nanoid_1 = require("nanoid");
|
|
10
|
-
function renderApp(
|
|
11
|
-
const
|
|
12
|
-
const html = renderSsrComponent(name, appComponent, props);
|
|
10
|
+
function renderApp({ name, assets, component }, props = {}) {
|
|
11
|
+
const html = renderSsrComponent(name, component, props);
|
|
13
12
|
return { html, assets };
|
|
14
13
|
}
|
|
15
14
|
exports.renderApp = renderApp;
|
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,29 +47,30 @@
|
|
|
46
47
|
]
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
|
-
"@fastify/static": "^6.
|
|
50
|
-
"@fastify/swagger": "^8.
|
|
51
|
-
"@fastify/swagger-ui": "^1.
|
|
50
|
+
"@fastify/static": "^6.10.2",
|
|
51
|
+
"@fastify/swagger": "^8.5.1",
|
|
52
|
+
"@fastify/swagger-ui": "^1.8.1",
|
|
52
53
|
"ajv": "^8.12.0",
|
|
53
54
|
"ajv-formats": "^2.1.1",
|
|
55
|
+
"dotenv": "^16.1.4",
|
|
54
56
|
"fast-uri": "^2.2.0",
|
|
55
|
-
"fastify": "^4.
|
|
56
|
-
"nanoid": "^3.3.
|
|
57
|
-
"vite": "^4.
|
|
57
|
+
"fastify": "^4.17.0",
|
|
58
|
+
"nanoid": "^3.3.6",
|
|
59
|
+
"vite": "^4.3.9"
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
60
|
-
"@tinkoff/eslint-config": "^1.
|
|
61
|
-
"@tinkoff/eslint-config-react": "^1.
|
|
62
|
-
"@tinkoff/prettier-config": "^1.
|
|
63
|
-
"@types/react": "^18.
|
|
64
|
-
"@types/react-dom": "^18.
|
|
65
|
-
"jest": "^29.
|
|
66
|
-
"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",
|
|
67
69
|
"react": "^18.2.0",
|
|
68
70
|
"react-dom": "^18.2.0",
|
|
69
71
|
"simple-git-hooks": "^2.8.1",
|
|
70
72
|
"sort-package-json": "^2.4.1",
|
|
71
|
-
"typescript": "^
|
|
73
|
+
"typescript": "^5.0.4"
|
|
72
74
|
},
|
|
73
75
|
"peerDependencies": {
|
|
74
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,15 +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';
|
|
17
|
-
import { setupSwagger } from './swagger';
|
|
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';
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
// will most likely be implemented separately
|
|
21
|
-
export async function createServer() {
|
|
19
|
+
export async function runDevelopmentServer() {
|
|
22
20
|
const root = process.cwd();
|
|
23
21
|
|
|
24
22
|
// TODO: move build config into a separate file
|
|
@@ -27,6 +25,7 @@ export async function createServer() {
|
|
|
27
25
|
const config: InlineConfig = {
|
|
28
26
|
root,
|
|
29
27
|
appType: 'custom',
|
|
28
|
+
envPrefix: 'NEREST_',
|
|
30
29
|
server: { middlewareMode: true },
|
|
31
30
|
build: {
|
|
32
31
|
// Manifest is needed to report used assets in SSR handles
|
|
@@ -37,12 +36,18 @@ export async function createServer() {
|
|
|
37
36
|
rollupOptions: {
|
|
38
37
|
input: '/node_modules/@nerest/nerest/client/index.ts',
|
|
39
38
|
output: {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
dir: 'build',
|
|
40
|
+
entryFileNames: `client/assets/[name].js`,
|
|
41
|
+
chunkFileNames: `client/assets/[name].js`,
|
|
42
|
+
assetFileNames: `client/assets/[name].[ext]`,
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
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
|
+
},
|
|
46
51
|
};
|
|
47
52
|
|
|
48
53
|
// Build the clientside assets and watch for changes
|
|
@@ -50,7 +55,8 @@ export async function createServer() {
|
|
|
50
55
|
await startClientBuildWatcher(config);
|
|
51
56
|
|
|
52
57
|
// Load app entries following the `apps/{name}/index.tsx` convention
|
|
53
|
-
|
|
58
|
+
// TODO: remove hardcoded port
|
|
59
|
+
const apps = await loadApps(root, 'http://0.0.0.0:3000/');
|
|
54
60
|
|
|
55
61
|
// Start vite server that will be rendering SSR components
|
|
56
62
|
const viteSsr = await vite.createServer(config);
|
|
@@ -68,8 +74,7 @@ export async function createServer() {
|
|
|
68
74
|
|
|
69
75
|
const routeOptions: RouteShorthandOptions = {};
|
|
70
76
|
|
|
71
|
-
// TODO: report error if schema is missing, unless this app is client-only
|
|
72
|
-
// TODO: disallow apps without schemas in production build
|
|
77
|
+
// TODO: report error if schema is missing, unless this app is client-only
|
|
73
78
|
if (schema) {
|
|
74
79
|
routeOptions.schema = {
|
|
75
80
|
// Use description as Swagger summary, since summary is visible
|
|
@@ -92,8 +97,11 @@ export async function createServer() {
|
|
|
92
97
|
fixStacktrace: true,
|
|
93
98
|
});
|
|
94
99
|
return renderApp(
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
{
|
|
101
|
+
name,
|
|
102
|
+
assets: appEntry.assets,
|
|
103
|
+
component: ssrComponent.default,
|
|
104
|
+
},
|
|
97
105
|
request.body as Record<string, unknown>
|
|
98
106
|
);
|
|
99
107
|
});
|
|
@@ -124,8 +132,11 @@ export async function createServer() {
|
|
|
124
132
|
fixStacktrace: true,
|
|
125
133
|
});
|
|
126
134
|
const { html, assets } = renderApp(
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
{
|
|
136
|
+
name,
|
|
137
|
+
assets: appEntry.assets,
|
|
138
|
+
component: ssrComponent.default,
|
|
139
|
+
},
|
|
129
140
|
example as Record<string, unknown>
|
|
130
141
|
);
|
|
131
142
|
|
|
@@ -139,7 +150,7 @@ export async function createServer() {
|
|
|
139
150
|
|
|
140
151
|
// TODO: only do this locally, load from CDN in production
|
|
141
152
|
await app.register(fastifyStatic, {
|
|
142
|
-
root: path.join(root, '
|
|
153
|
+
root: path.join(root, 'build'),
|
|
143
154
|
// TODO: maybe use @fastify/cors instead
|
|
144
155
|
setHeaders(res: ServerResponse) {
|
|
145
156
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -151,7 +162,13 @@ export async function createServer() {
|
|
|
151
162
|
},
|
|
152
163
|
});
|
|
153
164
|
|
|
154
|
-
|
|
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');
|
|
155
172
|
}
|
|
156
173
|
|
|
157
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,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
package/dist/server/entry.js
DELETED
|
@@ -1,17 +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.renderSsrComponent = 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 renderSsrComponent(appName, AppComponent, props = {}) {
|
|
11
|
-
const html = (0, server_1.renderToString)(react_1.default.createElement(AppComponent, { ...props }));
|
|
12
|
-
const appId = (0, nanoid_1.nanoid)();
|
|
13
|
-
const container = `<div data-app-name="${appName}" data-app-id="${appId}">${html}</div>`;
|
|
14
|
-
const script = `<script type="application/json" data-app-id="${appId}">${JSON.stringify(props)}</script>`;
|
|
15
|
-
return container + script;
|
|
16
|
-
}
|
|
17
|
-
exports.renderSsrComponent = renderSsrComponent;
|
package/dist/server/index.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import type { ServerResponse } from 'http';
|
|
3
|
-
export declare function createServer(): Promise<{
|
|
4
|
-
app: import("fastify").FastifyInstance<import("http").Server<typeof import("http").IncomingMessage, typeof ServerResponse>, import("http").IncomingMessage, ServerResponse<import("http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault> & PromiseLike<import("fastify").FastifyInstance<import("http").Server<typeof import("http").IncomingMessage, typeof ServerResponse>, import("http").IncomingMessage, ServerResponse<import("http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
|
|
5
|
-
}>;
|
package/dist/server/preview.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function renderPreviewPage(html: string, assets: string[]): string;
|
package/dist/server/render.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { Manifest } from 'vite';
|
|
2
|
-
|
|
3
|
-
// TODO: this is not as simple in the real world
|
|
4
|
-
const publicPath = process.env.PUBLIC_PATH ?? 'http://0.0.0.0:3000/';
|
|
5
|
-
|
|
6
|
-
// Extracts the list of assets for a given app from the manifest file
|
|
7
|
-
export function loadAppAssets(manifest: Manifest, appName: string) {
|
|
8
|
-
const entries = Object.entries(manifest);
|
|
9
|
-
|
|
10
|
-
// TODO: handling errors and potentially missing entries
|
|
11
|
-
const clientEntryJs = entries.find(([_, entry]) => entry.isEntry)?.[1].file;
|
|
12
|
-
const appCss =
|
|
13
|
-
entries.find(([name, _]) => name.includes(`/${appName}/index.tsx`))?.[1]
|
|
14
|
-
.css ?? [];
|
|
15
|
-
|
|
16
|
-
return [clientEntryJs, ...appCss].map((x) => publicPath + x);
|
|
17
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|