@nerest/nerest 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/build.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { buildMicroFrontend } from '../build';
1
+ import { buildMicroFrontend } from '../build/index.js';
2
2
 
3
3
  // Produce the production build of the Nerest micro frontend
4
4
  export async function build() {
package/bin/index.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  // All executions of `nerest <command>` get routed through here
3
3
  import 'dotenv/config';
4
4
 
5
- import { build } from './build';
6
- import { watch } from './watch';
5
+ import { build } from './build.js';
6
+ import { watch } from './watch.js';
7
7
 
8
8
  // TODO: add CLI help and manual, maybe use a CLI framework like oclif
9
9
  async function cliEntry(args: string[]) {
package/bin/watch.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { runDevelopmentServer } from '../server/development';
1
+ import { runDevelopmentServer } from '../server/development.js';
2
2
 
3
3
  // Start dev server in watch mode, that restarts on file change
4
4
  // and rebuilds the client static files
package/build/index.ts CHANGED
@@ -2,13 +2,13 @@ import path from 'path';
2
2
  import fs from 'fs/promises';
3
3
  import { existsSync } from 'fs';
4
4
 
5
- import vite from 'vite';
5
+ import { build } from 'vite';
6
6
  import type { InlineConfig } from 'vite';
7
7
  import { viteExternalsPlugin } from 'vite-plugin-externals';
8
8
 
9
- import { loadApps } from '../server/parts/apps';
10
- import type { BuildConfiguration } from '../schemas/nerest-build.schema';
11
- import { excludes } from './excludes';
9
+ import { loadApps } from '../server/parts/apps.js';
10
+ import type { BuildConfiguration } from '../schemas/nerest-build.schema.js';
11
+ import { excludes } from './excludes/index.js';
12
12
 
13
13
  export async function buildMicroFrontend() {
14
14
  const root = process.cwd();
@@ -57,7 +57,7 @@ export async function buildMicroFrontend() {
57
57
  };
58
58
 
59
59
  console.log('Producing production client build...');
60
- await vite.build(clientConfig);
60
+ await build(clientConfig);
61
61
 
62
62
  console.log('Producing Nerest manifest file...');
63
63
  await buildAppsManifest(root, staticPath);
@@ -83,7 +83,7 @@ export async function buildMicroFrontend() {
83
83
  };
84
84
 
85
85
  console.log('Producing production server build...');
86
- await vite.build(serverConfig);
86
+ await build(serverConfig);
87
87
  }
88
88
 
89
89
  async function buildAppsManifest(root: string, staticPath: string) {
package/client/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Shared client entrypoint that bootstraps all of the apps supplied
2
2
  // by current microfrontend
3
3
  import React from 'react';
4
+ import type { ComponentType } from 'react';
4
5
  import ReactDOM from 'react-dom/client';
5
6
 
6
7
  // Since this is a shared entrypoint, it dynamically imports all of the
@@ -28,7 +29,10 @@ async function runHydration() {
28
29
  // TODO: more robust error handling and error logging
29
30
  const props = JSON.parse(propsContainer?.textContent ?? '{}');
30
31
 
31
- const reactElement = React.createElement(await appModuleLoader(), props);
32
+ const reactElement = React.createElement(
33
+ (await appModuleLoader()) as ComponentType,
34
+ props
35
+ );
32
36
  ReactDOM.hydrateRoot(container, reactElement);
33
37
  }
34
38
  }
package/dist/bin/build.js CHANGED
@@ -1,11 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.build = void 0;
4
- const build_1 = require("../build");
1
+ import { buildMicroFrontend } from '../build/index.js';
5
2
  // Produce the production build of the Nerest micro frontend
6
- async function build() {
3
+ export async function build() {
7
4
  console.log('Nerest is preparing production build...');
8
- await (0, build_1.buildMicroFrontend)();
5
+ await buildMicroFrontend();
9
6
  console.log('Nerest build is finished');
10
7
  }
11
- exports.build = build;
package/dist/bin/index.js CHANGED
@@ -1,17 +1,15 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
2
  // All executions of `nerest <command>` get routed through here
5
- require("dotenv/config");
6
- const build_1 = require("./build");
7
- const watch_1 = require("./watch");
3
+ import 'dotenv/config';
4
+ import { build } from './build.js';
5
+ import { watch } from './watch.js';
8
6
  // TODO: add CLI help and manual, maybe use a CLI framework like oclif
9
7
  async function cliEntry(args) {
10
8
  if (args[0] === 'build') {
11
- await (0, build_1.build)();
9
+ await build();
12
10
  }
13
11
  else if (args[0] === 'watch') {
14
- await (0, watch_1.watch)();
12
+ await watch();
15
13
  }
16
14
  }
17
15
  // [<path to node>, <path to nerest binary>, ...args]
package/dist/bin/watch.js CHANGED
@@ -1,12 +1,8 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.watch = void 0;
4
- const development_1 = require("../server/development");
1
+ import { runDevelopmentServer } from '../server/development.js';
5
2
  // Start dev server in watch mode, that restarts on file change
6
3
  // and rebuilds the client static files
7
- async function watch() {
4
+ export async function watch() {
8
5
  // TODO: will be replaced with nerest logger
9
6
  console.log('Starting Nerest watch...');
10
- await (0, development_1.runDevelopmentServer)();
7
+ await runDevelopmentServer();
11
8
  }
12
- exports.watch = watch;
@@ -1,11 +1,8 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.excludes = void 0;
4
1
  /**
5
2
  * Maps list of imports into a list of rollup aliases that resolve
6
3
  * into an empty module.
7
4
  */
8
- function excludes(list) {
5
+ export function excludes(list) {
9
6
  return list?.map((exclude) => ({
10
7
  // Excluding '@some/package' should exclude both '@some/package' and
11
8
  // '@some/package/...` imports
@@ -13,7 +10,6 @@ function excludes(list) {
13
10
  replacement: '@nerest/nerest/build/excludes/empty-module',
14
11
  }));
15
12
  }
16
- exports.excludes = excludes;
17
13
  /**
18
14
  * Escapes string to use inside of a regular expression.
19
15
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
@@ -1,17 +1,11 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.buildMicroFrontend = void 0;
7
- const path_1 = __importDefault(require("path"));
8
- const promises_1 = __importDefault(require("fs/promises"));
9
- const fs_1 = require("fs");
10
- const vite_1 = __importDefault(require("vite"));
11
- const vite_plugin_externals_1 = require("vite-plugin-externals");
12
- const apps_1 = require("../server/parts/apps");
13
- const excludes_1 = require("./excludes");
14
- async function buildMicroFrontend() {
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ import { build } from 'vite';
5
+ import { viteExternalsPlugin } from 'vite-plugin-externals';
6
+ import { loadApps } from '../server/parts/apps.js';
7
+ import { excludes } from './excludes/index.js';
8
+ export async function buildMicroFrontend() {
15
9
  const root = process.cwd();
16
10
  const staticPath = process.env.NEREST_STATIC_PATH;
17
11
  // TODO: The path where the client files are deployed is built-in during
@@ -44,15 +38,15 @@ async function buildMicroFrontend() {
44
38
  },
45
39
  resolve: {
46
40
  // excludes - map buildConfig.excludes packages to an empty module
47
- alias: (0, excludes_1.excludes)(buildConfig?.excludes),
41
+ alias: excludes(buildConfig?.excludes),
48
42
  },
49
43
  plugins: [
50
44
  // externals - map buildConfig.externals packages to a global variable on window
51
- (0, vite_plugin_externals_1.viteExternalsPlugin)(buildConfig?.externals, { useWindow: false }),
45
+ viteExternalsPlugin(buildConfig?.externals, { useWindow: false }),
52
46
  ],
53
47
  };
54
48
  console.log('Producing production client build...');
55
- await vite_1.default.build(clientConfig);
49
+ await build(clientConfig);
56
50
  console.log('Producing Nerest manifest file...');
57
51
  await buildAppsManifest(root, staticPath);
58
52
  // Build server using the client manifest
@@ -75,18 +69,17 @@ async function buildMicroFrontend() {
75
69
  },
76
70
  };
77
71
  console.log('Producing production server build...');
78
- await vite_1.default.build(serverConfig);
72
+ await build(serverConfig);
79
73
  }
80
- exports.buildMicroFrontend = buildMicroFrontend;
81
74
  async function buildAppsManifest(root, staticPath) {
82
- const apps = await (0, apps_1.loadApps)(root, staticPath);
83
- await promises_1.default.writeFile(path_1.default.join(root, 'build/nerest-manifest.json'), JSON.stringify(apps), { encoding: 'utf-8' });
75
+ const apps = await loadApps(root, staticPath);
76
+ await fs.writeFile(path.join(root, 'build/nerest-manifest.json'), JSON.stringify(apps), { encoding: 'utf-8' });
84
77
  }
85
78
  // TODO: error handling
86
79
  async function readBuildConfig(root) {
87
- const configPath = path_1.default.join(root, 'nerest-build.json');
88
- if ((0, fs_1.existsSync)(configPath)) {
89
- const content = await promises_1.default.readFile(configPath, { encoding: 'utf-8' });
80
+ const configPath = path.join(root, 'nerest-build.json');
81
+ if (existsSync(configPath)) {
82
+ const content = await fs.readFile(configPath, { encoding: 'utf-8' });
90
83
  return JSON.parse(content);
91
84
  }
92
85
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ // Shared client entrypoint that bootstraps all of the apps supplied
2
+ // by current microfrontend
3
+ import React from 'react';
4
+ import ReactDOM from 'react-dom/client';
5
+ // Since this is a shared entrypoint, it dynamically imports all of the
6
+ // available apps. They will be built as separate chunks and only loaded
7
+ // if needed.
8
+ const modules = import.meta.glob('/apps/*/index.tsx', { import: 'default' });
9
+ async function runHydration() {
10
+ for (const container of document.querySelectorAll('div[data-app-name]')) {
11
+ const appName = container.getAttribute('data-app-name');
12
+ const appModuleLoader = modules[`/apps/${appName}/index.tsx`];
13
+ if (!appModuleLoader || container.hasAttribute('data-app-hydrated')) {
14
+ continue;
15
+ }
16
+ // Mark container as hydrated, in case there are multiple instances
17
+ // of the same microfrontend script on the page
18
+ container.setAttribute('data-app-hydrated', 'true');
19
+ const appId = container.getAttribute('data-app-id');
20
+ const propsContainer = document.querySelector(`script[data-app-id="${appId}"]`);
21
+ // TODO: more robust error handling and error logging
22
+ const props = JSON.parse(propsContainer?.textContent ?? '{}');
23
+ const reactElement = React.createElement((await appModuleLoader()), props);
24
+ ReactDOM.hydrateRoot(container, reactElement);
25
+ }
26
+ }
27
+ if (document.readyState !== 'complete') {
28
+ document.addEventListener('DOMContentLoaded', runHydration);
29
+ }
30
+ else {
31
+ runHydration();
32
+ }
@@ -1,24 +1,18 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.runDevelopmentServer = void 0;
7
1
  // This is the nerest development server entrypoint
8
- const path_1 = __importDefault(require("path"));
9
- const vite_1 = __importDefault(require("vite"));
10
- const fastify_1 = __importDefault(require("fastify"));
11
- const static_1 = __importDefault(require("@fastify/static"));
12
- const fastify_graceful_shutdown_1 = __importDefault(require("fastify-graceful-shutdown"));
13
- const apps_1 = require("./parts/apps");
14
- const render_1 = require("./parts/render");
15
- const preview_1 = require("./parts/preview");
16
- const validator_1 = require("./parts/validator");
17
- const swagger_1 = require("./parts/swagger");
18
- const k8s_probes_1 = require("./parts/k8s-probes");
19
- const runtime_hook_1 = require("./parts/runtime-hook");
2
+ import path from 'path';
3
+ import { build, createServer } from 'vite';
4
+ import fastify from 'fastify';
5
+ 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 './parts/runtime-hook.js';
20
14
  // eslint-disable-next-line max-statements
21
- async function runDevelopmentServer() {
15
+ export async function runDevelopmentServer() {
22
16
  const root = process.cwd();
23
17
  // TODO: move build config into a separate file
24
18
  // TODO: look at @vitejs/plugin-react (everything seems fine without it though)
@@ -55,15 +49,15 @@ async function runDevelopmentServer() {
55
49
  await startClientBuildWatcher(config);
56
50
  // Load app entries following the `apps/{name}/index.tsx` convention
57
51
  // TODO: remove hardcoded port
58
- const apps = await (0, apps_1.loadApps)(root, 'http://0.0.0.0:3000/');
52
+ const apps = await loadApps(root, 'http://0.0.0.0:3000/');
59
53
  // Start vite server that will be rendering SSR components
60
- const viteSsr = await vite_1.default.createServer(config);
61
- const app = (0, fastify_1.default)();
54
+ const viteSsr = await createServer(config);
55
+ const app = fastify();
62
56
  // Setup schema validation. We have to use our own ajv instance that
63
57
  // we can use both to validate request bodies and examples against
64
58
  // app schemas
65
- app.setValidatorCompiler(({ schema }) => validator_1.validator.compile(schema));
66
- await (0, swagger_1.setupSwagger)(app);
59
+ app.setValidatorCompiler(({ schema }) => validator.compile(schema));
60
+ await setupSwagger(app);
67
61
  for (const appEntry of Object.values(apps)) {
68
62
  const { name, entry, examples, schema } = appEntry;
69
63
  const routeOptions = {};
@@ -88,7 +82,7 @@ async function runDevelopmentServer() {
88
82
  const ssrComponent = await viteSsr.ssrLoadModule(entry, {
89
83
  fixStacktrace: true,
90
84
  });
91
- return (0, render_1.renderApp)({
85
+ return renderApp({
92
86
  name,
93
87
  assets: appEntry.assets,
94
88
  component: ssrComponent.default,
@@ -96,9 +90,9 @@ async function runDevelopmentServer() {
96
90
  });
97
91
  for (const [exampleName, example] of Object.entries(examples)) {
98
92
  // Validate example against schema when specified
99
- if (schema && !validator_1.validator.validate(schema, example)) {
93
+ if (schema && !validator.validate(schema, example)) {
100
94
  // TODO: use logger and display errors more prominently
101
- console.error(`Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator_1.validator.errorsText()}`);
95
+ console.error(`Example "${exampleName}" of app "${name}" does not satisfy schema: ${validator.errorsText()}`);
102
96
  }
103
97
  // GET /api/{name}/examples/{example} -> render a preview page
104
98
  // with a predefined example body
@@ -113,24 +107,24 @@ async function runDevelopmentServer() {
113
107
  const ssrComponent = await viteSsr.ssrLoadModule(entry, {
114
108
  fixStacktrace: true,
115
109
  });
116
- const { html, assets } = (0, render_1.renderApp)({
110
+ const { html, assets } = renderApp({
117
111
  name,
118
112
  assets: appEntry.assets,
119
113
  component: ssrComponent.default,
120
114
  }, example);
121
115
  reply.type('text/html');
122
- return (0, preview_1.renderPreviewPage)(html, assets);
116
+ return renderPreviewPage(html, assets);
123
117
  });
124
118
  }
125
119
  }
126
120
  // Add graceful shutdown handler to prevent requests errors
127
- await app.register(fastify_graceful_shutdown_1.default);
121
+ await app.register(fastifyGracefulShutdown);
128
122
  if (process.env.ENABLE_K8S_PROBES) {
129
- await (0, k8s_probes_1.setupK8SProbes)(app);
123
+ await setupK8SProbes(app);
130
124
  }
131
125
  // TODO: only do this locally, load from CDN in production
132
- await app.register(static_1.default, {
133
- root: path_1.default.join(root, 'build'),
126
+ await app.register(fastifyStatic, {
127
+ root: path.join(root, 'build'),
134
128
  // TODO: maybe use @fastify/cors instead
135
129
  setHeaders(res) {
136
130
  res.setHeader('Access-Control-Allow-Origin', '*');
@@ -139,7 +133,7 @@ async function runDevelopmentServer() {
139
133
  },
140
134
  });
141
135
  // Execute runtime hook in nerest-runtime.ts if it exists
142
- await (0, runtime_hook_1.runRuntimeHook)(app, () => viteSsr.ssrLoadModule('/nerest-runtime.ts'));
136
+ await runRuntimeHook(app, () => viteSsr.ssrLoadModule('/nerest-runtime.ts'));
143
137
  // TODO: remove hardcoded port
144
138
  await app.listen({
145
139
  host: '0.0.0.0',
@@ -147,10 +141,9 @@ async function runDevelopmentServer() {
147
141
  });
148
142
  console.log('Nerest is listening on 0.0.0.0:3000');
149
143
  }
150
- exports.runDevelopmentServer = runDevelopmentServer;
151
144
  // TODO: this should probably be moved from here
152
145
  async function startClientBuildWatcher(config) {
153
- const watcher = (await vite_1.default.build(config));
146
+ const watcher = (await build(config));
154
147
  return new Promise((resolve) => {
155
148
  // We need to have a built manifest.json to provide assets
156
149
  // links in SSR. We will wait for rollup to report when it
@@ -1,8 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loadAppAssets = void 0;
4
1
  // Extracts the list of assets for a given app from the manifest file
5
- function loadAppAssets(appName, manifest, staticPath) {
2
+ export function loadAppAssets(appName, manifest, staticPath) {
6
3
  // TODO: handling errors and potentially missing entries
7
4
  // All apps share the same JS entry that dynamically imports the chunks of the apps
8
5
  // that are used on the page based on their name
@@ -11,4 +8,3 @@ function loadAppAssets(appName, manifest, staticPath) {
11
8
  const appCss = manifest[`apps/${appName}/index.tsx`].css ?? [];
12
9
  return [clientEntryJs, ...appCss].map((x) => staticPath + x);
13
10
  }
14
- exports.loadAppAssets = loadAppAssets;
@@ -1,31 +1,24 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.loadAppExamples = void 0;
7
- const path_1 = __importDefault(require("path"));
8
- const fs_1 = require("fs");
9
- const promises_1 = __importDefault(require("fs/promises"));
1
+ import path from 'path';
2
+ import { existsSync } from 'fs';
3
+ import fs from 'fs/promises';
10
4
  // Loads and parses the example json files for providing
11
5
  // `/examples/` routes of the dev server
12
- async function loadAppExamples(appRoot) {
13
- const examplesRoot = path_1.default.join(appRoot, 'examples');
6
+ export async function loadAppExamples(appRoot) {
7
+ const examplesRoot = path.join(appRoot, 'examples');
14
8
  // Examples are optional and may not exist
15
- if (!(0, fs_1.existsSync)(examplesRoot)) {
9
+ if (!existsSync(examplesRoot)) {
16
10
  return {};
17
11
  }
18
- const exampleFiles = (await promises_1.default.readdir(examplesRoot, { withFileTypes: true }))
12
+ const exampleFiles = (await fs.readdir(examplesRoot, { withFileTypes: true }))
19
13
  .filter((d) => d.isFile() && d.name.endsWith('.json'))
20
14
  .map((d) => d.name);
21
15
  const examples = {};
22
16
  // TODO: error handling and reporting
23
17
  for (const filename of exampleFiles) {
24
- const file = path_1.default.join(examplesRoot, filename);
25
- const content = await promises_1.default.readFile(file, { encoding: 'utf8' });
18
+ const file = path.join(examplesRoot, filename);
19
+ const content = await fs.readFile(file, { encoding: 'utf8' });
26
20
  const json = JSON.parse(content);
27
- examples[path_1.default.basename(filename, '.json')] = json;
21
+ examples[path.basename(filename, '.json')] = json;
28
22
  }
29
23
  return examples;
30
24
  }
31
- exports.loadAppExamples = loadAppExamples;
@@ -1,17 +1,10 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.loadAppManifest = void 0;
7
- const path_1 = __importDefault(require("path"));
8
- const promises_1 = __importDefault(require("fs/promises"));
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
9
3
  // Manifest is used to provide assets list for every app
10
4
  // for use with SSR
11
- async function loadAppManifest(root) {
5
+ export async function loadAppManifest(root) {
12
6
  // TODO: error handling
13
- const manifestPath = path_1.default.join(root, 'build', 'manifest.json');
14
- const manifestData = await promises_1.default.readFile(manifestPath, { encoding: 'utf8' });
7
+ const manifestPath = path.join(root, 'build', 'manifest.json');
8
+ const manifestData = await fs.readFile(manifestPath, { encoding: 'utf8' });
15
9
  return JSON.parse(manifestData);
16
10
  }
17
- exports.loadAppManifest = loadAppManifest;
@@ -1,21 +1,14 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.loadAppSchema = void 0;
7
- const path_1 = __importDefault(require("path"));
8
- const fs_1 = require("fs");
9
- const promises_1 = __importDefault(require("fs/promises"));
1
+ import path from 'path';
2
+ import { existsSync } from 'fs';
3
+ import fs from 'fs/promises';
10
4
  // Loads and parses the schema file for a specific app
11
- async function loadAppSchema(appRoot) {
12
- const schemaPath = path_1.default.join(appRoot, 'schema.json');
5
+ export async function loadAppSchema(appRoot) {
6
+ const schemaPath = path.join(appRoot, 'schema.json');
13
7
  let schema = null;
14
8
  // TODO: error handling and reporting
15
- if ((0, fs_1.existsSync)(schemaPath)) {
16
- const file = await promises_1.default.readFile(schemaPath, { encoding: 'utf-8' });
9
+ if (existsSync(schemaPath)) {
10
+ const file = await fs.readFile(schemaPath, { encoding: 'utf-8' });
17
11
  schema = JSON.parse(file);
18
12
  }
19
13
  return schema;
20
14
  }
21
- exports.loadAppSchema = loadAppSchema;
@@ -1,22 +1,16 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.loadApps = void 0;
7
- const path_1 = __importDefault(require("path"));
8
- const promises_1 = __importDefault(require("fs/promises"));
9
- const assets_1 = require("../loaders/assets");
10
- const examples_1 = require("../loaders/examples");
11
- const schema_1 = require("../loaders/schema");
12
- const manifest_1 = require("../loaders/manifest");
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import { loadAppAssets } from '../loaders/assets.js';
4
+ import { loadAppExamples } from '../loaders/examples.js';
5
+ import { loadAppSchema } from '../loaders/schema.js';
6
+ import { loadAppManifest } from '../loaders/manifest.js';
13
7
  // Build the record of the available apps by convention
14
8
  // apps -> /apps/{name}/index.tsx
15
9
  // examples -> /apps/{name}/examples/{example}.json
16
- async function loadApps(root, deployedStaticPath) {
17
- const appsRoot = path_1.default.join(root, 'apps');
18
- const manifest = await (0, manifest_1.loadAppManifest)(root);
19
- const appsDirs = (await promises_1.default.readdir(appsRoot, { withFileTypes: true }))
10
+ export async function loadApps(root, deployedStaticPath) {
11
+ const appsRoot = path.join(root, 'apps');
12
+ const manifest = await loadAppManifest(root);
13
+ const appsDirs = (await fs.readdir(appsRoot, { withFileTypes: true }))
20
14
  .filter((d) => d.isDirectory())
21
15
  .map((d) => d.name);
22
16
  const apps = [];
@@ -25,19 +19,18 @@ async function loadApps(root, deployedStaticPath) {
25
19
  }
26
20
  return Object.fromEntries(apps);
27
21
  }
28
- exports.loadApps = loadApps;
29
22
  async function loadApp(appsRoot, name, manifest, deployedStaticPath) {
30
23
  // TODO: report problems with loading entries, assets and/or examples
31
- const appRoot = path_1.default.join(appsRoot, name);
24
+ const appRoot = path.join(appsRoot, name);
32
25
  return [
33
26
  name,
34
27
  {
35
28
  name,
36
29
  root: appRoot,
37
- entry: path_1.default.join(appRoot, 'index.tsx'),
38
- assets: (0, assets_1.loadAppAssets)(name, manifest, deployedStaticPath),
39
- examples: await (0, examples_1.loadAppExamples)(appRoot),
40
- schema: await (0, schema_1.loadAppSchema)(appRoot),
30
+ entry: path.join(appRoot, 'index.tsx'),
31
+ assets: loadAppAssets(name, manifest, deployedStaticPath),
32
+ examples: await loadAppExamples(appRoot),
33
+ schema: await loadAppSchema(appRoot),
41
34
  },
42
35
  ];
43
36
  }
@@ -1,8 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupK8SProbes = void 0;
4
1
  // Setup routes for k8s probes to check if application is live
5
- async function setupK8SProbes(app) {
2
+ export async function setupK8SProbes(app) {
6
3
  // Handler for graceful shutdowns
7
4
  // K8s can initiate shutdown at any moment: on pods restart or on deploy.
8
5
  // So, if we receive shutdown request, we:
@@ -28,4 +25,3 @@ async function setupK8SProbes(app) {
28
25
  }
29
26
  });
30
27
  }
31
- exports.setupK8SProbes = setupK8SProbes;
@@ -1,8 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.renderPreviewPage = void 0;
4
1
  // Renders the preview page available by convention at /api/{name}/examples/{example}
5
- function renderPreviewPage(html, assets) {
2
+ export function renderPreviewPage(html, assets) {
6
3
  const { scripts, styles } = mapAssets(assets);
7
4
  return `
8
5
  <html>
@@ -16,7 +13,6 @@ function renderPreviewPage(html, assets) {
16
13
  </html>
17
14
  `;
18
15
  }
19
- exports.renderPreviewPage = renderPreviewPage;
20
16
  function mapAssets(assets) {
21
17
  // TODO: script type="module" is not supported by older browsers
22
18
  // but vite doesn't provide `nomodule` fallback by default
@@ -1,22 +1,15 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.renderApp = void 0;
7
- const react_1 = __importDefault(require("react"));
8
- const server_1 = require("react-dom/server");
9
- const nanoid_1 = require("nanoid");
10
- function renderApp({ name, assets, component }, props = {}) {
1
+ import React from 'react';
2
+ import { renderToString } from 'react-dom/server';
3
+ import { nanoid } from 'nanoid';
4
+ export function renderApp({ name, assets, component }, props = {}) {
11
5
  const html = renderSsrComponent(name, component, props);
12
6
  return { html, assets };
13
7
  }
14
- exports.renderApp = renderApp;
15
8
  function renderSsrComponent(appName, appComponent, props) {
16
- const html = (0, server_1.renderToString)(react_1.default.createElement(appComponent, props));
9
+ const html = renderToString(React.createElement(appComponent, props));
17
10
  // There may be multiple instances of the same app on the page,
18
11
  // so we will use a randomized id to avoid collisions
19
- const appId = (0, nanoid_1.nanoid)();
12
+ const appId = nanoid();
20
13
  // data-app-name and data-app-id are used by client entrypoint to hydrate
21
14
  // apps using correct serialized props
22
15
  const container = `<div data-app-name="${appName}" data-app-id="${appId}">${html}</div>`;
@@ -1,2 +1,2 @@
1
1
  import type { FastifyInstance } from 'fastify';
2
- export declare function runRuntimeHook(app: FastifyInstance, loader: () => Promise<Record<string, any>>): Promise<void>;
2
+ export declare function runRuntimeHook(app: FastifyInstance, loader: () => Promise<unknown>): Promise<void>;
@@ -1,10 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runRuntimeHook = void 0;
4
1
  // Load the runtime hook module and run it if it exists, passing down our
5
2
  // fastify instance. This hook can be used to modify fastify settings, add
6
3
  // plugins or routes on an individual app level.
7
- async function runRuntimeHook(app, loader) {
4
+ export async function runRuntimeHook(app, loader) {
8
5
  let module;
9
6
  try {
10
7
  module = (await loader());
@@ -29,4 +26,3 @@ async function runRuntimeHook(app, loader) {
29
26
  console.log('Runtime hook not found, skipping...');
30
27
  }
31
28
  }
32
- exports.runRuntimeHook = runRuntimeHook;
@@ -1,19 +1,13 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.setupSwagger = void 0;
7
- const path_1 = __importDefault(require("path"));
8
- const fs_1 = __importDefault(require("fs"));
9
- const swagger_1 = __importDefault(require("@fastify/swagger"));
10
- const swagger_ui_1 = __importDefault(require("@fastify/swagger-ui"));
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import fastifySwagger from '@fastify/swagger';
4
+ import fastifySwaggerUi from '@fastify/swagger-ui';
11
5
  // Setup automatic OpenAPI specification compilation and enable
12
6
  // Swagger UI at the `/api` route
13
- async function setupSwagger(app) {
7
+ export async function setupSwagger(app) {
14
8
  let appInfo = {};
15
9
  try {
16
- const packageJson = fs_1.default.readFileSync(path_1.default.join(process.cwd(), 'package.json'), { encoding: 'utf-8' });
10
+ const packageJson = fs.readFileSync(path.join(process.cwd(), 'package.json'), { encoding: 'utf-8' });
17
11
  appInfo = JSON.parse(packageJson);
18
12
  }
19
13
  catch (e) {
@@ -24,7 +18,7 @@ async function setupSwagger(app) {
24
18
  (typeof appInfo.repository === 'string'
25
19
  ? appInfo.repository
26
20
  : appInfo.repository?.url);
27
- await app.register(swagger_1.default, {
21
+ await app.register(fastifySwagger, {
28
22
  openapi: {
29
23
  info: {
30
24
  title: appInfo.name ?? 'Nerest micro frontend',
@@ -43,8 +37,7 @@ async function setupSwagger(app) {
43
37
  },
44
38
  },
45
39
  });
46
- await app.register(swagger_ui_1.default, {
40
+ await app.register(fastifySwaggerUi, {
47
41
  routePrefix: '/api',
48
42
  });
49
43
  }
50
- exports.setupSwagger = setupSwagger;
@@ -1,2 +1,2 @@
1
1
  import Ajv from 'ajv';
2
- export declare const validator: Ajv;
2
+ export declare const validator: Ajv.default;
@@ -1,17 +1,14 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.validator = void 0;
7
- const ajv_1 = __importDefault(require("ajv"));
8
- const fast_uri_1 = __importDefault(require("fast-uri"));
9
- const ajv_formats_1 = __importDefault(require("ajv-formats"));
10
- exports.validator = new ajv_1.default({
1
+ import Ajv from 'ajv';
2
+ import fastUri from 'fast-uri';
3
+ import addFormats from 'ajv-formats';
4
+ // Ajv default export is broken, so we have to specify `.default`
5
+ // manually: https://github.com/ajv-validator/ajv/issues/2132
6
+ // eslint-disable-next-line new-cap
7
+ export const validator = new Ajv.default({
11
8
  coerceTypes: 'array',
12
9
  useDefaults: true,
13
10
  removeAdditional: true,
14
- uriResolver: fast_uri_1.default,
11
+ uriResolver: fastUri,
15
12
  addUsedSchema: false,
16
13
  // Explicitly set allErrors to `false`.
17
14
  // When set to `true`, a DoS attack is possible.
@@ -20,4 +17,4 @@ exports.validator = new ajv_1.default({
20
17
  // Support additional type formats in JSON schema like `date`,
21
18
  // `email`, `url`, etc. Used by default in fastify
22
19
  // https://www.npmjs.com/package/ajv-formats
23
- (0, ajv_formats_1.default)(exports.validator);
20
+ addFormats.default(validator);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ // This is the nerest production server entrypoint
2
+ import path from 'path';
3
+ import fs from 'fs/promises';
4
+ import fastify from 'fastify';
5
+ import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
6
+ import { renderApp } from './parts/render.js';
7
+ import { setupSwagger } from './parts/swagger.js';
8
+ import { validator } from './parts/validator.js';
9
+ import { renderPreviewPage } from './parts/preview.js';
10
+ import { setupK8SProbes } from './parts/k8s-probes.js';
11
+ import { runRuntimeHook } from './parts/runtime-hook.js';
12
+ // TODO: refactor to merge the similar parts between production and development server?
13
+ async function runProductionServer() {
14
+ const root = process.cwd();
15
+ // TODO: error handling for file reading
16
+ const apps = JSON.parse(await fs.readFile(path.join(root, 'build/nerest-manifest.json'), {
17
+ encoding: 'utf-8',
18
+ }));
19
+ // TODO: fix client-side vite types
20
+ const components = import.meta.glob('/apps/*/index.tsx', {
21
+ import: 'default',
22
+ eager: true,
23
+ });
24
+ const app = fastify();
25
+ // Setup schema validation. We have to use our own ajv instance that
26
+ // we can use both to validate request bodies and examples against
27
+ // app schemas
28
+ app.setValidatorCompiler(({ schema }) => validator.compile(schema));
29
+ await setupSwagger(app);
30
+ for (const appEntry of Object.values(apps)) {
31
+ const { name, examples, schema, assets } = appEntry;
32
+ const component = components[`/apps/${name}/index.tsx`];
33
+ const routeOptions = {};
34
+ // TODO: report error if schema is missing, unless this app is client-only
35
+ // TODO: disallow apps without schemas in production build
36
+ if (schema) {
37
+ routeOptions.schema = {
38
+ // Use description as Swagger summary, since summary is visible
39
+ // even when the route is collapsed in the UI
40
+ summary: schema.description,
41
+ // TODO: do we need to mix in examples like in the development server?
42
+ body: schema,
43
+ };
44
+ }
45
+ // POST /api/{name} -> render app with request.body as props
46
+ app.post(`/api/${name}`, routeOptions, (request) => renderApp({ name, assets, component }, request.body));
47
+ for (const [exampleName, example] of Object.entries(examples)) {
48
+ // GET /api/{name}/examples/{example} -> render a preview page
49
+ // with a predefined example body
50
+ const exampleRoute = `/api/${name}/examples/${exampleName}`;
51
+ app.get(exampleRoute, {
52
+ schema: {
53
+ // Add a clickable link to the example route in route's Swagger
54
+ // description so it's easier to navigate to
55
+ description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
56
+ },
57
+ }, async (_, reply) => {
58
+ const { html, assets: outAssets } = renderApp({
59
+ name,
60
+ assets,
61
+ component,
62
+ }, example);
63
+ reply.type('text/html');
64
+ return renderPreviewPage(html, outAssets);
65
+ });
66
+ }
67
+ }
68
+ // Add graceful shutdown handler to prevent requests errors
69
+ await app.register(fastifyGracefulShutdown);
70
+ if (process.env.ENABLE_K8S_PROBES) {
71
+ await setupK8SProbes(app);
72
+ }
73
+ // Execute runtime hook in nerest-runtime.ts if it exists
74
+ await runRuntimeHook(app, async () => {
75
+ const glob = import.meta.glob('/nerest-runtime.ts', { eager: true });
76
+ return glob['/nerest-runtime.ts'];
77
+ });
78
+ // TODO: remove hardcoded port
79
+ await app.listen({
80
+ host: '0.0.0.0',
81
+ port: 3000,
82
+ });
83
+ console.log('Nerest is listening on 0.0.0.0:3000');
84
+ }
85
+ runProductionServer();
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@nerest/nerest",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "React micro frontend framework",
5
5
  "homepage": "https://github.com/nerestjs/nerest#readme",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/nerestjs/nerest.git"
9
9
  },
10
+ "type": "module",
10
11
  "bin": {
11
12
  "nerest": "dist/bin/index.js"
12
13
  },
@@ -61,7 +62,7 @@
61
62
  "fast-uri": "^2.3.0",
62
63
  "fastify": "^4.24.3",
63
64
  "fastify-graceful-shutdown": "^3.5.1",
64
- "nanoid": "^3.3.6",
65
+ "nanoid": "^5.0.2",
65
66
  "vite": "^4.5.0",
66
67
  "vite-plugin-externals": "^0.6.2"
67
68
  },
@@ -69,7 +70,7 @@
69
70
  "@tinkoff/eslint-config": "^1.54.4",
70
71
  "@tinkoff/eslint-config-react": "^1.54.4",
71
72
  "@tinkoff/prettier-config": "^1.52.1",
72
- "@types/react": "^18.2.33",
73
+ "@types/react": "^18.2.34",
73
74
  "@types/react-dom": "^18.2.14",
74
75
  "jest": "^29.7.0",
75
76
  "json-schema-to-typescript": "^13.1.1",
@@ -2,7 +2,7 @@
2
2
  import path from 'path';
3
3
  import type { ServerResponse } from 'http';
4
4
 
5
- import vite from 'vite';
5
+ import { build, createServer } from 'vite';
6
6
  import type { InlineConfig } from 'vite';
7
7
  import type { RollupWatcher, RollupWatcherEvent } from 'rollup';
8
8
 
@@ -11,13 +11,13 @@ import fastify from 'fastify';
11
11
  import fastifyStatic from '@fastify/static';
12
12
  import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
13
13
 
14
- import { loadApps } from './parts/apps';
15
- import { renderApp } from './parts/render';
16
- import { renderPreviewPage } from './parts/preview';
17
- import { validator } from './parts/validator';
18
- import { setupSwagger } from './parts/swagger';
19
- import { setupK8SProbes } from './parts/k8s-probes';
20
- import { runRuntimeHook } from './parts/runtime-hook';
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 './parts/runtime-hook.js';
21
21
 
22
22
  // eslint-disable-next-line max-statements
23
23
  export async function runDevelopmentServer() {
@@ -63,7 +63,7 @@ export async function runDevelopmentServer() {
63
63
  const apps = await loadApps(root, 'http://0.0.0.0:3000/');
64
64
 
65
65
  // Start vite server that will be rendering SSR components
66
- const viteSsr = await vite.createServer(config);
66
+ const viteSsr = await createServer(config);
67
67
  const app = fastify();
68
68
 
69
69
  // Setup schema validation. We have to use our own ajv instance that
@@ -187,7 +187,7 @@ export async function runDevelopmentServer() {
187
187
 
188
188
  // TODO: this should probably be moved from here
189
189
  async function startClientBuildWatcher(config: InlineConfig) {
190
- const watcher = (await vite.build(config)) as RollupWatcher;
190
+ const watcher = (await build(config)) as RollupWatcher;
191
191
  return new Promise<void>((resolve) => {
192
192
  // We need to have a built manifest.json to provide assets
193
193
  // links in SSR. We will wait for rollup to report when it
@@ -2,10 +2,10 @@ import path from 'path';
2
2
  import fs from 'fs/promises';
3
3
  import type { Manifest } from 'vite';
4
4
 
5
- import { loadAppAssets } from '../loaders/assets';
6
- import { loadAppExamples } from '../loaders/examples';
7
- import { loadAppSchema } from '../loaders/schema';
8
- import { loadAppManifest } from '../loaders/manifest';
5
+ import { loadAppAssets } from '../loaders/assets.js';
6
+ import { loadAppExamples } from '../loaders/examples.js';
7
+ import { loadAppSchema } from '../loaders/schema.js';
8
+ import { loadAppManifest } from '../loaders/manifest.js';
9
9
 
10
10
  export type AppEntry = {
11
11
  name: string;
@@ -9,7 +9,7 @@ type RuntimeHookModule = {
9
9
  // plugins or routes on an individual app level.
10
10
  export async function runRuntimeHook(
11
11
  app: FastifyInstance,
12
- loader: () => Promise<Record<string, any>>
12
+ loader: () => Promise<unknown>
13
13
  ) {
14
14
  let module: RuntimeHookModule | undefined;
15
15
 
@@ -2,7 +2,10 @@ import Ajv from 'ajv';
2
2
  import fastUri from 'fast-uri';
3
3
  import addFormats from 'ajv-formats';
4
4
 
5
- export const validator = new Ajv({
5
+ // Ajv default export is broken, so we have to specify `.default`
6
+ // manually: https://github.com/ajv-validator/ajv/issues/2132
7
+ // eslint-disable-next-line new-cap
8
+ export const validator = new Ajv.default({
6
9
  coerceTypes: 'array',
7
10
  useDefaults: true,
8
11
  removeAdditional: true,
@@ -16,4 +19,4 @@ export const validator = new Ajv({
16
19
  // Support additional type formats in JSON schema like `date`,
17
20
  // `email`, `url`, etc. Used by default in fastify
18
21
  // https://www.npmjs.com/package/ajv-formats
19
- addFormats(validator);
22
+ addFormats.default(validator);
@@ -6,13 +6,13 @@ import fastify from 'fastify';
6
6
  import fastifyGracefulShutdown from 'fastify-graceful-shutdown';
7
7
  import type { RouteShorthandOptions } from 'fastify';
8
8
 
9
- import type { AppEntry } from './parts/apps';
10
- import { renderApp } from './parts/render';
11
- import { setupSwagger } from './parts/swagger';
12
- import { validator } from './parts/validator';
13
- import { renderPreviewPage } from './parts/preview';
14
- import { setupK8SProbes } from './parts/k8s-probes';
15
- import { runRuntimeHook } from './parts/runtime-hook';
9
+ import type { AppEntry } from './parts/apps.js';
10
+ import { renderApp } from './parts/render.js';
11
+ import { setupSwagger } from './parts/swagger.js';
12
+ import { validator } from './parts/validator.js';
13
+ import { renderPreviewPage } from './parts/preview.js';
14
+ import { setupK8SProbes } from './parts/k8s-probes.js';
15
+ import { runRuntimeHook } from './parts/runtime-hook.js';
16
16
 
17
17
  // TODO: refactor to merge the similar parts between production and development server?
18
18
  async function runProductionServer() {