@nerest/nerest 0.0.7 → 0.0.9

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 (43) hide show
  1. package/README.md +15 -0
  2. package/bin/build.ts +1 -1
  3. package/bin/index.ts +2 -2
  4. package/bin/watch.ts +1 -1
  5. package/build/index.ts +6 -6
  6. package/client/index.ts +5 -1
  7. package/dist/bin/build.js +3 -7
  8. package/dist/bin/index.js +5 -7
  9. package/dist/bin/watch.js +3 -7
  10. package/dist/build/excludes/index.js +1 -5
  11. package/dist/build/index.js +17 -24
  12. package/dist/client/index.d.ts +1 -0
  13. package/dist/client/index.js +32 -0
  14. package/dist/server/development.js +35 -39
  15. package/dist/server/hooks/props.d.ts +1 -0
  16. package/dist/server/hooks/props.js +12 -0
  17. package/dist/server/hooks/runtime.d.ts +2 -0
  18. package/dist/server/hooks/runtime.js +28 -0
  19. package/dist/server/loaders/assets.js +1 -5
  20. package/dist/server/loaders/examples.js +10 -17
  21. package/dist/server/loaders/manifest.js +5 -12
  22. package/dist/server/loaders/schema.js +7 -14
  23. package/dist/server/parts/apps.d.ts +1 -0
  24. package/dist/server/parts/apps.js +16 -22
  25. package/dist/server/parts/k8s-probes.js +1 -5
  26. package/dist/server/parts/preview.js +1 -5
  27. package/dist/server/parts/props-hook.d.ts +1 -0
  28. package/dist/server/parts/props-hook.js +8 -0
  29. package/dist/server/parts/render.js +6 -13
  30. package/dist/server/parts/runtime-hook.d.ts +1 -1
  31. package/dist/server/parts/runtime-hook.js +1 -5
  32. package/dist/server/parts/swagger.js +8 -15
  33. package/dist/server/parts/validator.d.ts +1 -1
  34. package/dist/server/parts/validator.js +9 -12
  35. package/dist/server/production.d.ts +1 -0
  36. package/dist/server/production.js +93 -0
  37. package/package.json +4 -3
  38. package/server/development.ts +20 -13
  39. package/server/hooks/props.ts +22 -0
  40. package/server/{parts/runtime-hook.ts → hooks/runtime.ts} +1 -1
  41. package/server/parts/apps.ts +6 -4
  42. package/server/parts/validator.ts +5 -2
  43. package/server/production.ts +19 -15
@@ -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,19 @@ 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
+ propsHookEntry: path.join(appRoot, 'props.ts'),
32
+ assets: loadAppAssets(name, manifest, deployedStaticPath),
33
+ examples: await loadAppExamples(appRoot),
34
+ schema: await loadAppSchema(appRoot),
41
35
  },
42
36
  ];
43
37
  }
@@ -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
@@ -0,0 +1 @@
1
+ export declare function runPropsHook(props: unknown, loader: () => Promise<unknown>): Promise<Record<string, unknown>>;
@@ -0,0 +1,8 @@
1
+ export async function runPropsHook(props, loader) {
2
+ let module;
3
+ try {
4
+ module = (await loader());
5
+ }
6
+ catch { }
7
+ return (typeof module?.default === 'function' ? module.default(props) : props);
8
+ }
@@ -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,93 @@
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 './hooks/runtime.js';
12
+ import { runPropsHook } from './hooks/props.js';
13
+ // TODO: refactor to merge the similar parts between production and development server?
14
+ async function runProductionServer() {
15
+ const root = process.cwd();
16
+ // TODO: error handling for file reading
17
+ const apps = JSON.parse(await fs.readFile(path.join(root, 'build/nerest-manifest.json'), {
18
+ encoding: 'utf-8',
19
+ }));
20
+ const components = import.meta.glob('/apps/*/index.tsx', {
21
+ import: 'default',
22
+ eager: true,
23
+ });
24
+ const propsHooks = import.meta.glob('/apps/*/props.ts', {
25
+ eager: true,
26
+ });
27
+ const app = fastify();
28
+ // Setup schema validation. We have to use our own ajv instance that
29
+ // we can use both to validate request bodies and examples against
30
+ // app schemas
31
+ app.setValidatorCompiler(({ schema }) => validator.compile(schema));
32
+ await setupSwagger(app);
33
+ for (const appEntry of Object.values(apps)) {
34
+ const { name, examples, schema, assets } = appEntry;
35
+ const component = components[`/apps/${name}/index.tsx`];
36
+ const propsHook = async () => propsHooks[`/apps/${name}/props.ts`];
37
+ const routeOptions = {};
38
+ // TODO: report error if schema is missing, unless this app is client-only
39
+ // TODO: disallow apps without schemas in production build
40
+ if (schema) {
41
+ routeOptions.schema = {
42
+ // Use description as Swagger summary, since summary is visible
43
+ // even when the route is collapsed in the UI
44
+ summary: schema.description,
45
+ // TODO: do we need to mix in examples like in the development server?
46
+ body: schema,
47
+ };
48
+ }
49
+ // POST /api/{name} -> render app with request.body as props
50
+ app.post(`/api/${name}`, routeOptions, async (request) => {
51
+ const props = await runPropsHook(request.body, propsHook);
52
+ return renderApp({ name, assets, component }, props);
53
+ });
54
+ for (const [exampleName, example] of Object.entries(examples)) {
55
+ // GET /api/{name}/examples/{example} -> render a preview page
56
+ // with a predefined example body
57
+ const exampleRoute = `/api/${name}/examples/${exampleName}`;
58
+ app.get(exampleRoute, {
59
+ schema: {
60
+ // Add a clickable link to the example route in route's Swagger
61
+ // description so it's easier to navigate to
62
+ description: `Open sandbox: [${exampleRoute}](${exampleRoute})`,
63
+ },
64
+ }, async (_, reply) => {
65
+ const props = await runPropsHook(example, propsHook);
66
+ const { html, assets: outAssets } = renderApp({
67
+ name,
68
+ assets,
69
+ component,
70
+ }, props);
71
+ reply.type('text/html');
72
+ return renderPreviewPage(html, outAssets);
73
+ });
74
+ }
75
+ }
76
+ // Add graceful shutdown handler to prevent requests errors
77
+ await app.register(fastifyGracefulShutdown);
78
+ if (process.env.ENABLE_K8S_PROBES) {
79
+ await setupK8SProbes(app);
80
+ }
81
+ // Execute runtime hook in nerest-runtime.ts if it exists
82
+ await runRuntimeHook(app, async () => {
83
+ const glob = import.meta.glob('/nerest-runtime.ts', { eager: true });
84
+ return glob['/nerest-runtime.ts'];
85
+ });
86
+ // TODO: remove hardcoded port
87
+ await app.listen({
88
+ host: '0.0.0.0',
89
+ port: 3000,
90
+ });
91
+ console.log('Nerest is listening on 0.0.0.0:3000');
92
+ }
93
+ 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.9",
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,14 @@ 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 './hooks/runtime.js';
21
+ import { runPropsHook } from './hooks/props.js';
21
22
 
22
23
  // eslint-disable-next-line max-statements
23
24
  export async function runDevelopmentServer() {
@@ -63,7 +64,7 @@ export async function runDevelopmentServer() {
63
64
  const apps = await loadApps(root, 'http://0.0.0.0:3000/');
64
65
 
65
66
  // Start vite server that will be rendering SSR components
66
- const viteSsr = await vite.createServer(config);
67
+ const viteSsr = await createServer(config);
67
68
  const app = fastify();
68
69
 
69
70
  // Setup schema validation. We have to use our own ajv instance that
@@ -74,7 +75,7 @@ export async function runDevelopmentServer() {
74
75
  await setupSwagger(app);
75
76
 
76
77
  for (const appEntry of Object.values(apps)) {
77
- const { name, entry, examples, schema } = appEntry;
78
+ const { name, entry, propsHookEntry, examples, schema } = appEntry;
78
79
 
79
80
  const routeOptions: RouteShorthandOptions = {};
80
81
 
@@ -100,13 +101,16 @@ export async function runDevelopmentServer() {
100
101
  const ssrComponent = await viteSsr.ssrLoadModule(entry, {
101
102
  fixStacktrace: true,
102
103
  });
104
+ const props = await runPropsHook(request.body, () =>
105
+ viteSsr.ssrLoadModule(propsHookEntry)
106
+ );
103
107
  return renderApp(
104
108
  {
105
109
  name,
106
110
  assets: appEntry.assets,
107
111
  component: ssrComponent.default,
108
112
  },
109
- request.body as Record<string, unknown>
113
+ props
110
114
  );
111
115
  });
112
116
 
@@ -135,13 +139,16 @@ export async function runDevelopmentServer() {
135
139
  const ssrComponent = await viteSsr.ssrLoadModule(entry, {
136
140
  fixStacktrace: true,
137
141
  });
142
+ const props = await runPropsHook(example, () =>
143
+ viteSsr.ssrLoadModule(propsHookEntry)
144
+ );
138
145
  const { html, assets } = renderApp(
139
146
  {
140
147
  name,
141
148
  assets: appEntry.assets,
142
149
  component: ssrComponent.default,
143
150
  },
144
- example as Record<string, unknown>
151
+ props
145
152
  );
146
153
 
147
154
  reply.type('text/html');
@@ -187,7 +194,7 @@ export async function runDevelopmentServer() {
187
194
 
188
195
  // TODO: this should probably be moved from here
189
196
  async function startClientBuildWatcher(config: InlineConfig) {
190
- const watcher = (await vite.build(config)) as RollupWatcher;
197
+ const watcher = (await build(config)) as RollupWatcher;
191
198
  return new Promise<void>((resolve) => {
192
199
  // We need to have a built manifest.json to provide assets
193
200
  // links in SSR. We will wait for rollup to report when it
@@ -0,0 +1,22 @@
1
+ type PropsHookModule = {
2
+ default: (props: unknown) => unknown;
3
+ };
4
+
5
+ // Load the props hook module and run it if it exists, passing down app's
6
+ // props object. This hook can be used to modify props before they are passed
7
+ // to the root app component.
8
+ export async function runPropsHook(
9
+ props: unknown,
10
+ loader: () => Promise<unknown>
11
+ ) {
12
+ let module: PropsHookModule | undefined;
13
+
14
+ try {
15
+ module = (await loader()) as PropsHookModule;
16
+ } catch {}
17
+
18
+ // If module exists and exports a default function, run it to modify props
19
+ return (
20
+ typeof module?.default === 'function' ? module.default(props) : props
21
+ ) as Record<string, unknown>;
22
+ }
@@ -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,15 +2,16 @@ 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;
12
12
  root: string;
13
13
  entry: string;
14
+ propsHookEntry: string;
14
15
  assets: string[];
15
16
  examples: Record<string, unknown>;
16
17
  schema: Record<string, unknown> | null;
@@ -49,6 +50,7 @@ async function loadApp(
49
50
  name,
50
51
  root: appRoot,
51
52
  entry: path.join(appRoot, 'index.tsx'),
53
+ propsHookEntry: path.join(appRoot, 'props.ts'),
52
54
  assets: loadAppAssets(name, manifest, deployedStaticPath),
53
55
  examples: await loadAppExamples(appRoot),
54
56
  schema: await loadAppSchema(appRoot),
@@ -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,14 @@ 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 './hooks/runtime.js';
16
+ import { runPropsHook } from './hooks/props.js';
16
17
 
17
18
  // TODO: refactor to merge the similar parts between production and development server?
18
19
  async function runProductionServer() {
@@ -25,12 +26,15 @@ async function runProductionServer() {
25
26
  })
26
27
  ) as Record<string, AppEntry>;
27
28
 
28
- // TODO: fix client-side vite types
29
29
  const components = import.meta.glob('/apps/*/index.tsx', {
30
30
  import: 'default',
31
31
  eager: true,
32
32
  }) as Record<string, React.ComponentType>;
33
33
 
34
+ const propsHooks = import.meta.glob('/apps/*/props.ts', {
35
+ eager: true,
36
+ });
37
+
34
38
  const app = fastify();
35
39
 
36
40
  // Setup schema validation. We have to use our own ajv instance that
@@ -43,6 +47,7 @@ async function runProductionServer() {
43
47
  for (const appEntry of Object.values(apps)) {
44
48
  const { name, examples, schema, assets } = appEntry;
45
49
  const component = components[`/apps/${name}/index.tsx`];
50
+ const propsHook = async () => propsHooks[`/apps/${name}/props.ts`];
46
51
 
47
52
  const routeOptions: RouteShorthandOptions = {};
48
53
 
@@ -59,12 +64,10 @@ async function runProductionServer() {
59
64
  }
60
65
 
61
66
  // POST /api/{name} -> render app with request.body as props
62
- app.post(`/api/${name}`, routeOptions, (request) =>
63
- renderApp(
64
- { name, assets, component },
65
- request.body as Record<string, unknown>
66
- )
67
- );
67
+ app.post(`/api/${name}`, routeOptions, async (request) => {
68
+ const props = await runPropsHook(request.body, propsHook);
69
+ return renderApp({ name, assets, component }, props);
70
+ });
68
71
 
69
72
  for (const [exampleName, example] of Object.entries(examples)) {
70
73
  // GET /api/{name}/examples/{example} -> render a preview page
@@ -80,13 +83,14 @@ async function runProductionServer() {
80
83
  },
81
84
  },
82
85
  async (_, reply) => {
86
+ const props = await runPropsHook(example, propsHook);
83
87
  const { html, assets: outAssets } = renderApp(
84
88
  {
85
89
  name,
86
90
  assets,
87
91
  component,
88
92
  },
89
- example as Record<string, unknown>
93
+ props
90
94
  );
91
95
 
92
96
  reply.type('text/html');