@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
@@ -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('div[data-app-name]')) {
11
- const appName = container.getAttribute('data-app-name');
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,150 +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 fastifyGracefulShutdown from 'fastify-graceful-shutdown';
7
- import { loadApps } from './parts/apps.js';
8
- import { renderApp } from './parts/render.js';
9
- import { renderPreviewPage } from './parts/preview.js';
10
- import { validator } from './parts/validator.js';
11
- import { setupSwagger } from './parts/swagger.js';
12
- import { setupK8SProbes } from './parts/k8s-probes.js';
13
- import { runRuntimeHook } from './hooks/runtime.js';
14
- import { runPropsHook } from './hooks/props.js';
15
- // eslint-disable-next-line max-statements
16
- 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) {
17
12
  const root = process.cwd();
18
- // TODO: move build config into a separate file
19
- // TODO: look at @vitejs/plugin-react (everything seems fine without it though)
20
- // TODO: look at @vitejs/plugin-legacy (needed for browsers without module support)
21
- const config = {
22
- root,
23
- appType: 'custom',
24
- envPrefix: 'NEREST_',
25
- server: { middlewareMode: true },
26
- build: {
27
- // Manifest is needed to report used assets in SSR handles
28
- manifest: true,
29
- modulePreload: false,
30
- // TODO: watch is only necessary for the client build
31
- watch: {},
32
- rollupOptions: {
33
- input: '/node_modules/@nerest/nerest/client/index.ts',
34
- output: {
35
- dir: 'build',
36
- entryFileNames: `client/assets/[name].js`,
37
- chunkFileNames: `client/assets/[name].js`,
38
- assetFileNames: `client/assets/[name].[ext]`,
39
- },
40
- },
41
- },
42
- // TODO: this doesn't seem to work without the index.html file entry and
43
- // produces warnings in dev mode. look into this maybe
44
- optimizeDeps: {
45
- disabled: true,
46
- },
47
- };
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);
48
23
  // Build the clientside assets and watch for changes
49
- // TODO: this should probably be moved from here
50
- await startClientBuildWatcher(config);
51
- // Load app entries following the `apps/{name}/index.tsx` convention
52
- // TODO: remove hardcoded port
53
- const apps = await loadApps(root, 'http://0.0.0.0:3000/');
24
+ await startClientBuildWatcher(await viteConfigDevelopmentClient({
25
+ root,
26
+ base: staticPath,
27
+ buildConfig,
28
+ project,
29
+ }));
54
30
  // Start vite server that will be rendering SSR components
55
- const viteSsr = await createServer(config);
56
- const app = fastify();
57
- // Setup schema validation. We have to use our own ajv instance that
58
- // we can use both to validate request bodies and examples against
59
- // app schemas
60
- app.setValidatorCompiler(({ schema }) => validator.compile(schema));
61
- await setupSwagger(app);
62
- for (const appEntry of Object.values(apps)) {
63
- const { name, entry, propsHookEntry, examples, schema } = appEntry;
64
- const routeOptions = {};
65
- // TODO: report error if schema is missing, unless this app is client-only
66
- if (schema) {
67
- routeOptions.schema = {
68
- // Use description as Swagger summary, since summary is visible
69
- // even when the route is collapsed in the UI
70
- summary: schema.description,
71
- body: {
72
- ...schema,
73
- // Mix examples into the schema so they become accessible
74
- // in the Swagger UI
75
- examples: Object.values(examples),
76
- },
77
- };
78
- }
79
- // POST /api/{name} -> render app with request.body as props
80
- app.post(`/api/${name}`, routeOptions, async (request) => {
81
- // ssrLoadModule drives the "hot-reload" logic, and allows
82
- // picking up changes to the source without restarting the server
83
- const ssrComponent = await viteSsr.ssrLoadModule(entry, {
84
- fixStacktrace: true,
85
- });
86
- const props = await runPropsHook(request.body, () => viteSsr.ssrLoadModule(propsHookEntry));
87
- return renderApp({
88
- name,
89
- assets: appEntry.assets,
90
- component: ssrComponent.default,
91
- }, props);
92
- });
93
- for (const [exampleName, example] of Object.entries(examples)) {
94
- // Validate example against schema when specified
95
- if (schema && !validator.validate(schema, example)) {
96
- // TODO: use logger and display errors more prominently
97
- console.error(`Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator.errorsText()}`);
98
- }
99
- // GET /api/{name}/examples/{example} -> render a preview page
100
- // with a predefined example body
101
- const exampleRoute = `/api/${name}/examples/${exampleName}`;
102
- app.get(exampleRoute, {
103
- schema: {
104
- // Add a clickable link to the example route in route's Swagger
105
- // description so it's easier to navigate to
106
- description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
107
- },
108
- }, async (_, reply) => {
109
- const ssrComponent = await viteSsr.ssrLoadModule(entry, {
110
- fixStacktrace: true,
111
- });
112
- const props = await runPropsHook(example, () => viteSsr.ssrLoadModule(propsHookEntry));
113
- const { html, assets } = renderApp({
114
- name,
115
- assets: appEntry.assets,
116
- component: ssrComponent.default,
117
- }, props);
118
- reply.type('text/html');
119
- return renderPreviewPage(html, assets);
120
- });
121
- }
122
- }
123
- // Add graceful shutdown handler to prevent requests errors
124
- await app.register(fastifyGracefulShutdown);
125
- if (process.env.ENABLE_K8S_PROBES) {
126
- await setupK8SProbes(app);
127
- }
128
- // TODO: only do this locally, load from CDN in production
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'),
49
+ });
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
129
55
  await app.register(fastifyStatic, {
130
- root: path.join(root, 'build'),
131
- // TODO: maybe use @fastify/cors instead
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
132
59
  setHeaders(res) {
133
60
  res.setHeader('Access-Control-Allow-Origin', '*');
134
61
  res.setHeader('Access-Control-Allow-Methods', 'GET');
135
62
  res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, content-type, Authorization');
136
63
  },
137
64
  });
138
- // Execute runtime hook in nerest-runtime.ts if it exists
139
- await runRuntimeHook(app, () => viteSsr.ssrLoadModule('/nerest-runtime.ts'));
140
- // TODO: remove hardcoded port
141
65
  await app.listen({
142
66
  host: '0.0.0.0',
143
- port: 3000,
67
+ port,
144
68
  });
145
- console.log('Nerest is listening on 0.0.0.0:3000');
146
69
  }
147
- // TODO: this should probably be moved from here
148
70
  async function startClientBuildWatcher(config) {
149
71
  const watcher = (await build(config));
150
72
  return new Promise((resolve) => {
@@ -0,0 +1 @@
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>;
@@ -0,0 +1,24 @@
1
+ // Load the runtime hook module and run it if it exists, to receive
2
+ // logger configuration. If the `logger` function does not exist
3
+ // in the module, ignore it and use default configuration.
4
+ export async function runLoggerHook(loader) {
5
+ let module;
6
+ try {
7
+ module = (await loader());
8
+ }
9
+ catch { }
10
+ if (typeof module?.logger === 'function') {
11
+ // If module exists and exports a logger function, execute it
12
+ try {
13
+ return module.logger();
14
+ }
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
19
+ console.error('Failed to load logger configuration', e);
20
+ process.exit(1);
21
+ }
22
+ }
23
+ return null;
24
+ }
@@ -1 +1,2 @@
1
- export declare function runPropsHook(props: unknown, loader: () => Promise<unknown>): Promise<Record<string, unknown>>;
1
+ import type { FastifyInstance } from 'fastify';
2
+ export declare function runPropsHook(app: FastifyInstance, loader: () => Promise<unknown>, props: unknown): Promise<Record<string, unknown>>;
@@ -1,12 +1,14 @@
1
1
  // Load the props hook module and run it if it exists, passing down app's
2
- // props object. This hook can be used to modify props before they are passed
3
- // to the root app component.
4
- export async function runPropsHook(props, loader) {
2
+ // props object and logger. This hook can be used to modify props before
3
+ // they are passed to the root app component.
4
+ export async function runPropsHook(app, loader, props) {
5
5
  let module;
6
6
  try {
7
7
  module = (await loader());
8
8
  }
9
9
  catch { }
10
10
  // If module exists and exports a default function, run it to modify props
11
- return (typeof module?.default === 'function' ? module.default(props) : props);
11
+ return (typeof module?.default === 'function'
12
+ ? module.default(props, app.log)
13
+ : props);
12
14
  }
@@ -14,15 +14,15 @@ export async function runRuntimeHook(app, loader) {
14
14
  await module.default(app);
15
15
  }
16
16
  catch (e) {
17
- console.error('Failed to execute runtime hook', e);
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
- console.error("Runtime hook found, but doesn't export default function!");
22
+ app.log.fatal("Runtime hook found, but doesn't export default function!");
23
23
  process.exit(1);
24
24
  }
25
25
  else {
26
- console.log('Runtime hook not found, skipping...');
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: Record<string, unknown> | null;
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[`apps/${appName}/index.tsx`].css ?? [];
9
- return [clientEntryJs, ...appCss].map((x) => staticPath + 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,2 @@
1
+ import type { BuildConfiguration } from '../../schemas/nerest-build.schema.js';
2
+ export declare function loadBuildConfig(root: string): Promise<BuildConfiguration | undefined>;
@@ -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 examplesRoot = path.join(appRoot, 'examples');
8
- // Examples are optional and may not exist
9
- if (!existsSync(examplesRoot)) {
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 filename of exampleFiles) {
18
- const file = path.join(examplesRoot, filename);
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(filename, '.json')] = json;
15
+ examples[path.basename(filePath, '.json')] = json;
22
16
  }
23
17
  return examples;
24
18
  }
@@ -1 +1,8 @@
1
- export declare function loadAppManifest(root: string): Promise<any>;
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
- // Manifest is used to provide assets list for every app
3
+ // Vite manifest is used to provide assets list for every app
4
4
  // for use with SSR
5
- export async function loadAppManifest(root) {
5
+ export async function loadViteManifest(root) {
6
6
  // TODO: error handling
7
- const manifestPath = path.join(root, 'build', 'manifest.json');
8
- const manifestData = await fs.readFile(manifestPath, { encoding: 'utf8' });
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,4 @@
1
+ export type PreviewParts = {
2
+ head: string;
3
+ };
4
+ export declare function loadPreviewParts(root: string): Promise<PreviewParts>;
@@ -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
- export declare function loadAppSchema(appRoot: string): Promise<any>;
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 fs from 'fs/promises';
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
- let schema = null;
8
- // TODO: error handling and reporting
9
- if (existsSync(schemaPath)) {
10
- const file = await fs.readFile(schemaPath, { encoding: 'utf-8' });
11
- schema = JSON.parse(file);
12
- }
13
- return schema;
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((code, next) => {
11
- // TODO: replace with nerest logger, when it'll be ready
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', { logLevel: 'silent' }, (req, res) => {
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', { logLevel: 'silent' }, (req, res) => {
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
- export declare function renderPreviewPage(html: string, assets: string[]): string;
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>`);