@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.
Files changed (89) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +130 -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 +51 -134
  28. package/dist/server/hooks/logger.d.ts +1 -2
  29. package/dist/server/hooks/logger.js +3 -0
  30. package/dist/server/hooks/props.d.ts +2 -2
  31. package/dist/server/hooks/props.js +2 -2
  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 -83
  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 +45 -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 -172
  65. package/server/hooks/logger.ts +3 -0
  66. package/server/hooks/props.ts +5 -5
  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 -110
  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,186 +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';
22
- import { runLoggerHook } from './hooks/logger.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
+ }
23
26
 
24
- // eslint-disable-next-line max-statements
25
- export async function runDevelopmentServer() {
26
- const root = process.cwd();
27
+ // Generate vite configuration with nerest/build.json applied
28
+ const buildConfig = await loadBuildConfig(root);
27
29
 
28
- // TODO: move build config into a separate file
29
- // TODO: look at @vitejs/plugin-react (everything seems fine without it though)
30
- // TODO: look at @vitejs/plugin-legacy (needed for browsers without module support)
31
- const config: InlineConfig = {
32
- root,
33
- appType: 'custom',
34
- envPrefix: 'NEREST_',
35
- server: { middlewareMode: true },
36
- build: {
37
- // Manifest is needed to report used assets in SSR handles
38
- manifest: true,
39
- modulePreload: false,
40
- // TODO: watch is only necessary for the client build
41
- watch: {},
42
- rollupOptions: {
43
- input: '/node_modules/@nerest/nerest/client/index.ts',
44
- output: {
45
- dir: 'build',
46
- entryFileNames: `client/assets/[name].js`,
47
- chunkFileNames: `client/assets/[name].js`,
48
- assetFileNames: `client/assets/[name].[ext]`,
49
- },
50
- },
51
- },
52
- // TODO: this doesn't seem to work without the index.html file entry and
53
- // produces warnings in dev mode. look into this maybe
54
- optimizeDeps: {
55
- disabled: true,
56
- },
57
- };
30
+ // Load project meta details
31
+ const project = await loadProject(root);
58
32
 
59
33
  // Build the clientside assets and watch for changes
60
- // TODO: this should probably be moved from here
61
- await startClientBuildWatcher(config);
62
-
63
- // Load app entries following the `apps/{name}/index.tsx` convention
64
- // TODO: remove hardcoded port
65
- 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
+ );
66
42
 
67
43
  // Start vite server that will be rendering SSR components
68
- const viteSsr = await createServer(config);
69
-
70
- // Start fastify server that will be processing HTTP requests
71
- const app = fastify({
72
- // Receive logger configuration from `nerest-runtime.logger()` or
73
- // use the default fastify one (`true`)
74
- logger:
75
- (await runLoggerHook(() =>
76
- viteSsr.ssrLoadModule('/nerest-runtime.ts')
77
- )) ?? true,
78
- });
79
-
80
- // Setup schema validation. We have to use our own ajv instance that
81
- // we can use both to validate request bodies and examples against
82
- // app schemas
83
- app.setValidatorCompiler(({ schema }) => validator.compile(schema));
84
-
85
- await setupSwagger(app);
86
-
87
- for (const appEntry of Object.values(apps)) {
88
- const { name, entry, propsHookEntry, examples, schema } = appEntry;
89
-
90
- const routeOptions: RouteShorthandOptions = {};
44
+ const viteSsr = await createViteServer(
45
+ await viteConfigDevelopmentServer({
46
+ root,
47
+ base: staticPath,
48
+ buildConfig,
49
+ project,
50
+ })
51
+ );
91
52
 
92
- // TODO: report error if schema is missing, unless this app is client-only
93
- if (schema) {
94
- routeOptions.schema = {
95
- // Use description as Swagger summary, since summary is visible
96
- // even when the route is collapsed in the UI
97
- summary: schema.description as string,
98
- body: {
99
- ...schema,
100
- // Mix examples into the schema so they become accessible
101
- // in the Swagger UI
102
- examples: Object.values(examples),
103
- },
104
- };
105
- }
106
-
107
- // POST /api/{name} -> render app with request.body as props
108
- app.post(`/api/${name}`, routeOptions, async (request) => {
109
- // ssrLoadModule drives the "hot-reload" logic, and allows
110
- // picking up changes to the source without restarting the server
111
- const ssrComponent = await viteSsr.ssrLoadModule(entry, {
112
- fixStacktrace: true,
113
- });
114
- const props = await runPropsHook(request.body, request.log, () =>
115
- viteSsr.ssrLoadModule(propsHookEntry)
116
- );
117
- return renderApp(
118
- {
119
- name,
120
- assets: appEntry.assets,
121
- component: ssrComponent.default,
122
- },
123
- props
124
- );
125
- });
126
-
127
- for (const [exampleName, example] of Object.entries(examples)) {
128
- // Validate example against schema when specified
129
- if (schema && !validator.validate(schema, example)) {
130
- // TODO: use logger and display errors more prominently
131
- console.error(
132
- `Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator.errorsText()}`
133
- );
134
- }
135
-
136
- // GET /api/{name}/examples/{example} -> render a preview page
137
- // with a predefined example body
138
- const exampleRoute = `/api/${name}/examples/${exampleName}`;
139
- app.get(
140
- exampleRoute,
141
- {
142
- schema: {
143
- // Add a clickable link to the example route in route's Swagger
144
- // description so it's easier to navigate to
145
- description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
146
- },
147
- },
148
- async (request, reply) => {
149
- const ssrComponent = await viteSsr.ssrLoadModule(entry, {
150
- fixStacktrace: true,
151
- });
152
- const props = await runPropsHook(example, request.log, () =>
153
- viteSsr.ssrLoadModule(propsHookEntry)
154
- );
155
- const { html, assets } = renderApp(
156
- {
157
- name,
158
- assets: appEntry.assets,
159
- component: ssrComponent.default,
160
- },
161
- props
162
- );
163
-
164
- reply.type('text/html');
165
-
166
- return renderPreviewPage(html, assets);
167
- }
168
- );
169
- }
170
- }
53
+ // Load app entries following the `apps/{name}/index.tsx` convention
54
+ const apps = await loadApps(root, staticPath);
171
55
 
172
- // Add graceful shutdown handler to prevent requests errors
173
- 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
+ });
174
71
 
175
- if (process.env.ENABLE_K8S_PROBES) {
176
- await setupK8SProbes(app);
177
- }
72
+ // Register middie to use vite's Connect-style middlewares
73
+ await app.register(fastifyMiddie);
74
+ app.use(viteSsr.middlewares);
178
75
 
179
- // 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
180
78
  await app.register(fastifyStatic, {
181
- root: path.join(root, 'build'),
182
- // TODO: maybe use @fastify/cors instead
183
- 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) {
184
84
  res.setHeader('Access-Control-Allow-Origin', '*');
185
85
  res.setHeader('Access-Control-Allow-Methods', 'GET');
186
86
  res.setHeader(
@@ -190,17 +90,12 @@ export async function runDevelopmentServer() {
190
90
  },
191
91
  });
192
92
 
193
- // Execute runtime hook in nerest-runtime.ts if it exists
194
- await runRuntimeHook(app, () => viteSsr.ssrLoadModule('/nerest-runtime.ts'));
195
-
196
- // TODO: remove hardcoded port
197
93
  await app.listen({
198
94
  host: '0.0.0.0',
199
- port: 3000,
95
+ port,
200
96
  });
201
97
  }
202
98
 
203
- // TODO: this should probably be moved from here
204
99
  async function startClientBuildWatcher(config: InlineConfig) {
205
100
  const watcher = (await build(config)) as RollupWatcher;
206
101
  return new Promise<void>((resolve) => {
@@ -19,6 +19,9 @@ export async function runLoggerHook(loader: () => Promise<unknown>) {
19
19
  try {
20
20
  return module.logger();
21
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
22
25
  console.error('Failed to load logger configuration', e);
23
26
  process.exit(1);
24
27
  }
@@ -1,4 +1,4 @@
1
- import type { FastifyBaseLogger } from 'fastify';
1
+ import type { FastifyBaseLogger, FastifyInstance } from 'fastify';
2
2
 
3
3
  type PropsHookModule = {
4
4
  default: (props: unknown, logger: FastifyBaseLogger) => unknown;
@@ -8,9 +8,9 @@ type PropsHookModule = {
8
8
  // props object and logger. This hook can be used to modify props before
9
9
  // they are passed to the root app component.
10
10
  export async function runPropsHook(
11
- props: unknown,
12
- logger: FastifyBaseLogger,
13
- loader: () => Promise<unknown>
11
+ app: FastifyInstance,
12
+ loader: () => Promise<unknown>,
13
+ props: unknown
14
14
  ) {
15
15
  let module: PropsHookModule | undefined;
16
16
 
@@ -21,7 +21,7 @@ export async function runPropsHook(
21
21
  // If module exists and exports a default function, run it to modify props
22
22
  return (
23
23
  typeof module?.default === 'function'
24
- ? module.default(props, logger)
24
+ ? module.default(props, app.log)
25
25
  : props
26
26
  ) as Record<string, unknown>;
27
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
  }