@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
@@ -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,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 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
- 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
- // TODO: move build config into a separate file
20
- // TODO: look at @vitejs/plugin-react (everything seems fine without it though)
21
- // TODO: look at @vitejs/plugin-legacy (needed for browsers without module support)
22
- const config = {
23
- root,
24
- appType: 'custom',
25
- envPrefix: 'NEREST_',
26
- server: { middlewareMode: true },
27
- build: {
28
- // Manifest is needed to report used assets in SSR handles
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
- // TODO: this should probably be moved from here
51
- await startClientBuildWatcher(config);
52
- // Load app entries following the `apps/{name}/index.tsx` convention
53
- // TODO: remove hardcoded port
54
- 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
+ }));
55
30
  // Start vite server that will be rendering SSR components
56
- const viteSsr = await createServer(config);
57
- // Start fastify server that will be processing HTTP requests
58
- const app = fastify({
59
- // Receive logger configuration from `nerest-runtime.logger()` or
60
- // use the default fastify one (`true`)
61
- logger: (await runLoggerHook(() => viteSsr.ssrLoadModule('/nerest-runtime.ts'))) ?? true,
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
- // Setup schema validation. We have to use our own ajv instance that
64
- // we can use both to validate request bodies and examples against
65
- // app schemas
66
- app.setValidatorCompiler(({ schema }) => validator.compile(schema));
67
- await setupSwagger(app);
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
- // 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
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: 3000,
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
- /// <reference types="node" resolution-mode="require"/>
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 { FastifyBaseLogger } from 'fastify';
2
- export declare function runPropsHook(props: unknown, logger: FastifyBaseLogger, 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,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(props, logger, loader) {
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, logger)
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
- 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>`);
@@ -1,10 +1,12 @@
1
- import React from 'react';
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: React.ComponentType;
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
  };