@nerest/nerest 0.1.0 → 1.5.1
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/LICENSE.md +178 -0
- package/README.md +130 -39
- package/bin/index.ts +3 -0
- package/bin/typegen.ts +35 -0
- package/bin/watch.ts +3 -2
- package/build/configs/development.ts +63 -0
- package/build/configs/production.ts +59 -0
- package/build/configs/shared.ts +51 -0
- package/build/configs/vite-logger.development.ts +30 -0
- package/build/index.ts +53 -76
- package/client/index.ts +14 -2
- package/dist/bin/index.js +4 -0
- package/dist/bin/typegen.d.ts +1 -0
- package/dist/bin/typegen.js +31 -0
- package/dist/bin/watch.js +1 -2
- package/dist/build/configs/development.d.ts +4 -0
- package/dist/build/configs/development.js +53 -0
- package/dist/build/configs/production.d.ts +4 -0
- package/dist/build/configs/production.js +50 -0
- package/dist/build/configs/shared.d.ts +10 -0
- package/dist/build/configs/shared.js +24 -0
- package/dist/build/configs/vite-logger.development.d.ts +2 -0
- package/dist/build/configs/vite-logger.development.js +25 -0
- package/dist/build/index.js +39 -69
- package/dist/client/index.js +9 -2
- package/dist/server/development.d.ts +1 -1
- package/dist/server/development.js +51 -134
- package/dist/server/hooks/logger.d.ts +1 -2
- package/dist/server/hooks/logger.js +3 -0
- package/dist/server/hooks/props.d.ts +2 -2
- package/dist/server/hooks/props.js +2 -2
- package/dist/server/hooks/runtime.js +3 -3
- package/dist/server/{parts → loaders}/apps.d.ts +2 -1
- package/dist/server/loaders/apps.js +36 -0
- package/dist/server/loaders/assets.js +25 -2
- package/dist/server/loaders/build.d.ts +2 -0
- package/dist/server/loaders/build.js +11 -0
- package/dist/server/loaders/examples.js +7 -13
- package/dist/server/loaders/manifest.d.ts +8 -1
- package/dist/server/loaders/manifest.js +12 -4
- package/dist/server/loaders/preview.d.ts +4 -0
- package/dist/server/loaders/preview.js +11 -0
- package/dist/server/loaders/project.d.ts +16 -0
- package/dist/server/loaders/project.js +11 -0
- package/dist/server/loaders/schema.d.ts +2 -1
- package/dist/server/loaders/schema.js +12 -8
- package/dist/server/parts/k8s-probes.js +10 -6
- package/dist/server/parts/preview.d.ts +2 -1
- package/dist/server/parts/preview.js +5 -4
- package/dist/server/parts/render.d.ts +5 -3
- package/dist/server/parts/render.js +8 -8
- package/dist/server/parts/swagger.d.ts +2 -1
- package/dist/server/parts/swagger.js +8 -19
- package/dist/server/parts/validator.d.ts +3 -1
- package/dist/server/parts/validator.js +13 -0
- package/dist/server/production.js +17 -83
- package/dist/server/shared.d.ts +14 -0
- package/dist/server/shared.js +94 -0
- package/dist/server/utils.d.ts +1 -0
- package/dist/server/utils.js +5 -0
- package/package.json +45 -43
- package/schemas/nerest-build.schema.d.ts +19 -1
- package/schemas/nerest-build.schema.json +21 -1
- package/server/development.ts +67 -172
- package/server/hooks/logger.ts +3 -0
- package/server/hooks/props.ts +5 -5
- package/server/hooks/runtime.ts +3 -3
- package/server/loaders/apps.ts +58 -0
- package/server/loaders/assets.ts +30 -2
- package/server/loaders/build.ts +14 -0
- package/server/loaders/examples.ts +7 -15
- package/server/loaders/manifest.ts +23 -4
- package/server/loaders/preview.ts +17 -0
- package/server/loaders/project.ts +33 -0
- package/server/loaders/schema.ts +12 -10
- package/server/parts/k8s-probes.ts +26 -13
- package/server/parts/preview.ts +11 -4
- package/server/parts/render.ts +13 -9
- package/server/parts/swagger.ts +10 -29
- package/server/parts/validator.ts +14 -0
- package/server/production.ts +22 -110
- package/server/shared.ts +150 -0
- package/server/utils.ts +6 -0
- package/dist/server/parts/apps.js +0 -37
- package/dist/server/parts/props-hook.d.ts +0 -1
- package/dist/server/parts/props-hook.js +0 -8
- package/dist/server/parts/runtime-hook.d.ts +0 -2
- package/dist/server/parts/runtime-hook.js +0 -28
- package/server/parts/apps.ts +0 -59
package/dist/client/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import ReactDOM from 'react-dom/client';
|
|
|
7
7
|
// if needed.
|
|
8
8
|
const modules = import.meta.glob('/apps/*/index.tsx', { import: 'default' });
|
|
9
9
|
async function runHydration() {
|
|
10
|
-
for (const container of document.querySelectorAll(
|
|
11
|
-
const appName = container.getAttribute(
|
|
10
|
+
for (const container of document.querySelectorAll(`div[data-project-name="${import.meta.env.NEREST_PROJECT_NAME}"]`)) {
|
|
11
|
+
const appName = container.getAttribute(`data-app-name`);
|
|
12
12
|
const appModuleLoader = modules[`/apps/${appName}/index.tsx`];
|
|
13
13
|
if (!appModuleLoader || container.hasAttribute('data-app-hydrated')) {
|
|
14
14
|
continue;
|
|
@@ -24,9 +24,16 @@ async function runHydration() {
|
|
|
24
24
|
ReactDOM.hydrateRoot(container, reactElement);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
// Apps can only be hydrated after DOM is ready, because we need to query
|
|
28
|
+
// apps' containers and their corresponding scripts.
|
|
27
29
|
if (document.readyState !== 'complete') {
|
|
28
30
|
document.addEventListener('DOMContentLoaded', runHydration);
|
|
29
31
|
}
|
|
30
32
|
else {
|
|
31
33
|
runHydration();
|
|
32
34
|
}
|
|
35
|
+
// Entries might be self-initializing (e.g. client-only apps) or have other
|
|
36
|
+
// side effects. In that case we have to load them eagerly, so that their
|
|
37
|
+
// initialization code can run, even if there is nothing to hydrate.
|
|
38
|
+
const clientSideEffects = JSON.parse(import.meta.env.NEREST_CLIENT_SIDE_EFFECTS);
|
|
39
|
+
clientSideEffects?.forEach((name) => modules[`/apps/${name}/index.tsx`]?.());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function runDevelopmentServer(): Promise<void>;
|
|
1
|
+
export declare function runDevelopmentServer(port: number): Promise<void>;
|
|
@@ -1,155 +1,72 @@
|
|
|
1
1
|
// This is the nerest development server entrypoint
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { build, createServer } from 'vite';
|
|
4
|
-
import fastify from 'fastify';
|
|
3
|
+
import { build, createServer as createViteServer } from 'vite';
|
|
5
4
|
import fastifyStatic from '@fastify/static';
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
import { runRuntimeHook } from './hooks/runtime.js';
|
|
14
|
-
import { runPropsHook } from './hooks/props.js';
|
|
15
|
-
import { runLoggerHook } from './hooks/logger.js';
|
|
16
|
-
// eslint-disable-next-line max-statements
|
|
17
|
-
export async function runDevelopmentServer() {
|
|
5
|
+
import fastifyMiddie from '@fastify/middie';
|
|
6
|
+
import { createServer } from './shared.js';
|
|
7
|
+
import { viteConfigDevelopmentClient, viteConfigDevelopmentServer, } from '../build/configs/development.js';
|
|
8
|
+
import { loadBuildConfig } from './loaders/build.js';
|
|
9
|
+
import { loadApps } from './loaders/apps.js';
|
|
10
|
+
import { loadProject } from './loaders/project.js';
|
|
11
|
+
export async function runDevelopmentServer(port) {
|
|
18
12
|
const root = process.cwd();
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
manifest: true,
|
|
30
|
-
modulePreload: false,
|
|
31
|
-
// TODO: watch is only necessary for the client build
|
|
32
|
-
watch: {},
|
|
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
|
-
// 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
|
-
},
|
|
48
|
-
};
|
|
13
|
+
// Allow overriding STATIC_PATH in development, useful for debugging
|
|
14
|
+
// micro frontend from another device on the same local network
|
|
15
|
+
let staticPath = process.env.STATIC_PATH || `http://127.0.0.1:${port}/`;
|
|
16
|
+
if (!staticPath.endsWith('/')) {
|
|
17
|
+
staticPath += '/';
|
|
18
|
+
}
|
|
19
|
+
// Generate vite configuration with nerest/build.json applied
|
|
20
|
+
const buildConfig = await loadBuildConfig(root);
|
|
21
|
+
// Load project meta details
|
|
22
|
+
const project = await loadProject(root);
|
|
49
23
|
// Build the clientside assets and watch for changes
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
24
|
+
await startClientBuildWatcher(await viteConfigDevelopmentClient({
|
|
25
|
+
root,
|
|
26
|
+
base: staticPath,
|
|
27
|
+
buildConfig,
|
|
28
|
+
project,
|
|
29
|
+
}));
|
|
55
30
|
// Start vite server that will be rendering SSR components
|
|
56
|
-
const viteSsr = await
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
31
|
+
const viteSsr = await createViteServer(await viteConfigDevelopmentServer({
|
|
32
|
+
root,
|
|
33
|
+
base: staticPath,
|
|
34
|
+
buildConfig,
|
|
35
|
+
project,
|
|
36
|
+
}));
|
|
37
|
+
// Load app entries following the `apps/{name}/index.tsx` convention
|
|
38
|
+
const apps = await loadApps(root, staticPath);
|
|
39
|
+
const app = await createServer({
|
|
40
|
+
root,
|
|
41
|
+
project,
|
|
42
|
+
apps,
|
|
43
|
+
// ssrLoadModule picks up the changes without restarting the server
|
|
44
|
+
loadComponent: async (entry) => (await viteSsr.ssrLoadModule(`/apps/${entry}/index.tsx`, {
|
|
45
|
+
fixStacktrace: true,
|
|
46
|
+
})).default,
|
|
47
|
+
loadPropsHook: (entry) => viteSsr.ssrLoadModule(`/apps/${entry}/props.ts`),
|
|
48
|
+
loadRuntimeHook: () => viteSsr.ssrLoadModule('/nerest/runtime.ts'),
|
|
62
49
|
});
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (const appEntry of Object.values(apps)) {
|
|
69
|
-
const { name, entry, propsHookEntry, examples, schema } = appEntry;
|
|
70
|
-
const routeOptions = {};
|
|
71
|
-
// TODO: report error if schema is missing, unless this app is client-only
|
|
72
|
-
if (schema) {
|
|
73
|
-
routeOptions.schema = {
|
|
74
|
-
// Use description as Swagger summary, since summary is visible
|
|
75
|
-
// even when the route is collapsed in the UI
|
|
76
|
-
summary: schema.description,
|
|
77
|
-
body: {
|
|
78
|
-
...schema,
|
|
79
|
-
// Mix examples into the schema so they become accessible
|
|
80
|
-
// in the Swagger UI
|
|
81
|
-
examples: Object.values(examples),
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
// POST /api/{name} -> render app with request.body as props
|
|
86
|
-
app.post(`/api/${name}`, routeOptions, async (request) => {
|
|
87
|
-
// ssrLoadModule drives the "hot-reload" logic, and allows
|
|
88
|
-
// picking up changes to the source without restarting the server
|
|
89
|
-
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
90
|
-
fixStacktrace: true,
|
|
91
|
-
});
|
|
92
|
-
const props = await runPropsHook(request.body, request.log, () => viteSsr.ssrLoadModule(propsHookEntry));
|
|
93
|
-
return renderApp({
|
|
94
|
-
name,
|
|
95
|
-
assets: appEntry.assets,
|
|
96
|
-
component: ssrComponent.default,
|
|
97
|
-
}, props);
|
|
98
|
-
});
|
|
99
|
-
for (const [exampleName, example] of Object.entries(examples)) {
|
|
100
|
-
// Validate example against schema when specified
|
|
101
|
-
if (schema && !validator.validate(schema, example)) {
|
|
102
|
-
// TODO: use logger and display errors more prominently
|
|
103
|
-
console.error(`Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator.errorsText()}`);
|
|
104
|
-
}
|
|
105
|
-
// GET /api/{name}/examples/{example} -> render a preview page
|
|
106
|
-
// with a predefined example body
|
|
107
|
-
const exampleRoute = `/api/${name}/examples/${exampleName}`;
|
|
108
|
-
app.get(exampleRoute, {
|
|
109
|
-
schema: {
|
|
110
|
-
// Add a clickable link to the example route in route's Swagger
|
|
111
|
-
// description so it's easier to navigate to
|
|
112
|
-
description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
|
|
113
|
-
},
|
|
114
|
-
}, async (request, reply) => {
|
|
115
|
-
const ssrComponent = await viteSsr.ssrLoadModule(entry, {
|
|
116
|
-
fixStacktrace: true,
|
|
117
|
-
});
|
|
118
|
-
const props = await runPropsHook(example, request.log, () => viteSsr.ssrLoadModule(propsHookEntry));
|
|
119
|
-
const { html, assets } = renderApp({
|
|
120
|
-
name,
|
|
121
|
-
assets: appEntry.assets,
|
|
122
|
-
component: ssrComponent.default,
|
|
123
|
-
}, props);
|
|
124
|
-
reply.type('text/html');
|
|
125
|
-
return renderPreviewPage(html, assets);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// Add graceful shutdown handler to prevent requests errors
|
|
130
|
-
await app.register(fastifyGracefulShutdown);
|
|
131
|
-
if (process.env.ENABLE_K8S_PROBES) {
|
|
132
|
-
await setupK8SProbes(app);
|
|
133
|
-
}
|
|
134
|
-
// TODO: only do this locally, load from CDN in production
|
|
50
|
+
// Register middie to use vite's Connect-style middlewares
|
|
51
|
+
await app.register(fastifyMiddie);
|
|
52
|
+
app.use(viteSsr.middlewares);
|
|
53
|
+
// @fastify/static is only used locally for development, in production static
|
|
54
|
+
// files are served from STATIC_PATH, which is usually a CDN location
|
|
135
55
|
await app.register(fastifyStatic, {
|
|
136
|
-
root: path.join(root, 'build'),
|
|
137
|
-
//
|
|
56
|
+
root: path.join(root, 'build/client/assets'),
|
|
57
|
+
// Set CORS headers so the development server assets can be accessed from
|
|
58
|
+
// remote devices, e.g. mobile phones
|
|
138
59
|
setHeaders(res) {
|
|
139
60
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
140
61
|
res.setHeader('Access-Control-Allow-Methods', 'GET');
|
|
141
62
|
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, content-type, Authorization');
|
|
142
63
|
},
|
|
143
64
|
});
|
|
144
|
-
// Execute runtime hook in nerest-runtime.ts if it exists
|
|
145
|
-
await runRuntimeHook(app, () => viteSsr.ssrLoadModule('/nerest-runtime.ts'));
|
|
146
|
-
// TODO: remove hardcoded port
|
|
147
65
|
await app.listen({
|
|
148
66
|
host: '0.0.0.0',
|
|
149
|
-
port
|
|
67
|
+
port,
|
|
150
68
|
});
|
|
151
69
|
}
|
|
152
|
-
// TODO: this should probably be moved from here
|
|
153
70
|
async function startClientBuildWatcher(config) {
|
|
154
71
|
const watcher = (await build(config));
|
|
155
72
|
return new Promise((resolve) => {
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function runLoggerHook(loader: () => Promise<unknown>): Promise<boolean | import("fastify").FastifyBaseLogger | (import("fastify").FastifyLoggerOptions<import("fastify").RawServerDefault, import("fastify").FastifyRequest<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown, import("fastify").FastifyBaseLogger, import("fastify/types/type-provider.js").ResolveFastifyRequestType<import("fastify").FastifyTypeProviderDefault, import("fastify").FastifySchema, import("fastify").RouteGenericInterface>>, import("fastify").FastifyReply<import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, import("fastify").RouteGenericInterface, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>> & import("pino").default.LoggerOptions) | null | undefined>;
|
|
1
|
+
export declare function runLoggerHook(loader: () => Promise<unknown>): Promise<boolean | (import("fastify").FastifyLoggerOptions<import("fastify").RawServerDefault, import("fastify").FastifyRequest<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown, import("fastify").FastifyBaseLogger, import("fastify/types/type-provider.js").ResolveFastifyRequestType<import("fastify").FastifyTypeProviderDefault, import("fastify").FastifySchema, import("fastify").RouteGenericInterface>>, import("fastify").FastifyReply<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>> & import("fastify/types/logger.js").PinoLoggerOptions) | null | undefined>;
|
|
@@ -13,6 +13,9 @@ export async function runLoggerHook(loader) {
|
|
|
13
13
|
return module.logger();
|
|
14
14
|
}
|
|
15
15
|
catch (e) {
|
|
16
|
+
// Allow console.error here, because we can't app.log the error if we
|
|
17
|
+
// can't create the logger in the first place.
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
16
19
|
console.error('Failed to load logger configuration', e);
|
|
17
20
|
process.exit(1);
|
|
18
21
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function runPropsHook(
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
export declare function runPropsHook(app: FastifyInstance, loader: () => Promise<unknown>, props: unknown): Promise<Record<string, unknown>>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Load the props hook module and run it if it exists, passing down app's
|
|
2
2
|
// props object and logger. This hook can be used to modify props before
|
|
3
3
|
// they are passed to the root app component.
|
|
4
|
-
export async function runPropsHook(
|
|
4
|
+
export async function runPropsHook(app, loader, props) {
|
|
5
5
|
let module;
|
|
6
6
|
try {
|
|
7
7
|
module = (await loader());
|
|
@@ -9,6 +9,6 @@ export async function runPropsHook(props, logger, loader) {
|
|
|
9
9
|
catch { }
|
|
10
10
|
// If module exists and exports a default function, run it to modify props
|
|
11
11
|
return (typeof module?.default === 'function'
|
|
12
|
-
? module.default(props,
|
|
12
|
+
? module.default(props, app.log)
|
|
13
13
|
: props);
|
|
14
14
|
}
|
|
@@ -14,15 +14,15 @@ export async function runRuntimeHook(app, loader) {
|
|
|
14
14
|
await module.default(app);
|
|
15
15
|
}
|
|
16
16
|
catch (e) {
|
|
17
|
-
|
|
17
|
+
app.log.fatal('Failed to execute runtime hook', e);
|
|
18
18
|
process.exit(1);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
else if (module) {
|
|
22
|
-
|
|
22
|
+
app.log.fatal("Runtime hook found, but doesn't export default function!");
|
|
23
23
|
process.exit(1);
|
|
24
24
|
}
|
|
25
25
|
else {
|
|
26
|
-
|
|
26
|
+
app.log.info('Runtime hook not found, skipping...');
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { JSONSchema } from '@apidevtools/json-schema-ref-parser';
|
|
1
2
|
export type AppEntry = {
|
|
2
3
|
name: string;
|
|
3
4
|
root: string;
|
|
@@ -5,7 +6,7 @@ export type AppEntry = {
|
|
|
5
6
|
propsHookEntry: string;
|
|
6
7
|
assets: string[];
|
|
7
8
|
examples: Record<string, unknown>;
|
|
8
|
-
schema:
|
|
9
|
+
schema: JSONSchema | null;
|
|
9
10
|
};
|
|
10
11
|
export declare function loadApps(root: string, deployedStaticPath: string): Promise<{
|
|
11
12
|
[k: string]: AppEntry;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fg from 'fast-glob';
|
|
3
|
+
import { loadAppAssets } from './assets.js';
|
|
4
|
+
import { loadAppExamples } from './examples.js';
|
|
5
|
+
import { loadAppSchema } from './schema.js';
|
|
6
|
+
import { loadViteManifest } from './manifest.js';
|
|
7
|
+
// Build the record of the available apps by convention
|
|
8
|
+
// apps -> /apps/{name}/index.tsx
|
|
9
|
+
// examples -> /apps/{name}/examples/{example}.json
|
|
10
|
+
export async function loadApps(root, deployedStaticPath) {
|
|
11
|
+
const manifest = await loadViteManifest(root);
|
|
12
|
+
const appBase = path.join(root, 'apps');
|
|
13
|
+
const appPattern = `${fg.convertPathToPattern(appBase)}/*`;
|
|
14
|
+
const appDirs = await fg.glob(appPattern, { onlyDirectories: true });
|
|
15
|
+
const apps = [];
|
|
16
|
+
for (const appDir of appDirs) {
|
|
17
|
+
apps.push(await loadApp(appDir, manifest, deployedStaticPath));
|
|
18
|
+
}
|
|
19
|
+
return Object.fromEntries(apps);
|
|
20
|
+
}
|
|
21
|
+
async function loadApp(appDir, manifest, deployedStaticPath) {
|
|
22
|
+
// TODO: report problems with loading entries, assets and/or examples
|
|
23
|
+
const name = path.basename(appDir);
|
|
24
|
+
return [
|
|
25
|
+
name,
|
|
26
|
+
{
|
|
27
|
+
name,
|
|
28
|
+
root: appDir,
|
|
29
|
+
entry: path.join(appDir, 'index.tsx'),
|
|
30
|
+
propsHookEntry: path.join(appDir, 'props.ts'),
|
|
31
|
+
assets: loadAppAssets(name, manifest, deployedStaticPath),
|
|
32
|
+
examples: await loadAppExamples(appDir),
|
|
33
|
+
schema: await loadAppSchema(appDir),
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
}
|
|
@@ -5,6 +5,29 @@ export function loadAppAssets(appName, manifest, staticPath) {
|
|
|
5
5
|
// that are used on the page based on their name
|
|
6
6
|
const clientEntryJs = manifest['node_modules/@nerest/nerest/client/index.ts'].file;
|
|
7
7
|
// Each app has its own CSS bundles, if it imports any CSS
|
|
8
|
-
const appCss = manifest
|
|
9
|
-
return [clientEntryJs, ...appCss].map((x) =>
|
|
8
|
+
const appCss = collectCssUrls(manifest, `apps/${appName}/index.tsx`);
|
|
9
|
+
return [clientEntryJs, ...appCss].map((x) => new URL(x, staticPath).href);
|
|
10
|
+
}
|
|
11
|
+
// Collects all CSS URLs by walking the manifest tree of static imports
|
|
12
|
+
// for a specific entry. These CSS chunks are loaded dynamically by Vite,
|
|
13
|
+
// but we need to inject them into the page statically to prevent a flash
|
|
14
|
+
// of unstyled content
|
|
15
|
+
function collectCssUrls(manifest, entryName) {
|
|
16
|
+
const cssUrls = new Set();
|
|
17
|
+
const scannedEntries = new Set([entryName]);
|
|
18
|
+
const queue = [entryName];
|
|
19
|
+
while (queue.length > 0) {
|
|
20
|
+
const entry = queue.shift();
|
|
21
|
+
const manifestEntry = manifest[entry];
|
|
22
|
+
if (manifestEntry) {
|
|
23
|
+
manifestEntry.css?.forEach((url) => cssUrls.add(url));
|
|
24
|
+
manifestEntry.imports?.forEach((name) => {
|
|
25
|
+
if (!scannedEntries.has(name)) {
|
|
26
|
+
scannedEntries.add(name);
|
|
27
|
+
queue.push(name);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return cssUrls;
|
|
10
33
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
// TODO: error handling
|
|
5
|
+
export async function loadBuildConfig(root) {
|
|
6
|
+
const configPath = path.join(root, 'nerest/build.json');
|
|
7
|
+
if (existsSync(configPath)) {
|
|
8
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
9
|
+
return JSON.parse(content);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
2
|
import fs from 'fs/promises';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
4
|
// Loads and parses the example json files for providing
|
|
5
5
|
// `/examples/` routes of the dev server
|
|
6
6
|
export async function loadAppExamples(appRoot) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return {};
|
|
11
|
-
}
|
|
12
|
-
const exampleFiles = (await fs.readdir(examplesRoot, { withFileTypes: true }))
|
|
13
|
-
.filter((d) => d.isFile() && d.name.endsWith('.json'))
|
|
14
|
-
.map((d) => d.name);
|
|
7
|
+
const exampleBase = path.join(appRoot, 'examples');
|
|
8
|
+
const examplePattern = `${fg.convertPathToPattern(exampleBase)}/*.json`;
|
|
9
|
+
const exampleFiles = await fg.glob(examplePattern, { onlyFiles: true });
|
|
15
10
|
const examples = {};
|
|
16
11
|
// TODO: error handling and reporting
|
|
17
|
-
for (const
|
|
18
|
-
const
|
|
19
|
-
const content = await fs.readFile(file, { encoding: 'utf8' });
|
|
12
|
+
for (const filePath of exampleFiles) {
|
|
13
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
20
14
|
const json = JSON.parse(content);
|
|
21
|
-
examples[path.basename(
|
|
15
|
+
examples[path.basename(filePath, '.json')] = json;
|
|
22
16
|
}
|
|
23
17
|
return examples;
|
|
24
18
|
}
|
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Manifest as ViteManifest } from 'vite';
|
|
2
|
+
import type { Project } from './project.js';
|
|
3
|
+
import type { AppEntry } from './apps.js';
|
|
4
|
+
export declare function loadViteManifest(root: string): Promise<ViteManifest>;
|
|
5
|
+
export declare function loadNerestManifest(root: string): Promise<{
|
|
6
|
+
project: Project;
|
|
7
|
+
apps: Record<string, AppEntry>;
|
|
8
|
+
}>;
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
|
-
//
|
|
3
|
+
// Vite manifest is used to provide assets list for every app
|
|
4
4
|
// for use with SSR
|
|
5
|
-
export async function
|
|
5
|
+
export async function loadViteManifest(root) {
|
|
6
6
|
// TODO: error handling
|
|
7
|
-
const manifestPath = path.join(root, 'build
|
|
8
|
-
const manifestData = await fs.readFile(manifestPath,
|
|
7
|
+
const manifestPath = path.join(root, 'build/client/assets/.vite/manifest.json');
|
|
8
|
+
const manifestData = await fs.readFile(manifestPath, 'utf-8');
|
|
9
|
+
return JSON.parse(manifestData);
|
|
10
|
+
}
|
|
11
|
+
// Nerest manifest contains info about the whole project
|
|
12
|
+
// and every app within it
|
|
13
|
+
export async function loadNerestManifest(root) {
|
|
14
|
+
// TODO: error handling
|
|
15
|
+
const manifestPath = path.join(root, 'build/nerest-manifest.json');
|
|
16
|
+
const manifestData = await fs.readFile(manifestPath, 'utf-8');
|
|
9
17
|
return JSON.parse(manifestData);
|
|
10
18
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
export async function loadPreviewParts(root) {
|
|
5
|
+
let head = '';
|
|
6
|
+
const headPath = path.join(root, 'nerest/preview-head.html');
|
|
7
|
+
if (existsSync(headPath)) {
|
|
8
|
+
head = await fs.readFile(headPath, 'utf-8');
|
|
9
|
+
}
|
|
10
|
+
return { head };
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Project = {
|
|
2
|
+
name: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
homepage?: string;
|
|
6
|
+
repository?: string | {
|
|
7
|
+
url?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare function loadProject(root: string): Promise<{
|
|
11
|
+
name: any;
|
|
12
|
+
description: any;
|
|
13
|
+
version: any;
|
|
14
|
+
homepage: any;
|
|
15
|
+
repository: any;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
// Loads project meta information and available apps
|
|
4
|
+
export async function loadProject(root) {
|
|
5
|
+
const packageJson = await fs.readFile(path.join(root, 'package.json'), 'utf-8');
|
|
6
|
+
const { name = '', description, version, homepage, repository, } = JSON.parse(packageJson);
|
|
7
|
+
if (!name) {
|
|
8
|
+
console.warn('"name" is not set in package.json, this may cause conflicts when hydrating multiple apps from different micro frontends');
|
|
9
|
+
}
|
|
10
|
+
return { name, description, version, homepage, repository };
|
|
11
|
+
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import RefParser from '@apidevtools/json-schema-ref-parser';
|
|
2
|
+
export declare function loadAppSchema(appRoot: string): Promise<RefParser.JSONSchema | null>;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
|
-
import
|
|
3
|
+
import RefParser from '@apidevtools/json-schema-ref-parser';
|
|
4
4
|
// Loads and parses the schema file for a specific app
|
|
5
5
|
export async function loadAppSchema(appRoot) {
|
|
6
6
|
const schemaPath = path.join(appRoot, 'schema.json');
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
// We are using dereference to resolve $refs in schema files and
|
|
8
|
+
// to resolve relative dependencies between schemas. The resolved
|
|
9
|
+
// schema will be stringified and saved in the build manifest,
|
|
10
|
+
// so we need it to have no circular references.
|
|
11
|
+
return existsSync(schemaPath)
|
|
12
|
+
? RefParser.dereference(schemaPath, {
|
|
13
|
+
dereference: {
|
|
14
|
+
circular: false,
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
: null;
|
|
14
18
|
}
|
|
@@ -7,16 +7,20 @@ export async function setupK8SProbes(app) {
|
|
|
7
7
|
// - finishes all current requests
|
|
8
8
|
// - shuts down the server
|
|
9
9
|
let isShutdownInProgress = false;
|
|
10
|
-
app.gracefulShutdown((
|
|
11
|
-
|
|
12
|
-
console.log('Graceful shutdown in process...');
|
|
10
|
+
app.gracefulShutdown(() => {
|
|
11
|
+
app.log.info('Graceful shutdown in process...');
|
|
13
12
|
isShutdownInProgress = true;
|
|
14
|
-
next();
|
|
15
13
|
});
|
|
16
|
-
app.get('/livenessProbe', {
|
|
14
|
+
app.get('/livenessProbe', {
|
|
15
|
+
schema: { tags: ['@service'] },
|
|
16
|
+
logLevel: 'silent',
|
|
17
|
+
}, (req, res) => {
|
|
17
18
|
res.status(200).send();
|
|
18
19
|
});
|
|
19
|
-
app.get('/readinessProbe', {
|
|
20
|
+
app.get('/readinessProbe', {
|
|
21
|
+
schema: { tags: ['@service'] },
|
|
22
|
+
logLevel: 'silent',
|
|
23
|
+
}, (req, res) => {
|
|
20
24
|
if (isShutdownInProgress) {
|
|
21
25
|
res.status(503).send({ status: 'Shutdown in progress' });
|
|
22
26
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { PreviewParts } from '../loaders/preview.js';
|
|
2
|
+
export declare function renderPreviewPage(html: string, assets: string[], parts: PreviewParts): string;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// Renders the preview page available by convention at /api/{name}/examples/{example}
|
|
2
|
-
export function renderPreviewPage(html, assets) {
|
|
2
|
+
export function renderPreviewPage(html, assets, parts) {
|
|
3
3
|
const { scripts, styles } = mapAssets(assets);
|
|
4
4
|
return `
|
|
5
|
+
<!DOCTYPE html>
|
|
5
6
|
<html>
|
|
6
7
|
<head>
|
|
8
|
+
<meta charset="utf-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
10
|
${styles.join('\n')}
|
|
11
|
+
${parts.head}
|
|
8
12
|
</head>
|
|
9
13
|
<body>
|
|
10
14
|
${html}
|
|
@@ -14,9 +18,6 @@ export function renderPreviewPage(html, assets) {
|
|
|
14
18
|
`;
|
|
15
19
|
}
|
|
16
20
|
function mapAssets(assets) {
|
|
17
|
-
// TODO: script type="module" is not supported by older browsers
|
|
18
|
-
// but vite doesn't provide `nomodule` fallback by default
|
|
19
|
-
// see @vitejs/plugin-legacy
|
|
20
21
|
const scripts = assets
|
|
21
22
|
.filter((src) => src.endsWith('.js'))
|
|
22
23
|
.map((src) => `<script type="module" src="${src}"></script>`);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { Project } from '../loaders/project.js';
|
|
2
3
|
type RenderProps = {
|
|
3
4
|
name: string;
|
|
4
5
|
assets: string[];
|
|
5
|
-
component:
|
|
6
|
+
component: ComponentType;
|
|
7
|
+
project: Project;
|
|
6
8
|
};
|
|
7
|
-
export declare function renderApp({ name, assets, component }: RenderProps, props?: Record<string, unknown>): {
|
|
9
|
+
export declare function renderApp({ name, assets, component, project }: RenderProps, props?: Record<string, unknown>): {
|
|
8
10
|
html: string;
|
|
9
11
|
assets: string[];
|
|
10
12
|
};
|