@nerest/nerest 0.0.9 → 1.5.0

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.
Files changed (89) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +148 -39
  3. package/bin/index.ts +3 -0
  4. package/bin/typegen.ts +35 -0
  5. package/bin/watch.ts +3 -2
  6. package/build/configs/development.ts +63 -0
  7. package/build/configs/production.ts +59 -0
  8. package/build/configs/shared.ts +51 -0
  9. package/build/configs/vite-logger.development.ts +30 -0
  10. package/build/index.ts +53 -76
  11. package/client/index.ts +14 -2
  12. package/dist/bin/index.js +4 -0
  13. package/dist/bin/typegen.d.ts +1 -0
  14. package/dist/bin/typegen.js +31 -0
  15. package/dist/bin/watch.js +1 -2
  16. package/dist/build/configs/development.d.ts +4 -0
  17. package/dist/build/configs/development.js +53 -0
  18. package/dist/build/configs/production.d.ts +4 -0
  19. package/dist/build/configs/production.js +50 -0
  20. package/dist/build/configs/shared.d.ts +10 -0
  21. package/dist/build/configs/shared.js +24 -0
  22. package/dist/build/configs/vite-logger.development.d.ts +2 -0
  23. package/dist/build/configs/vite-logger.development.js +25 -0
  24. package/dist/build/index.js +39 -69
  25. package/dist/client/index.js +9 -2
  26. package/dist/server/development.d.ts +1 -1
  27. package/dist/server/development.js +52 -130
  28. package/dist/server/hooks/logger.d.ts +1 -0
  29. package/dist/server/hooks/logger.js +24 -0
  30. package/dist/server/hooks/props.d.ts +2 -1
  31. package/dist/server/hooks/props.js +6 -4
  32. package/dist/server/hooks/runtime.js +3 -3
  33. package/dist/server/{parts → loaders}/apps.d.ts +2 -1
  34. package/dist/server/loaders/apps.js +36 -0
  35. package/dist/server/loaders/assets.js +25 -2
  36. package/dist/server/loaders/build.d.ts +2 -0
  37. package/dist/server/loaders/build.js +11 -0
  38. package/dist/server/loaders/examples.js +7 -13
  39. package/dist/server/loaders/manifest.d.ts +8 -1
  40. package/dist/server/loaders/manifest.js +12 -4
  41. package/dist/server/loaders/preview.d.ts +4 -0
  42. package/dist/server/loaders/preview.js +11 -0
  43. package/dist/server/loaders/project.d.ts +16 -0
  44. package/dist/server/loaders/project.js +11 -0
  45. package/dist/server/loaders/schema.d.ts +2 -1
  46. package/dist/server/loaders/schema.js +12 -8
  47. package/dist/server/parts/k8s-probes.js +10 -6
  48. package/dist/server/parts/preview.d.ts +2 -1
  49. package/dist/server/parts/preview.js +5 -4
  50. package/dist/server/parts/render.d.ts +5 -3
  51. package/dist/server/parts/render.js +8 -8
  52. package/dist/server/parts/swagger.d.ts +2 -1
  53. package/dist/server/parts/swagger.js +8 -19
  54. package/dist/server/parts/validator.d.ts +3 -1
  55. package/dist/server/parts/validator.js +13 -0
  56. package/dist/server/production.js +17 -80
  57. package/dist/server/shared.d.ts +14 -0
  58. package/dist/server/shared.js +94 -0
  59. package/dist/server/utils.d.ts +1 -0
  60. package/dist/server/utils.js +5 -0
  61. package/package.json +46 -43
  62. package/schemas/nerest-build.schema.d.ts +19 -1
  63. package/schemas/nerest-build.schema.json +21 -1
  64. package/server/development.ts +67 -164
  65. package/server/hooks/logger.ts +31 -0
  66. package/server/hooks/props.ts +11 -6
  67. package/server/hooks/runtime.ts +3 -3
  68. package/server/loaders/apps.ts +58 -0
  69. package/server/loaders/assets.ts +30 -2
  70. package/server/loaders/build.ts +14 -0
  71. package/server/loaders/examples.ts +7 -15
  72. package/server/loaders/manifest.ts +23 -4
  73. package/server/loaders/preview.ts +17 -0
  74. package/server/loaders/project.ts +33 -0
  75. package/server/loaders/schema.ts +12 -10
  76. package/server/parts/k8s-probes.ts +26 -13
  77. package/server/parts/preview.ts +11 -4
  78. package/server/parts/render.ts +13 -9
  79. package/server/parts/swagger.ts +10 -29
  80. package/server/parts/validator.ts +14 -0
  81. package/server/production.ts +22 -106
  82. package/server/shared.ts +150 -0
  83. package/server/utils.ts +6 -0
  84. package/dist/server/parts/apps.js +0 -37
  85. package/dist/server/parts/props-hook.d.ts +0 -1
  86. package/dist/server/parts/props-hook.js +0 -8
  87. package/dist/server/parts/runtime-hook.d.ts +0 -2
  88. package/dist/server/parts/runtime-hook.js +0 -28
  89. package/server/parts/apps.ts +0 -59
@@ -1,176 +1,86 @@
1
1
  // This is the nerest development server entrypoint
2
2
  import path from 'path';
3
- import type { ServerResponse } from 'http';
4
-
5
- import { build, createServer } from 'vite';
3
+ import { build, createServer as createViteServer } from 'vite';
6
4
  import type { InlineConfig } from 'vite';
7
5
  import type { RollupWatcher, RollupWatcherEvent } from 'rollup';
8
-
9
- import type { RouteShorthandOptions } from 'fastify';
10
- import fastify from 'fastify';
11
6
  import fastifyStatic from '@fastify/static';
12
- import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
7
+ import fastifyMiddie from '@fastify/middie';
8
+ import { createServer } from './shared.js';
9
+ import {
10
+ viteConfigDevelopmentClient,
11
+ viteConfigDevelopmentServer,
12
+ } from '../build/configs/development.js';
13
+ import { loadBuildConfig } from './loaders/build.js';
14
+ import { loadApps } from './loaders/apps.js';
15
+ import { loadProject } from './loaders/project.js';
16
+
17
+ export async function runDevelopmentServer(port: number) {
18
+ const root = process.cwd();
13
19
 
14
- import { loadApps } from './parts/apps.js';
15
- import { renderApp } from './parts/render.js';
16
- import { renderPreviewPage } from './parts/preview.js';
17
- import { validator } from './parts/validator.js';
18
- import { setupSwagger } from './parts/swagger.js';
19
- import { setupK8SProbes } from './parts/k8s-probes.js';
20
- import { runRuntimeHook } from './hooks/runtime.js';
21
- import { runPropsHook } from './hooks/props.js';
20
+ // Allow overriding STATIC_PATH in development, useful for debugging
21
+ // micro frontend from another device on the same local network
22
+ let staticPath = process.env.STATIC_PATH || `http://127.0.0.1:${port}/`;
23
+ if (!staticPath.endsWith('/')) {
24
+ staticPath += '/';
25
+ }
22
26
 
23
- // eslint-disable-next-line max-statements
24
- export async function runDevelopmentServer() {
25
- const root = process.cwd();
27
+ // Generate vite configuration with nerest/build.json applied
28
+ const buildConfig = await loadBuildConfig(root);
26
29
 
27
- // TODO: move build config into a separate file
28
- // TODO: look at @vitejs/plugin-react (everything seems fine without it though)
29
- // TODO: look at @vitejs/plugin-legacy (needed for browsers without module support)
30
- const config: InlineConfig = {
31
- root,
32
- appType: 'custom',
33
- envPrefix: 'NEREST_',
34
- server: { middlewareMode: true },
35
- build: {
36
- // Manifest is needed to report used assets in SSR handles
37
- manifest: true,
38
- modulePreload: false,
39
- // TODO: watch is only necessary for the client build
40
- watch: {},
41
- rollupOptions: {
42
- input: '/node_modules/@nerest/nerest/client/index.ts',
43
- output: {
44
- dir: 'build',
45
- entryFileNames: `client/assets/[name].js`,
46
- chunkFileNames: `client/assets/[name].js`,
47
- assetFileNames: `client/assets/[name].[ext]`,
48
- },
49
- },
50
- },
51
- // TODO: this doesn't seem to work without the index.html file entry and
52
- // produces warnings in dev mode. look into this maybe
53
- optimizeDeps: {
54
- disabled: true,
55
- },
56
- };
30
+ // Load project meta details
31
+ const project = await loadProject(root);
57
32
 
58
33
  // Build the clientside assets and watch for changes
59
- // TODO: this should probably be moved from here
60
- await startClientBuildWatcher(config);
61
-
62
- // Load app entries following the `apps/{name}/index.tsx` convention
63
- // TODO: remove hardcoded port
64
- const apps = await loadApps(root, 'http://0.0.0.0:3000/');
34
+ await startClientBuildWatcher(
35
+ await viteConfigDevelopmentClient({
36
+ root,
37
+ base: staticPath,
38
+ buildConfig,
39
+ project,
40
+ })
41
+ );
65
42
 
66
43
  // Start vite server that will be rendering SSR components
67
- const viteSsr = await createServer(config);
68
- const app = fastify();
69
-
70
- // Setup schema validation. We have to use our own ajv instance that
71
- // we can use both to validate request bodies and examples against
72
- // app schemas
73
- app.setValidatorCompiler(({ schema }) => validator.compile(schema));
74
-
75
- await setupSwagger(app);
44
+ const viteSsr = await createViteServer(
45
+ await viteConfigDevelopmentServer({
46
+ root,
47
+ base: staticPath,
48
+ buildConfig,
49
+ project,
50
+ })
51
+ );
76
52
 
77
- for (const appEntry of Object.values(apps)) {
78
- const { name, entry, propsHookEntry, examples, schema } = appEntry;
79
-
80
- const routeOptions: RouteShorthandOptions = {};
81
-
82
- // TODO: report error if schema is missing, unless this app is client-only
83
- if (schema) {
84
- routeOptions.schema = {
85
- // Use description as Swagger summary, since summary is visible
86
- // even when the route is collapsed in the UI
87
- summary: schema.description as string,
88
- body: {
89
- ...schema,
90
- // Mix examples into the schema so they become accessible
91
- // in the Swagger UI
92
- examples: Object.values(examples),
93
- },
94
- };
95
- }
96
-
97
- // POST /api/{name} -> render app with request.body as props
98
- app.post(`/api/${name}`, routeOptions, async (request) => {
99
- // ssrLoadModule drives the "hot-reload" logic, and allows
100
- // picking up changes to the source without restarting the server
101
- const ssrComponent = await viteSsr.ssrLoadModule(entry, {
102
- fixStacktrace: true,
103
- });
104
- const props = await runPropsHook(request.body, () =>
105
- viteSsr.ssrLoadModule(propsHookEntry)
106
- );
107
- return renderApp(
108
- {
109
- name,
110
- assets: appEntry.assets,
111
- component: ssrComponent.default,
112
- },
113
- props
114
- );
115
- });
116
-
117
- for (const [exampleName, example] of Object.entries(examples)) {
118
- // Validate example against schema when specified
119
- if (schema && !validator.validate(schema, example)) {
120
- // TODO: use logger and display errors more prominently
121
- console.error(
122
- `Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator.errorsText()}`
123
- );
124
- }
125
-
126
- // GET /api/{name}/examples/{example} -> render a preview page
127
- // with a predefined example body
128
- const exampleRoute = `/api/${name}/examples/${exampleName}`;
129
- app.get(
130
- exampleRoute,
131
- {
132
- schema: {
133
- // Add a clickable link to the example route in route's Swagger
134
- // description so it's easier to navigate to
135
- description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
136
- },
137
- },
138
- async (_, reply) => {
139
- const ssrComponent = await viteSsr.ssrLoadModule(entry, {
140
- fixStacktrace: true,
141
- });
142
- const props = await runPropsHook(example, () =>
143
- viteSsr.ssrLoadModule(propsHookEntry)
144
- );
145
- const { html, assets } = renderApp(
146
- {
147
- name,
148
- assets: appEntry.assets,
149
- component: ssrComponent.default,
150
- },
151
- props
152
- );
153
-
154
- reply.type('text/html');
155
-
156
- return renderPreviewPage(html, assets);
157
- }
158
- );
159
- }
160
- }
53
+ // Load app entries following the `apps/{name}/index.tsx` convention
54
+ const apps = await loadApps(root, staticPath);
161
55
 
162
- // Add graceful shutdown handler to prevent requests errors
163
- await app.register(fastifyGracefulShutdown);
56
+ const app = await createServer({
57
+ root,
58
+ project,
59
+ apps,
60
+ // ssrLoadModule picks up the changes without restarting the server
61
+ loadComponent: async (entry: string) =>
62
+ (
63
+ await viteSsr.ssrLoadModule(`/apps/${entry}/index.tsx`, {
64
+ fixStacktrace: true,
65
+ })
66
+ ).default,
67
+ loadPropsHook: (entry: string) =>
68
+ viteSsr.ssrLoadModule(`/apps/${entry}/props.ts`),
69
+ loadRuntimeHook: () => viteSsr.ssrLoadModule('/nerest/runtime.ts'),
70
+ });
164
71
 
165
- if (process.env.ENABLE_K8S_PROBES) {
166
- await setupK8SProbes(app);
167
- }
72
+ // Register middie to use vite's Connect-style middlewares
73
+ await app.register(fastifyMiddie);
74
+ app.use(viteSsr.middlewares);
168
75
 
169
- // TODO: only do this locally, load from CDN in production
76
+ // @fastify/static is only used locally for development, in production static
77
+ // files are served from STATIC_PATH, which is usually a CDN location
170
78
  await app.register(fastifyStatic, {
171
- root: path.join(root, 'build'),
172
- // TODO: maybe use @fastify/cors instead
173
- setHeaders(res: ServerResponse) {
79
+ root: path.join(root, 'build/client/assets'),
80
+
81
+ // Set CORS headers so the development server assets can be accessed from
82
+ // remote devices, e.g. mobile phones
83
+ setHeaders(res) {
174
84
  res.setHeader('Access-Control-Allow-Origin', '*');
175
85
  res.setHeader('Access-Control-Allow-Methods', 'GET');
176
86
  res.setHeader(
@@ -180,19 +90,12 @@ export async function runDevelopmentServer() {
180
90
  },
181
91
  });
182
92
 
183
- // Execute runtime hook in nerest-runtime.ts if it exists
184
- await runRuntimeHook(app, () => viteSsr.ssrLoadModule('/nerest-runtime.ts'));
185
-
186
- // TODO: remove hardcoded port
187
93
  await app.listen({
188
94
  host: '0.0.0.0',
189
- port: 3000,
95
+ port,
190
96
  });
191
-
192
- console.log('Nerest is listening on 0.0.0.0:3000');
193
97
  }
194
98
 
195
- // TODO: this should probably be moved from here
196
99
  async function startClientBuildWatcher(config: InlineConfig) {
197
100
  const watcher = (await build(config)) as RollupWatcher;
198
101
  return new Promise<void>((resolve) => {
@@ -0,0 +1,31 @@
1
+ import type { FastifyServerOptions } from 'fastify';
2
+
3
+ type LoggerHookModule = {
4
+ logger?: () => FastifyServerOptions['logger'];
5
+ };
6
+
7
+ // Load the runtime hook module and run it if it exists, to receive
8
+ // logger configuration. If the `logger` function does not exist
9
+ // in the module, ignore it and use default configuration.
10
+ export async function runLoggerHook(loader: () => Promise<unknown>) {
11
+ let module: LoggerHookModule | undefined;
12
+
13
+ try {
14
+ module = (await loader()) as LoggerHookModule;
15
+ } catch {}
16
+
17
+ if (typeof module?.logger === 'function') {
18
+ // If module exists and exports a logger function, execute it
19
+ try {
20
+ return module.logger();
21
+ } catch (e) {
22
+ // Allow console.error here, because we can't app.log the error if we
23
+ // can't create the logger in the first place.
24
+ // eslint-disable-next-line no-console
25
+ console.error('Failed to load logger configuration', e);
26
+ process.exit(1);
27
+ }
28
+ }
29
+
30
+ return null;
31
+ }
@@ -1,13 +1,16 @@
1
+ import type { FastifyBaseLogger, FastifyInstance } from 'fastify';
2
+
1
3
  type PropsHookModule = {
2
- default: (props: unknown) => unknown;
4
+ default: (props: unknown, logger: FastifyBaseLogger) => unknown;
3
5
  };
4
6
 
5
7
  // Load the props hook module and run it if it exists, passing down app's
6
- // props object. This hook can be used to modify props before they are passed
7
- // to the root app component.
8
+ // props object and logger. This hook can be used to modify props before
9
+ // they are passed to the root app component.
8
10
  export async function runPropsHook(
9
- props: unknown,
10
- loader: () => Promise<unknown>
11
+ app: FastifyInstance,
12
+ loader: () => Promise<unknown>,
13
+ props: unknown
11
14
  ) {
12
15
  let module: PropsHookModule | undefined;
13
16
 
@@ -17,6 +20,8 @@ export async function runPropsHook(
17
20
 
18
21
  // If module exists and exports a default function, run it to modify props
19
22
  return (
20
- typeof module?.default === 'function' ? module.default(props) : props
23
+ typeof module?.default === 'function'
24
+ ? module.default(props, app.log)
25
+ : props
21
26
  ) as Record<string, unknown>;
22
27
  }
@@ -23,13 +23,13 @@ export async function runRuntimeHook(
23
23
  try {
24
24
  await module.default(app);
25
25
  } catch (e) {
26
- console.error('Failed to execute runtime hook', e);
26
+ app.log.fatal('Failed to execute runtime hook', e);
27
27
  process.exit(1);
28
28
  }
29
29
  } else if (module) {
30
- console.error("Runtime hook found, but doesn't export default function!");
30
+ app.log.fatal("Runtime hook found, but doesn't export default function!");
31
31
  process.exit(1);
32
32
  } else {
33
- console.log('Runtime hook not found, skipping...');
33
+ app.log.info('Runtime hook not found, skipping...');
34
34
  }
35
35
  }
@@ -0,0 +1,58 @@
1
+ import path from 'path';
2
+ import fg from 'fast-glob';
3
+ import type { Manifest as ViteManifest } from 'vite';
4
+ import type { JSONSchema } from '@apidevtools/json-schema-ref-parser';
5
+
6
+ import { loadAppAssets } from './assets.js';
7
+ import { loadAppExamples } from './examples.js';
8
+ import { loadAppSchema } from './schema.js';
9
+ import { loadViteManifest } from './manifest.js';
10
+
11
+ export type AppEntry = {
12
+ name: string;
13
+ root: string;
14
+ entry: string;
15
+ propsHookEntry: string;
16
+ assets: string[];
17
+ examples: Record<string, unknown>;
18
+ schema: JSONSchema | null;
19
+ };
20
+
21
+ // Build the record of the available apps by convention
22
+ // apps -> /apps/{name}/index.tsx
23
+ // examples -> /apps/{name}/examples/{example}.json
24
+ export async function loadApps(root: string, deployedStaticPath: string) {
25
+ const manifest = await loadViteManifest(root);
26
+
27
+ const appBase = path.join(root, 'apps');
28
+ const appPattern = `${fg.convertPathToPattern(appBase)}/*`;
29
+ const appDirs = await fg.glob(appPattern, { onlyDirectories: true });
30
+
31
+ const apps: Array<[name: string, entry: AppEntry]> = [];
32
+ for (const appDir of appDirs) {
33
+ apps.push(await loadApp(appDir, manifest, deployedStaticPath));
34
+ }
35
+
36
+ return Object.fromEntries(apps);
37
+ }
38
+
39
+ async function loadApp(
40
+ appDir: string,
41
+ manifest: ViteManifest,
42
+ deployedStaticPath: string
43
+ ): Promise<[name: string, entry: AppEntry]> {
44
+ // TODO: report problems with loading entries, assets and/or examples
45
+ const name = path.basename(appDir);
46
+ return [
47
+ name,
48
+ {
49
+ name,
50
+ root: appDir,
51
+ entry: path.join(appDir, 'index.tsx'),
52
+ propsHookEntry: path.join(appDir, 'props.ts'),
53
+ assets: loadAppAssets(name, manifest, deployedStaticPath),
54
+ examples: await loadAppExamples(appDir),
55
+ schema: await loadAppSchema(appDir),
56
+ },
57
+ ];
58
+ }
@@ -13,7 +13,35 @@ export function loadAppAssets(
13
13
  manifest['node_modules/@nerest/nerest/client/index.ts'].file;
14
14
 
15
15
  // Each app has its own CSS bundles, if it imports any CSS
16
- const appCss = manifest[`apps/${appName}/index.tsx`].css ?? [];
16
+ const appCss = collectCssUrls(manifest, `apps/${appName}/index.tsx`);
17
17
 
18
- return [clientEntryJs, ...appCss].map((x) => staticPath + x);
18
+ return [clientEntryJs, ...appCss].map((x) => new URL(x, staticPath).href);
19
+ }
20
+
21
+ // Collects all CSS URLs by walking the manifest tree of static imports
22
+ // for a specific entry. These CSS chunks are loaded dynamically by Vite,
23
+ // but we need to inject them into the page statically to prevent a flash
24
+ // of unstyled content
25
+ function collectCssUrls(manifest: Manifest, entryName: string) {
26
+ const cssUrls = new Set<string>();
27
+ const scannedEntries = new Set([entryName]);
28
+ const queue = [entryName];
29
+
30
+ while (queue.length > 0) {
31
+ const entry = queue.shift()!;
32
+ const manifestEntry = manifest[entry];
33
+
34
+ if (manifestEntry) {
35
+ manifestEntry.css?.forEach((url) => cssUrls.add(url));
36
+
37
+ manifestEntry.imports?.forEach((name) => {
38
+ if (!scannedEntries.has(name)) {
39
+ scannedEntries.add(name);
40
+ queue.push(name);
41
+ }
42
+ });
43
+ }
44
+ }
45
+
46
+ return cssUrls;
19
47
  }
@@ -0,0 +1,14 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+
5
+ import type { BuildConfiguration } from '../../schemas/nerest-build.schema.js';
6
+
7
+ // TODO: error handling
8
+ export async function loadBuildConfig(root: string) {
9
+ const configPath = path.join(root, 'nerest/build.json');
10
+ if (existsSync(configPath)) {
11
+ const content = await fs.readFile(configPath, 'utf-8');
12
+ return JSON.parse(content) as BuildConfiguration;
13
+ }
14
+ }
@@ -1,29 +1,21 @@
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
 
5
5
  // Loads and parses the example json files for providing
6
6
  // `/examples/` routes of the dev server
7
7
  export async function loadAppExamples(appRoot: string) {
8
- const examplesRoot = path.join(appRoot, 'examples');
9
-
10
- // Examples are optional and may not exist
11
- if (!existsSync(examplesRoot)) {
12
- return {};
13
- }
14
-
15
- const exampleFiles = (await fs.readdir(examplesRoot, { withFileTypes: true }))
16
- .filter((d) => d.isFile() && d.name.endsWith('.json'))
17
- .map((d) => d.name);
8
+ const exampleBase = path.join(appRoot, 'examples');
9
+ const examplePattern = `${fg.convertPathToPattern(exampleBase)}/*.json`;
10
+ const exampleFiles = await fg.glob(examplePattern, { onlyFiles: true });
18
11
 
19
12
  const examples: Record<string, unknown> = {};
20
13
 
21
14
  // TODO: error handling and reporting
22
- for (const filename of exampleFiles) {
23
- const file = path.join(examplesRoot, filename);
24
- const content = await fs.readFile(file, { encoding: 'utf8' });
15
+ for (const filePath of exampleFiles) {
16
+ const content = await fs.readFile(filePath, 'utf-8');
25
17
  const json = JSON.parse(content);
26
- examples[path.basename(filename, '.json')] = json;
18
+ examples[path.basename(filePath, '.json')] = json;
27
19
  }
28
20
 
29
21
  return examples;
@@ -1,11 +1,30 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs/promises';
3
+ import type { Manifest as ViteManifest } from 'vite';
3
4
 
4
- // Manifest is used to provide assets list for every app
5
+ import type { Project } from './project.js';
6
+ import type { AppEntry } from './apps.js';
7
+
8
+ // Vite manifest is used to provide assets list for every app
5
9
  // for use with SSR
6
- export async function loadAppManifest(root: string) {
10
+ export async function loadViteManifest(root: string): Promise<ViteManifest> {
11
+ // TODO: error handling
12
+ const manifestPath = path.join(
13
+ root,
14
+ 'build/client/assets/.vite/manifest.json'
15
+ );
16
+ const manifestData = await fs.readFile(manifestPath, 'utf-8');
17
+ return JSON.parse(manifestData);
18
+ }
19
+
20
+ // Nerest manifest contains info about the whole project
21
+ // and every app within it
22
+ export async function loadNerestManifest(root: string): Promise<{
23
+ project: Project;
24
+ apps: Record<string, AppEntry>;
25
+ }> {
7
26
  // TODO: error handling
8
- const manifestPath = path.join(root, 'build', 'manifest.json');
9
- const manifestData = await fs.readFile(manifestPath, { encoding: 'utf8' });
27
+ const manifestPath = path.join(root, 'build/nerest-manifest.json');
28
+ const manifestData = await fs.readFile(manifestPath, 'utf-8');
10
29
  return JSON.parse(manifestData);
11
30
  }
@@ -0,0 +1,17 @@
1
+ import path from 'path';
2
+ import { existsSync } from 'fs';
3
+ import fs from 'fs/promises';
4
+
5
+ export type PreviewParts = {
6
+ head: string;
7
+ };
8
+
9
+ export async function loadPreviewParts(root: string): Promise<PreviewParts> {
10
+ let head = '';
11
+ const headPath = path.join(root, 'nerest/preview-head.html');
12
+ if (existsSync(headPath)) {
13
+ head = await fs.readFile(headPath, 'utf-8');
14
+ }
15
+
16
+ return { head };
17
+ }
@@ -0,0 +1,33 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+
4
+ export type Project = {
5
+ name: string;
6
+ description?: string;
7
+ version?: string;
8
+ homepage?: string;
9
+ repository?: string | { url?: string };
10
+ };
11
+
12
+ // Loads project meta information and available apps
13
+ export async function loadProject(root: string) {
14
+ const packageJson = await fs.readFile(
15
+ path.join(root, 'package.json'),
16
+ 'utf-8'
17
+ );
18
+ const {
19
+ name = '',
20
+ description,
21
+ version,
22
+ homepage,
23
+ repository,
24
+ } = JSON.parse(packageJson);
25
+
26
+ if (!name) {
27
+ console.warn(
28
+ '"name" is not set in package.json, this may cause conflicts when hydrating multiple apps from different micro frontends'
29
+ );
30
+ }
31
+
32
+ return { name, description, version, homepage, repository };
33
+ }
@@ -1,18 +1,20 @@
1
1
  import path from 'path';
2
2
  import { existsSync } from 'fs';
3
- import fs from 'fs/promises';
3
+ import RefParser from '@apidevtools/json-schema-ref-parser';
4
4
 
5
5
  // Loads and parses the schema file for a specific app
6
6
  export async function loadAppSchema(appRoot: string) {
7
7
  const schemaPath = path.join(appRoot, 'schema.json');
8
8
 
9
- let schema = null;
10
-
11
- // TODO: error handling and reporting
12
- if (existsSync(schemaPath)) {
13
- const file = await fs.readFile(schemaPath, { encoding: 'utf-8' });
14
- schema = JSON.parse(file);
15
- }
16
-
17
- return schema;
9
+ // We are using dereference to resolve $refs in schema files and
10
+ // to resolve relative dependencies between schemas. The resolved
11
+ // schema will be stringified and saved in the build manifest,
12
+ // so we need it to have no circular references.
13
+ return existsSync(schemaPath)
14
+ ? RefParser.dereference(schemaPath, {
15
+ dereference: {
16
+ circular: false,
17
+ },
18
+ })
19
+ : null;
18
20
  }
@@ -9,22 +9,35 @@ export async function setupK8SProbes(app: FastifyInstance) {
9
9
  // - finishes all current requests
10
10
  // - shuts down the server
11
11
  let isShutdownInProgress = false;
12
- app.gracefulShutdown((code, next) => {
13
- // TODO: replace with nerest logger, when it'll be ready
14
- console.log('Graceful shutdown in process...');
15
- isShutdownInProgress = true;
16
- next();
17
- });
18
12
 
19
- app.get('/livenessProbe', { logLevel: 'silent' }, (req, res) => {
20
- res.status(200).send();
13
+ app.gracefulShutdown(() => {
14
+ app.log.info('Graceful shutdown in process...');
15
+ isShutdownInProgress = true;
21
16
  });
22
17
 
23
- app.get('/readinessProbe', { logLevel: 'silent' }, (req, res) => {
24
- if (isShutdownInProgress) {
25
- res.status(503).send({ status: 'Shutdown in progress' });
26
- } else {
18
+ app.get(
19
+ '/livenessProbe',
20
+ {
21
+ schema: { tags: ['@service'] },
22
+ logLevel: 'silent',
23
+ },
24
+ (req, res) => {
27
25
  res.status(200).send();
28
26
  }
29
- });
27
+ );
28
+
29
+ app.get(
30
+ '/readinessProbe',
31
+ {
32
+ schema: { tags: ['@service'] },
33
+ logLevel: 'silent',
34
+ },
35
+ (req, res) => {
36
+ if (isShutdownInProgress) {
37
+ res.status(503).send({ status: 'Shutdown in progress' });
38
+ } else {
39
+ res.status(200).send();
40
+ }
41
+ }
42
+ );
30
43
  }