@nerest/nerest 0.1.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +129 -40
  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 +46 -43
  62. package/schemas/nerest-build.schema.d.ts +19 -1
  63. package/schemas/nerest-build.schema.json +21 -1
  64. package/server/development.ts +67 -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
@@ -0,0 +1,30 @@
1
+ // https://vite.dev/config/shared-options.html#customlogger
2
+ import { createLogger } from 'vite';
3
+
4
+ const logger = createLogger();
5
+
6
+ const loggerError = logger.error;
7
+ logger.error = (msg, options) => {
8
+ if (typeof msg === 'string' && !isIgnoredError(msg)) {
9
+ loggerError(msg, options);
10
+ }
11
+ };
12
+
13
+ export default logger;
14
+
15
+ // These are errors expected in development that we don't need to log.
16
+ // If the error message includes all markers from a set, it is suppressed
17
+ const ignoredErrors = [
18
+ // Hook files are optional, but in development vite logs an error even though
19
+ // we suppress the exception. Silence these logs manually.
20
+ ['cannot find entry point module', 'props.ts'],
21
+ ['cannot find entry point module', 'runtime.ts'],
22
+ ];
23
+
24
+ function isIgnoredError(msg: string) {
25
+ for (const markers of ignoredErrors) {
26
+ if (markers.every((m) => msg.includes(m))) {
27
+ return true;
28
+ }
29
+ }
30
+ }
package/build/index.ts CHANGED
@@ -1,105 +1,82 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs/promises';
3
- import { existsSync } from 'fs';
4
3
 
5
4
  import { build } from 'vite';
6
- import type { InlineConfig } from 'vite';
7
- import { viteExternalsPlugin } from 'vite-plugin-externals';
8
5
 
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';
6
+ import { loadBuildConfig } from '../server/loaders/build.js';
7
+ import { loadApps } from '../server/loaders/apps.js';
8
+ import type { Project } from '../server/loaders/project.js';
9
+ import { loadProject } from '../server/loaders/project.js';
10
+ import {
11
+ viteConfigProductionClient,
12
+ viteConfigProductionServer,
13
+ } from './configs/production.js';
12
14
 
13
15
  export async function buildMicroFrontend() {
14
16
  const root = process.cwd();
15
- const staticPath = process.env.NEREST_STATIC_PATH;
17
+ const staticPath = prepareStaticPath();
16
18
 
17
- // TODO: The path where the client files are deployed is built-in during
18
- // the initial build, but the client scripts aren't using it, so maybe it should
19
- // be a runtime env variable for the server instead?
20
- if (!staticPath) {
21
- throw new Error(
22
- 'NEREST_STATIC_PATH environment variable is not set but is required for the production build'
23
- );
24
- }
19
+ // Read build customizations from nerest/build.json
20
+ const buildConfig = await loadBuildConfig(root);
25
21
 
26
- const buildConfig = await readBuildConfig(root);
22
+ // Read project meta info from package.json
23
+ const project = await loadProject(root);
27
24
 
28
25
  // Build client
29
- // TODO: extract shared parts between build/index.ts and server/index.ts
30
- // into a shared config
31
- const clientConfig: InlineConfig = {
26
+ const clientViteConfig = await viteConfigProductionClient({
32
27
  root,
33
- appType: 'custom',
34
- envPrefix: 'NEREST_',
35
- build: {
36
- // Manifest is needed to report used assets in SSR handles
37
- manifest: true,
38
- modulePreload: false,
39
- rollupOptions: {
40
- input: '/node_modules/@nerest/nerest/client/index.ts',
41
- output: {
42
- dir: 'build',
43
- entryFileNames: `client/assets/[name].js`,
44
- chunkFileNames: `client/assets/[name].js`,
45
- assetFileNames: `client/assets/[name].[ext]`,
46
- },
47
- },
48
- },
49
- resolve: {
50
- // excludes - map buildConfig.excludes packages to an empty module
51
- alias: excludes(buildConfig?.excludes),
52
- },
53
- plugins: [
54
- // externals - map buildConfig.externals packages to a global variable on window
55
- viteExternalsPlugin(buildConfig?.externals, { useWindow: false }),
56
- ],
57
- };
58
-
28
+ base: staticPath,
29
+ buildConfig,
30
+ project,
31
+ });
59
32
  console.log('Producing production client build...');
60
- await build(clientConfig);
33
+ await build(clientViteConfig);
61
34
 
35
+ // Create nerest-manifest.json that production server reads on startup
62
36
  console.log('Producing Nerest manifest file...');
63
- await buildAppsManifest(root, staticPath);
37
+ await createNerestManifest(root, staticPath, project);
64
38
 
65
- // Build server using the client manifest
66
- const serverConfig: InlineConfig = {
39
+ // Build server
40
+ const serverViteConfig = await viteConfigProductionServer({
67
41
  root,
68
- appType: 'custom',
69
- envPrefix: 'NEREST_',
70
- build: {
71
- emptyOutDir: false,
72
- modulePreload: false,
73
- // This is an important setting for producing a server build
74
- ssr: true,
75
- rollupOptions: {
76
- input: '/node_modules/@nerest/nerest/server/production.ts',
77
- output: {
78
- dir: 'build',
79
- entryFileNames: `server.mjs`,
80
- },
81
- },
82
- },
83
- };
84
-
42
+ base: staticPath,
43
+ buildConfig,
44
+ project,
45
+ });
85
46
  console.log('Producing production server build...');
86
- await build(serverConfig);
47
+ await build(serverViteConfig);
87
48
  }
88
49
 
89
- async function buildAppsManifest(root: string, staticPath: string) {
50
+ async function createNerestManifest(
51
+ root: string,
52
+ staticPath: string,
53
+ project: Project
54
+ ) {
90
55
  const apps = await loadApps(root, staticPath);
91
56
  await fs.writeFile(
92
57
  path.join(root, 'build/nerest-manifest.json'),
93
- JSON.stringify(apps),
94
- { encoding: 'utf-8' }
58
+ JSON.stringify({ project, apps }),
59
+ 'utf-8'
95
60
  );
96
61
  }
97
62
 
98
- // TODO: error handling
99
- async function readBuildConfig(root: string) {
100
- const configPath = path.join(root, 'nerest-build.json');
101
- if (existsSync(configPath)) {
102
- const content = await fs.readFile(configPath, { encoding: 'utf-8' });
103
- return JSON.parse(content) as BuildConfiguration;
63
+ function prepareStaticPath() {
64
+ let staticPath = process.env.STATIC_PATH;
65
+
66
+ // The path where the client files are deployed is embedded
67
+ // during the initial build.
68
+ // TODO: handle error if STATIC_PATH isn't a valid base URL
69
+ if (!staticPath) {
70
+ throw new Error(
71
+ 'STATIC_PATH environment variable is not set but is required for the production build'
72
+ );
104
73
  }
74
+
75
+ // Static path is a directory URI. To be treated as such by node:url,
76
+ // it has to end with a trailing slash
77
+ if (!staticPath.endsWith('/')) {
78
+ staticPath += '/';
79
+ }
80
+
81
+ return staticPath;
105
82
  }
package/client/index.ts CHANGED
@@ -10,8 +10,10 @@ import ReactDOM from 'react-dom/client';
10
10
  const modules = import.meta.glob('/apps/*/index.tsx', { import: 'default' });
11
11
 
12
12
  async function runHydration() {
13
- for (const container of document.querySelectorAll('div[data-app-name]')) {
14
- const appName = container.getAttribute('data-app-name');
13
+ for (const container of document.querySelectorAll(
14
+ `div[data-project-name="${import.meta.env.NEREST_PROJECT_NAME}"]`
15
+ )) {
16
+ const appName = container.getAttribute(`data-app-name`);
15
17
  const appModuleLoader = modules[`/apps/${appName}/index.tsx`];
16
18
 
17
19
  if (!appModuleLoader || container.hasAttribute('data-app-hydrated')) {
@@ -37,8 +39,18 @@ async function runHydration() {
37
39
  }
38
40
  }
39
41
 
42
+ // Apps can only be hydrated after DOM is ready, because we need to query
43
+ // apps' containers and their corresponding scripts.
40
44
  if (document.readyState !== 'complete') {
41
45
  document.addEventListener('DOMContentLoaded', runHydration);
42
46
  } else {
43
47
  runHydration();
44
48
  }
49
+
50
+ // Entries might be self-initializing (e.g. client-only apps) or have other
51
+ // side effects. In that case we have to load them eagerly, so that their
52
+ // initialization code can run, even if there is nothing to hydrate.
53
+ const clientSideEffects: string[] | undefined = JSON.parse(
54
+ import.meta.env.NEREST_CLIENT_SIDE_EFFECTS
55
+ );
56
+ clientSideEffects?.forEach((name) => modules[`/apps/${name}/index.tsx`]?.());
package/dist/bin/index.js CHANGED
@@ -2,12 +2,16 @@
2
2
  // All executions of `nerest <command>` get routed through here
3
3
  import 'dotenv/config';
4
4
  import { build } from './build.js';
5
+ import { typegen } from './typegen.js';
5
6
  import { watch } from './watch.js';
6
7
  // TODO: add CLI help and manual, maybe use a CLI framework like oclif
7
8
  async function cliEntry(args) {
8
9
  if (args[0] === 'build') {
9
10
  await build();
10
11
  }
12
+ else if (args[0] === 'typegen') {
13
+ await typegen(args.slice(1));
14
+ }
11
15
  else if (args[0] === 'watch') {
12
16
  await watch();
13
17
  }
@@ -0,0 +1 @@
1
+ export declare function typegen(globs: string[]): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import fg from 'fast-glob';
4
+ import { compileFromFile } from 'json-schema-to-typescript';
5
+ // Comment at the start of every type definition file, alerting
6
+ // developers to not modify the file by hand
7
+ const bannerComment = `
8
+ /**
9
+ * This file was automatically generated by Nerest.
10
+ * DO NOT MODIFY IT BY HAND. Instead, modify the source schema.json file,
11
+ * and run "nerest typegen" to regenerate this file.
12
+ */`.trim();
13
+ // Generates a TypeScript type definition for each JSON Schema in the given globs
14
+ export async function typegen(globs) {
15
+ // If no globs are provided, default to the schema.json file in each app directory
16
+ if (globs.length === 0) {
17
+ globs = ['apps/*/schema.json'];
18
+ }
19
+ const paths = await fg.glob(globs, { onlyFiles: true });
20
+ console.log(`Found ${paths.length} schemas, generating types...`);
21
+ for (const schemaPath of paths) {
22
+ // Turn each schema.json file into schema.d.ts
23
+ const typePath = schemaPath.replace('.json', '.d.ts');
24
+ const type = await compileFromFile(schemaPath, {
25
+ cwd: path.resolve(path.dirname(schemaPath)),
26
+ bannerComment,
27
+ });
28
+ await fs.writeFile(typePath, type, 'utf-8');
29
+ console.log(`${schemaPath} -> ${typePath}`);
30
+ }
31
+ }
package/dist/bin/watch.js CHANGED
@@ -2,7 +2,6 @@ import { runDevelopmentServer } from '../server/development.js';
2
2
  // Start dev server in watch mode, that restarts on file change
3
3
  // and rebuilds the client static files
4
4
  export async function watch() {
5
- // TODO: will be replaced with nerest logger
6
5
  console.log('Starting Nerest watch...');
7
- await runDevelopmentServer();
6
+ await runDevelopmentServer(process.env.PORT ? Number(process.env.PORT) : 3000);
8
7
  }
@@ -0,0 +1,4 @@
1
+ import type { InlineConfig } from 'vite';
2
+ import type { BuildArgs } from './shared.js';
3
+ export declare function viteConfigDevelopmentClient(args: BuildArgs): Promise<InlineConfig>;
4
+ export declare function viteConfigDevelopmentServer(args: BuildArgs): Promise<InlineConfig>;
@@ -0,0 +1,53 @@
1
+ import { viteConfigShared } from './shared.js';
2
+ import logger from './vite-logger.development.js';
3
+ export async function viteConfigDevelopmentClient(args) {
4
+ return {
5
+ ...(await viteConfigShared(args)),
6
+ build: {
7
+ // Manifest is needed to report used assets in SSR handles
8
+ manifest: true,
9
+ modulePreload: false,
10
+ watch: {},
11
+ rollupOptions: {
12
+ input: '/node_modules/@nerest/nerest/client/index.ts',
13
+ output: {
14
+ dir: 'build/client/assets',
15
+ entryFileNames: `[name].js`,
16
+ chunkFileNames: `[name].js`,
17
+ assetFileNames: `[name].[ext]`,
18
+ },
19
+ },
20
+ },
21
+ customLogger: logger,
22
+ };
23
+ }
24
+ export async function viteConfigDevelopmentServer(args) {
25
+ return {
26
+ ...(await viteConfigShared(args)),
27
+ server: {
28
+ // Middleware lets vite compile on the fly, providing
29
+ // hot reload of certain modules
30
+ middlewareMode: true,
31
+ // Origin to serve imported assets from (like images)
32
+ origin: new URL(args.base).origin,
33
+ // Run HMR WebSocket server on random port to prevent conflicts
34
+ // between multiple projects running simultaneously
35
+ hmr: { port: randomPort() },
36
+ // Allow requests from all hosts in development
37
+ allowedHosts: true,
38
+ },
39
+ // optimizeDeps is only necessary with index.html entrypoint,
40
+ // which we don't have
41
+ optimizeDeps: {
42
+ noDiscovery: true,
43
+ include: [],
44
+ },
45
+ customLogger: logger,
46
+ };
47
+ }
48
+ // Returns a random high-number port to prevent conflicts
49
+ function randomPort() {
50
+ // 49152-65535 is the ephemeral port range
51
+ // https://datatracker.ietf.org/doc/html/rfc6335#section-6
52
+ return 49152 + Math.floor(Math.random() * (65535 - 49152));
53
+ }
@@ -0,0 +1,4 @@
1
+ import type { InlineConfig } from 'vite';
2
+ import type { BuildArgs } from './shared.js';
3
+ export declare function viteConfigProductionClient(args: BuildArgs): Promise<InlineConfig>;
4
+ export declare function viteConfigProductionServer(args: BuildArgs): Promise<InlineConfig>;
@@ -0,0 +1,50 @@
1
+ import { viteExternalsPlugin } from 'vite-plugin-externals';
2
+ import { viteConfigShared } from './shared.js';
3
+ import { excludes } from '../excludes/index.js';
4
+ export async function viteConfigProductionClient(args) {
5
+ return {
6
+ ...(await viteConfigShared(args)),
7
+ build: {
8
+ // Manifest is needed to report used assets in SSR handles
9
+ manifest: true,
10
+ modulePreload: false,
11
+ rollupOptions: {
12
+ input: '/node_modules/@nerest/nerest/client/index.ts',
13
+ output: {
14
+ dir: 'build/client/assets',
15
+ entryFileNames: `[name].js`,
16
+ chunkFileNames: `[name].js`,
17
+ assetFileNames: `[name].[ext]`,
18
+ },
19
+ },
20
+ },
21
+ resolve: {
22
+ // excludes - map buildConfig.excludes packages to an empty module
23
+ alias: excludes(args.buildConfig?.excludes),
24
+ },
25
+ plugins: [
26
+ // externals - map buildConfig.externals packages to a global variable on window
27
+ viteExternalsPlugin(args.buildConfig?.externals, { useWindow: false }),
28
+ ],
29
+ };
30
+ }
31
+ export async function viteConfigProductionServer(args) {
32
+ return {
33
+ ...(await viteConfigShared(args)),
34
+ build: {
35
+ emptyOutDir: false,
36
+ modulePreload: false,
37
+ // This is an important setting for producing a server build
38
+ ssr: true,
39
+ rollupOptions: {
40
+ input: '/node_modules/@nerest/nerest/server/production.ts',
41
+ output: {
42
+ dir: 'build',
43
+ entryFileNames: `server.mjs`,
44
+ chunkFileNames: `[name].mjs`,
45
+ assetFileNames: `[name].[ext]`,
46
+ },
47
+ },
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,10 @@
1
+ import type { InlineConfig } from 'vite';
2
+ import type { BuildConfiguration } from '../../schemas/nerest-build.schema.js';
3
+ import type { Project } from '../../server/loaders/project.js';
4
+ export type BuildArgs = {
5
+ root: string;
6
+ base: string;
7
+ buildConfig: BuildConfiguration | undefined;
8
+ project: Project;
9
+ };
10
+ export declare function viteConfigShared({ root, base, buildConfig, project, }: BuildArgs): Promise<InlineConfig>;
@@ -0,0 +1,24 @@
1
+ export async function viteConfigShared({ root, base, buildConfig, project, }) {
2
+ // This will be available to client scripts with import.meta.env
3
+ process.env.NEREST_PROJECT_NAME = project.name;
4
+ process.env.NEREST_CLIENT_SIDE_EFFECTS = JSON.stringify(buildConfig?.clientSideEffects ?? []);
5
+ return {
6
+ root,
7
+ base,
8
+ appType: 'custom',
9
+ envPrefix: 'NEREST_',
10
+ css: {
11
+ postcss: {
12
+ // postcss plugins - import modules mentioned in buildConfig.postcss.plugins
13
+ plugins: await loadPostcssPlugins(buildConfig?.postcss?.plugins),
14
+ },
15
+ },
16
+ };
17
+ }
18
+ async function loadPostcssPlugins(plugins) {
19
+ if (!plugins) {
20
+ return undefined;
21
+ }
22
+ const imports = Object.entries(plugins).map(async ([name, options]) => (await import(name)).default(options));
23
+ return Promise.all(imports);
24
+ }
@@ -0,0 +1,2 @@
1
+ declare const logger: import("vite").Logger;
2
+ export default logger;
@@ -0,0 +1,25 @@
1
+ // https://vite.dev/config/shared-options.html#customlogger
2
+ import { createLogger } from 'vite';
3
+ const logger = createLogger();
4
+ const loggerError = logger.error;
5
+ logger.error = (msg, options) => {
6
+ if (typeof msg === 'string' && !isIgnoredError(msg)) {
7
+ loggerError(msg, options);
8
+ }
9
+ };
10
+ export default logger;
11
+ // These are errors expected in development that we don't need to log.
12
+ // If the error message includes all markers from a set, it is suppressed
13
+ const ignoredErrors = [
14
+ // Hook files are optional, but in development vite logs an error even though
15
+ // we suppress the exception. Silence these logs manually.
16
+ ['cannot find entry point module', 'props.ts'],
17
+ ['cannot find entry point module', 'runtime.ts'],
18
+ ];
19
+ function isIgnoredError(msg) {
20
+ for (const markers of ignoredErrors) {
21
+ if (markers.every((m) => msg.includes(m))) {
22
+ return true;
23
+ }
24
+ }
25
+ }
@@ -1,85 +1,55 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs/promises';
3
- import { existsSync } from 'fs';
4
3
  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';
4
+ import { loadBuildConfig } from '../server/loaders/build.js';
5
+ import { loadApps } from '../server/loaders/apps.js';
6
+ import { loadProject } from '../server/loaders/project.js';
7
+ import { viteConfigProductionClient, viteConfigProductionServer, } from './configs/production.js';
8
8
  export async function buildMicroFrontend() {
9
9
  const root = process.cwd();
10
- const staticPath = process.env.NEREST_STATIC_PATH;
11
- // TODO: The path where the client files are deployed is built-in during
12
- // the initial build, but the client scripts aren't using it, so maybe it should
13
- // be a runtime env variable for the server instead?
14
- if (!staticPath) {
15
- throw new Error('NEREST_STATIC_PATH environment variable is not set but is required for the production build');
16
- }
17
- const buildConfig = await readBuildConfig(root);
10
+ const staticPath = prepareStaticPath();
11
+ // Read build customizations from nerest/build.json
12
+ const buildConfig = await loadBuildConfig(root);
13
+ // Read project meta info from package.json
14
+ const project = await loadProject(root);
18
15
  // Build client
19
- // TODO: extract shared parts between build/index.ts and server/index.ts
20
- // into a shared config
21
- const clientConfig = {
16
+ const clientViteConfig = await viteConfigProductionClient({
22
17
  root,
23
- appType: 'custom',
24
- envPrefix: 'NEREST_',
25
- build: {
26
- // Manifest is needed to report used assets in SSR handles
27
- manifest: true,
28
- modulePreload: false,
29
- rollupOptions: {
30
- input: '/node_modules/@nerest/nerest/client/index.ts',
31
- output: {
32
- dir: 'build',
33
- entryFileNames: `client/assets/[name].js`,
34
- chunkFileNames: `client/assets/[name].js`,
35
- assetFileNames: `client/assets/[name].[ext]`,
36
- },
37
- },
38
- },
39
- resolve: {
40
- // excludes - map buildConfig.excludes packages to an empty module
41
- alias: excludes(buildConfig?.excludes),
42
- },
43
- plugins: [
44
- // externals - map buildConfig.externals packages to a global variable on window
45
- viteExternalsPlugin(buildConfig?.externals, { useWindow: false }),
46
- ],
47
- };
18
+ base: staticPath,
19
+ buildConfig,
20
+ project,
21
+ });
48
22
  console.log('Producing production client build...');
49
- await build(clientConfig);
23
+ await build(clientViteConfig);
24
+ // Create nerest-manifest.json that production server reads on startup
50
25
  console.log('Producing Nerest manifest file...');
51
- await buildAppsManifest(root, staticPath);
52
- // Build server using the client manifest
53
- const serverConfig = {
26
+ await createNerestManifest(root, staticPath, project);
27
+ // Build server
28
+ const serverViteConfig = await viteConfigProductionServer({
54
29
  root,
55
- appType: 'custom',
56
- envPrefix: 'NEREST_',
57
- build: {
58
- emptyOutDir: false,
59
- modulePreload: false,
60
- // This is an important setting for producing a server build
61
- ssr: true,
62
- rollupOptions: {
63
- input: '/node_modules/@nerest/nerest/server/production.ts',
64
- output: {
65
- dir: 'build',
66
- entryFileNames: `server.mjs`,
67
- },
68
- },
69
- },
70
- };
30
+ base: staticPath,
31
+ buildConfig,
32
+ project,
33
+ });
71
34
  console.log('Producing production server build...');
72
- await build(serverConfig);
35
+ await build(serverViteConfig);
73
36
  }
74
- async function buildAppsManifest(root, staticPath) {
37
+ async function createNerestManifest(root, staticPath, project) {
75
38
  const apps = await loadApps(root, staticPath);
76
- await fs.writeFile(path.join(root, 'build/nerest-manifest.json'), JSON.stringify(apps), { encoding: 'utf-8' });
39
+ await fs.writeFile(path.join(root, 'build/nerest-manifest.json'), JSON.stringify({ project, apps }), 'utf-8');
77
40
  }
78
- // TODO: error handling
79
- async function readBuildConfig(root) {
80
- const configPath = path.join(root, 'nerest-build.json');
81
- if (existsSync(configPath)) {
82
- const content = await fs.readFile(configPath, { encoding: 'utf-8' });
83
- return JSON.parse(content);
41
+ function prepareStaticPath() {
42
+ let staticPath = process.env.STATIC_PATH;
43
+ // The path where the client files are deployed is embedded
44
+ // during the initial build.
45
+ // TODO: handle error if STATIC_PATH isn't a valid base URL
46
+ if (!staticPath) {
47
+ throw new Error('STATIC_PATH environment variable is not set but is required for the production build');
48
+ }
49
+ // Static path is a directory URI. To be treated as such by node:url,
50
+ // it has to end with a trailing slash
51
+ if (!staticPath.endsWith('/')) {
52
+ staticPath += '/';
84
53
  }
54
+ return staticPath;
85
55
  }
@@ -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>;