@modern-js/prod-server 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +30 -0
  4. package/dist/js/modern/constants.js +26 -0
  5. package/dist/js/modern/index.js +14 -0
  6. package/dist/js/modern/libs/context/context.js +180 -0
  7. package/dist/js/modern/libs/context/index.js +3 -0
  8. package/dist/js/modern/libs/hook-api/route.js +39 -0
  9. package/dist/js/modern/libs/hook-api/template.js +61 -0
  10. package/dist/js/modern/libs/metrics.js +12 -0
  11. package/dist/js/modern/libs/proxy.js +33 -0
  12. package/dist/js/modern/libs/render/cache/__tests__/cache.fun.test.js +70 -0
  13. package/dist/js/modern/libs/render/cache/__tests__/cache.test.js +233 -0
  14. package/dist/js/modern/libs/render/cache/__tests__/cacheable.js +53 -0
  15. package/dist/js/modern/libs/render/cache/__tests__/error-configuration.js +35 -0
  16. package/dist/js/modern/libs/render/cache/__tests__/matched-cache.js +121 -0
  17. package/dist/js/modern/libs/render/cache/index.js +74 -0
  18. package/dist/js/modern/libs/render/cache/page-caches/index.js +9 -0
  19. package/dist/js/modern/libs/render/cache/page-caches/lru.js +35 -0
  20. package/dist/js/modern/libs/render/cache/spr.js +280 -0
  21. package/dist/js/modern/libs/render/cache/type.js +1 -0
  22. package/dist/js/modern/libs/render/cache/util.js +79 -0
  23. package/dist/js/modern/libs/render/index.js +65 -0
  24. package/dist/js/modern/libs/render/modern/browser-list.js +7 -0
  25. package/dist/js/modern/libs/render/modern/index.js +42 -0
  26. package/dist/js/modern/libs/render/reader.js +112 -0
  27. package/dist/js/modern/libs/render/ssr.js +58 -0
  28. package/dist/js/modern/libs/render/static.js +46 -0
  29. package/dist/js/modern/libs/render/type.js +7 -0
  30. package/dist/js/modern/libs/route/index.js +68 -0
  31. package/dist/js/modern/libs/route/matcher.js +94 -0
  32. package/dist/js/modern/libs/route/route.js +24 -0
  33. package/dist/js/modern/libs/serve-file.js +28 -0
  34. package/dist/js/modern/server/index.js +120 -0
  35. package/dist/js/modern/server/modern-server-split.js +81 -0
  36. package/dist/js/modern/server/modern-server.js +576 -0
  37. package/dist/js/modern/type.js +1 -0
  38. package/dist/js/modern/utils.js +112 -0
  39. package/dist/js/node/constants.js +36 -0
  40. package/dist/js/node/index.js +74 -0
  41. package/dist/js/node/libs/context/context.js +194 -0
  42. package/dist/js/node/libs/context/index.js +18 -0
  43. package/dist/js/node/libs/hook-api/route.js +48 -0
  44. package/dist/js/node/libs/hook-api/template.js +69 -0
  45. package/dist/js/node/libs/metrics.js +18 -0
  46. package/dist/js/node/libs/proxy.js +44 -0
  47. package/dist/js/node/libs/render/cache/__tests__/cache.fun.test.js +77 -0
  48. package/dist/js/node/libs/render/cache/__tests__/cache.test.js +238 -0
  49. package/dist/js/node/libs/render/cache/__tests__/cacheable.js +60 -0
  50. package/dist/js/node/libs/render/cache/__tests__/error-configuration.js +42 -0
  51. package/dist/js/node/libs/render/cache/__tests__/matched-cache.js +128 -0
  52. package/dist/js/node/libs/render/cache/index.js +86 -0
  53. package/dist/js/node/libs/render/cache/page-caches/index.js +17 -0
  54. package/dist/js/node/libs/render/cache/page-caches/lru.js +47 -0
  55. package/dist/js/node/libs/render/cache/spr.js +298 -0
  56. package/dist/js/node/libs/render/cache/type.js +5 -0
  57. package/dist/js/node/libs/render/cache/util.js +105 -0
  58. package/dist/js/node/libs/render/index.js +91 -0
  59. package/dist/js/node/libs/render/modern/browser-list.js +14 -0
  60. package/dist/js/node/libs/render/modern/index.js +58 -0
  61. package/dist/js/node/libs/render/reader.js +139 -0
  62. package/dist/js/node/libs/render/ssr.js +76 -0
  63. package/dist/js/node/libs/render/static.js +62 -0
  64. package/dist/js/node/libs/render/type.js +14 -0
  65. package/dist/js/node/libs/route/index.js +83 -0
  66. package/dist/js/node/libs/route/matcher.js +108 -0
  67. package/dist/js/node/libs/route/route.js +33 -0
  68. package/dist/js/node/libs/serve-file.js +41 -0
  69. package/dist/js/node/server/index.js +142 -0
  70. package/dist/js/node/server/modern-server-split.js +97 -0
  71. package/dist/js/node/server/modern-server.js +614 -0
  72. package/dist/js/node/type.js +5 -0
  73. package/dist/js/node/utils.js +143 -0
  74. package/dist/js/styles/tsconfig.json +12 -0
  75. package/dist/types/constants.d.ts +20 -0
  76. package/dist/types/index.d.ts +11 -0
  77. package/dist/types/libs/context/context.d.ts +61 -0
  78. package/dist/types/libs/context/index.d.ts +4 -0
  79. package/dist/types/libs/hook-api/route.d.ts +14 -0
  80. package/dist/types/libs/hook-api/template.d.ts +14 -0
  81. package/dist/types/libs/metrics.d.ts +3 -0
  82. package/dist/types/libs/proxy.d.ts +4 -0
  83. package/dist/types/libs/render/cache/__tests__/cache.fun.test.d.ts +1 -0
  84. package/dist/types/libs/render/cache/__tests__/cache.test.d.ts +1 -0
  85. package/dist/types/libs/render/cache/__tests__/cacheable.d.ts +62 -0
  86. package/dist/types/libs/render/cache/__tests__/error-configuration.d.ts +28 -0
  87. package/dist/types/libs/render/cache/__tests__/matched-cache.d.ts +124 -0
  88. package/dist/types/libs/render/cache/index.d.ts +6 -0
  89. package/dist/types/libs/render/cache/page-caches/index.d.ts +2 -0
  90. package/dist/types/libs/render/cache/page-caches/lru.d.ts +15 -0
  91. package/dist/types/libs/render/cache/spr.d.ts +24 -0
  92. package/dist/types/libs/render/cache/type.d.ts +48 -0
  93. package/dist/types/libs/render/cache/util.d.ts +17 -0
  94. package/dist/types/libs/render/index.d.ts +18 -0
  95. package/dist/types/libs/render/modern/browser-list.d.ts +1 -0
  96. package/dist/types/libs/render/modern/index.d.ts +3 -0
  97. package/dist/types/libs/render/reader.d.ts +18 -0
  98. package/dist/types/libs/render/ssr.d.ts +10 -0
  99. package/dist/types/libs/render/static.d.ts +3 -0
  100. package/dist/types/libs/render/type.d.ts +33 -0
  101. package/dist/types/libs/route/index.d.ts +15 -0
  102. package/dist/types/libs/route/matcher.d.ts +15 -0
  103. package/dist/types/libs/route/route.d.ts +14 -0
  104. package/dist/types/libs/serve-file.d.ts +8 -0
  105. package/dist/types/server/index.d.ts +20 -0
  106. package/dist/types/server/modern-server-split.d.ts +26 -0
  107. package/dist/types/server/modern-server.d.ts +72 -0
  108. package/dist/types/type.d.ts +56 -0
  109. package/dist/types/utils.d.ts +19 -0
  110. package/jest.config.js +9 -0
  111. package/modern.config.js +2 -0
  112. package/package.json +82 -0
  113. package/src/constants.ts +26 -0
  114. package/src/index.ts +18 -0
  115. package/src/libs/context/context.ts +183 -0
  116. package/src/libs/context/index.ts +7 -0
  117. package/src/libs/hook-api/route.ts +42 -0
  118. package/src/libs/hook-api/template.ts +53 -0
  119. package/src/libs/metrics.ts +15 -0
  120. package/src/libs/proxy.ts +42 -0
  121. package/src/libs/render/cache/__tests__/cache.fun.test.ts +94 -0
  122. package/src/libs/render/cache/__tests__/cache.test.ts +240 -0
  123. package/src/libs/render/cache/__tests__/cacheable.ts +44 -0
  124. package/src/libs/render/cache/__tests__/error-configuration.ts +34 -0
  125. package/src/libs/render/cache/__tests__/matched-cache.ts +88 -0
  126. package/src/libs/render/cache/index.ts +75 -0
  127. package/src/libs/render/cache/page-caches/index.ts +11 -0
  128. package/src/libs/render/cache/page-caches/lru.ts +38 -0
  129. package/src/libs/render/cache/spr.ts +301 -0
  130. package/src/libs/render/cache/type.ts +59 -0
  131. package/src/libs/render/cache/util.ts +97 -0
  132. package/src/libs/render/index.ts +79 -0
  133. package/src/libs/render/modern/browser-list.ts +7 -0
  134. package/src/libs/render/modern/index.ts +41 -0
  135. package/src/libs/render/modern/module.d.ts +4 -0
  136. package/src/libs/render/reader.ts +119 -0
  137. package/src/libs/render/ssr.ts +67 -0
  138. package/src/libs/render/static.ts +52 -0
  139. package/src/libs/render/type.ts +38 -0
  140. package/src/libs/route/index.ts +76 -0
  141. package/src/libs/route/matcher.ts +108 -0
  142. package/src/libs/route/route.ts +34 -0
  143. package/src/libs/serve-file.ts +34 -0
  144. package/src/server/index.ts +147 -0
  145. package/src/server/modern-server-split.ts +97 -0
  146. package/src/server/modern-server.ts +613 -0
  147. package/src/tsconfig.json +12 -0
  148. package/src/type.ts +61 -0
  149. package/src/utils.ts +122 -0
  150. package/tests/.eslintrc.js +6 -0
  151. package/tests/context.test.ts +52 -0
  152. package/tests/fixtures/hosting-files/static/index.js +1 -0
  153. package/tests/fixtures/pure/modern.config.js +5 -0
  154. package/tests/fixtures/pure/package.json +21 -0
  155. package/tests/fixtures/pure/src/App.css +119 -0
  156. package/tests/fixtures/pure/src/App.tsx +43 -0
  157. package/tests/fixtures/pure/tsconfig.json +12 -0
  158. package/tests/fixtures/reader/index.ts +3 -0
  159. package/tests/fixtures/route-spec/dynamic.json +13 -0
  160. package/tests/fixtures/route-spec/index.json +29 -0
  161. package/tests/fixtures/ssr/bundle.js +5 -0
  162. package/tests/fixtures/static-dir/bar.html +11 -0
  163. package/tests/fixtures/static-dir/baz/index.html +11 -0
  164. package/tests/fixtures/static-dir/foo/index.html +11 -0
  165. package/tests/helper.ts +8 -0
  166. package/tests/hook.test.ts +44 -0
  167. package/tests/middleware.test.ts +179 -0
  168. package/tests/render.test.ts +102 -0
  169. package/tests/route.test.ts +77 -0
  170. package/tests/server.test.ts +101 -0
  171. package/tests/tsconfig.json +12 -0
  172. package/tests/utils.test.ts +106 -0
  173. package/tsconfig.json +11 -0
@@ -0,0 +1,97 @@
1
+ import { APIServerStartInput } from '@modern-js/server-core';
2
+ import { mergeExtension } from '../utils';
3
+ import { ModernRoute, ModernRouteInterface, RouteMatcher } from '../libs/route';
4
+ import { ApiServerMode } from '../constants';
5
+ import { ModernServerContext } from '../libs/context';
6
+ import { ModernServer } from './modern-server';
7
+
8
+ export class ModernSSRServer extends ModernServer {
9
+ // Todo should not invoke any route hook in modernSSRServer
10
+
11
+ protected async warmupSSRBundle() {
12
+ // empty
13
+ }
14
+
15
+ protected verifyMatch(context: ModernServerContext, matched: RouteMatcher) {
16
+ if (matched.generate(context.url).isApi) {
17
+ this.render404(context);
18
+ }
19
+ }
20
+
21
+ protected prepareAPIHandler(
22
+ _m: ApiServerMode,
23
+ _: APIServerStartInput['config'],
24
+ ) {
25
+ return null as any;
26
+ }
27
+
28
+ protected async prepareWebHandler(
29
+ extension: ReturnType<typeof mergeExtension>,
30
+ ) {
31
+ return super.prepareWebHandler(extension);
32
+ }
33
+
34
+ // protected filterRoutes(routes: ModernRouteInterface[]) {
35
+ // return routes.filter(route => route.entryName);
36
+ // }
37
+
38
+ protected async preServerInit() {
39
+ // empty
40
+ }
41
+ }
42
+
43
+ export class ModernAPIServer extends ModernServer {
44
+ protected async emitRouteHook(_: string, _input: any) {
45
+ // empty
46
+ }
47
+
48
+ protected async warmupSSRBundle() {
49
+ // empty
50
+ }
51
+
52
+ protected prepareWebHandler(_: ReturnType<typeof mergeExtension>) {
53
+ return null as any;
54
+ }
55
+
56
+ protected async prepareAPIHandler(
57
+ mode: ApiServerMode,
58
+ extension: APIServerStartInput['config'],
59
+ ) {
60
+ return super.prepareAPIHandler(mode, extension);
61
+ }
62
+
63
+ protected filterRoutes(routes: ModernRouteInterface[]) {
64
+ return routes.filter(route => route.isApi);
65
+ }
66
+
67
+ protected async preServerInit() {
68
+ // empty
69
+ }
70
+ }
71
+
72
+ export class ModernWebServer extends ModernServer {
73
+ protected async warmupSSRBundle() {
74
+ // empty
75
+ }
76
+
77
+ protected async handleAPI(context: ModernServerContext) {
78
+ const { proxyTarget } = this;
79
+ if (!proxyTarget?.api) {
80
+ this.proxy();
81
+ } else {
82
+ this.render404(context);
83
+ }
84
+ }
85
+
86
+ protected async handleWeb(context: ModernServerContext, route: ModernRoute) {
87
+ const { proxyTarget } = this;
88
+
89
+ if (route.isSSR && proxyTarget?.ssr) {
90
+ return this.proxy();
91
+ } else {
92
+ // if no proxyTarget but access web server, degradation to csr
93
+ route.isSSR = false;
94
+ return super.handleWeb(context, route);
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,613 @@
1
+ /* eslint-disable max-lines */
2
+ import { IncomingMessage, ServerResponse, Server, createServer } from 'http';
3
+ import util from 'util';
4
+ import path from 'path';
5
+ import { fs, ROUTE_SPEC_FILE } from '@modern-js/utils';
6
+ import { Adapter, APIServerStartInput } from '@modern-js/server-core';
7
+ import type { NormalizedConfig } from '@modern-js/core';
8
+ import mime from 'mime-types';
9
+ import axios from 'axios';
10
+ import clone from 'lodash.clone';
11
+ import {
12
+ ModernServerOptions,
13
+ NextFunction,
14
+ ServerHookRunner,
15
+ Metrics,
16
+ Logger,
17
+ ReadyOptions,
18
+ ConfWithBFF,
19
+ } from '../type';
20
+ import {
21
+ RouteMatchManager,
22
+ ModernRouteInterface,
23
+ ModernRoute,
24
+ RouteMatcher,
25
+ } from '../libs/route';
26
+ import { createRenderHandler } from '../libs/render';
27
+ import { createStaticFileHandler } from '../libs/serve-file';
28
+ import {
29
+ createErrorDocument,
30
+ createMiddlewareCollecter,
31
+ getStaticReg,
32
+ mergeExtension,
33
+ noop,
34
+ } from '../utils';
35
+ import * as reader from '../libs/render/reader';
36
+ import { createProxyHandler, ProxyOptions } from '../libs/proxy';
37
+ import { createContext, ModernServerContext } from '../libs/context';
38
+ import {
39
+ AGGRED_DIR,
40
+ ApiServerMode,
41
+ ERROR_DIGEST,
42
+ ERROR_PAGE_TEXT,
43
+ } from '../constants';
44
+ import { createTemplateAPI } from '../libs/hook-api/template';
45
+ import { createRouteAPI } from '../libs/hook-api/route';
46
+
47
+ type ModernServerHandler = (
48
+ context: ModernServerContext,
49
+ next: NextFunction,
50
+ ) => Promise<void> | void;
51
+
52
+ type ModernServerAsyncHandler = (
53
+ context: ModernServerContext,
54
+ next: NextFunction,
55
+ ) => Promise<void>;
56
+
57
+ const API_DIR = './api';
58
+ const SERVER_DIR = './server';
59
+
60
+ export class ModernServer {
61
+ // appDirectory
62
+ protected pwd: string;
63
+
64
+ // product dist dir
65
+ protected distDir: string;
66
+
67
+ // work on src or dist
68
+ protected workDir: string;
69
+
70
+ protected router!: RouteMatchManager;
71
+
72
+ protected conf: NormalizedConfig;
73
+
74
+ protected handlers: ModernServerAsyncHandler[] = [];
75
+
76
+ protected presetRoutes?: ModernRouteInterface[];
77
+
78
+ protected runner!: ServerHookRunner;
79
+
80
+ protected readonly logger: Logger;
81
+
82
+ protected readonly metrics: Metrics;
83
+
84
+ protected reader: typeof reader = reader;
85
+
86
+ protected readonly proxyTarget: ModernServerOptions['proxyTarget'];
87
+
88
+ private staticFileHandler!: ReturnType<typeof createStaticFileHandler>;
89
+
90
+ private routeRenderHandler!: ReturnType<typeof createRenderHandler>;
91
+
92
+ private frameWebHandler: Adapter | null = null;
93
+
94
+ private frameAPIHandler: Adapter | null = null;
95
+
96
+ private proxyHandler: ReturnType<typeof createProxyHandler> = null;
97
+
98
+ private _handler!: (context: ModernServerContext, next: NextFunction) => void;
99
+
100
+ private readonly staticGenerate: boolean = false;
101
+
102
+ constructor({
103
+ pwd,
104
+ config,
105
+ routes,
106
+ staticGenerate,
107
+ logger,
108
+ metrics,
109
+ proxyTarget,
110
+ }: ModernServerOptions) {
111
+ require('ignore-styles');
112
+
113
+ this.pwd = pwd;
114
+ this.distDir = path.join(pwd, config.output?.path || 'dist');
115
+ this.workDir = this.distDir;
116
+ this.conf = config;
117
+ this.logger = logger!;
118
+ this.metrics = metrics!;
119
+ this.router = new RouteMatchManager();
120
+ this.presetRoutes = routes;
121
+ this.proxyTarget = proxyTarget;
122
+
123
+ if (staticGenerate) {
124
+ this.staticGenerate = staticGenerate;
125
+ }
126
+ process.env.BUILD_TYPE = `${this.staticGenerate ? 'ssg' : 'ssr'}`;
127
+ }
128
+
129
+ // exposed requestHandler
130
+ public getRequestHandler() {
131
+ return this.requestHandler.bind(this);
132
+ }
133
+
134
+ // server prepare
135
+ public async init(runner: ServerHookRunner) {
136
+ this.runner = runner;
137
+
138
+ const { distDir, staticGenerate, conf } = this;
139
+
140
+ this.addHandler((ctx: ModernServerContext, next: NextFunction) => {
141
+ ctx.res.setHeader('Access-Control-Allow-Origin', '*');
142
+ ctx.res.setHeader('Access-Control-Allow-Credentials', 'false');
143
+ next();
144
+ });
145
+
146
+ // proxy handler, each proxy has own handler
147
+ this.proxyHandler = createProxyHandler(conf.bff?.proxy as ProxyOptions);
148
+ if (this.proxyHandler) {
149
+ this.proxyHandler.forEach(handler => {
150
+ this.addHandler(handler);
151
+ });
152
+ }
153
+
154
+ // start reader, include an time interval
155
+ this.reader.init();
156
+
157
+ // use preset routes priority
158
+ this.router.reset(
159
+ this.filterRoutes(this.presetRoutes || this.readRouteSpec()),
160
+ );
161
+
162
+ this.warmupSSRBundle();
163
+
164
+ await this.prepareFrameHandler();
165
+
166
+ // Only work when without setting `assetPrefix`.
167
+ // Setting `assetPrefix` means these resources should be uploaded to CDN.
168
+ const staticPathRegExp = getStaticReg(this.conf.output || {});
169
+
170
+ this.staticFileHandler = createStaticFileHandler([
171
+ {
172
+ path: staticPathRegExp,
173
+ target: distDir,
174
+ },
175
+ ]);
176
+
177
+ this.routeRenderHandler = createRenderHandler({
178
+ distDir,
179
+ staticGenerate,
180
+ });
181
+
182
+ await this.preServerInit();
183
+
184
+ this.addHandler(this.staticFileHandler);
185
+ this.addHandler(this.routeHandler.bind(this));
186
+
187
+ this.compose();
188
+ }
189
+
190
+ // server ready
191
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
192
+ public ready(_: ReadyOptions) {}
193
+
194
+ // invoke when http server listen
195
+ public onListening(_: Server) {
196
+ // empty
197
+ }
198
+
199
+ // close any thing run in server
200
+ public async close() {
201
+ this.reader.close();
202
+ }
203
+
204
+ public async createHTTPServer(
205
+ handler: (
206
+ req: IncomingMessage,
207
+ res: ServerResponse,
208
+ next?: () => void,
209
+ ) => void,
210
+ ) {
211
+ return createServer(handler);
212
+ }
213
+
214
+ // read route spec from route.json
215
+ protected readRouteSpec() {
216
+ const file = path.join(this.distDir, ROUTE_SPEC_FILE);
217
+
218
+ if (fs.existsSync(file)) {
219
+ const content: { routes: ModernRouteInterface[] } = fs.readJSONSync(file);
220
+ return content.routes;
221
+ }
222
+
223
+ return [];
224
+ }
225
+
226
+ // add promisify request handler to server
227
+ // handler should do not do more things after invoke next
228
+ protected addHandler(handler: ModernServerHandler) {
229
+ if ((handler as any)[Symbol.toStringTag] === 'AsyncFunction') {
230
+ this.handlers.push(handler as ModernServerAsyncHandler);
231
+ } else {
232
+ this.handlers.push(util.promisify(handler));
233
+ }
234
+ }
235
+
236
+ // return 404 page
237
+ protected render404(context: ModernServerContext) {
238
+ context.error(ERROR_DIGEST.ENOTF);
239
+ this.renderErrorPage(context, 404);
240
+ }
241
+
242
+ // gather frame extension and get framework handler
243
+ protected async prepareFrameHandler() {
244
+ const { workDir, runner } = this;
245
+
246
+ // server hook, gather plugin inject
247
+ const { getMiddlewares, ...collector } = createMiddlewareCollecter();
248
+
249
+ await runner.gather(collector);
250
+ const { api: pluginAPIExt, web: pluginWebExt } = getMiddlewares();
251
+
252
+ const apiDir = path.join(workDir, API_DIR);
253
+ const serverDir = path.join(workDir, SERVER_DIR);
254
+
255
+ // get api or web server handler from server-framework plugin
256
+ if (await fs.pathExists(path.join(serverDir))) {
257
+ const webExtension = mergeExtension(pluginWebExt);
258
+ this.frameWebHandler = await this.prepareWebHandler(webExtension);
259
+ }
260
+
261
+ if (fs.existsSync(apiDir)) {
262
+ const mode = fs.existsSync(path.join(apiDir, AGGRED_DIR.lambda))
263
+ ? ApiServerMode.frame
264
+ : ApiServerMode.func;
265
+
266
+ // if use lambda/, mean framework style of writing, then discard user extension
267
+ const apiExtension = mergeExtension(pluginAPIExt);
268
+ this.frameAPIHandler = await this.prepareAPIHandler(mode, apiExtension);
269
+ }
270
+ }
271
+
272
+ // Todo
273
+ protected async proxy() {
274
+ return null as any;
275
+ }
276
+
277
+ /* —————————————————————— function will be overwrite —————————————————————— */
278
+ protected async prepareWebHandler(
279
+ extension: ReturnType<typeof mergeExtension>,
280
+ ) {
281
+ const { workDir, runner } = this;
282
+
283
+ return runner.prepareWebServer(
284
+ {
285
+ pwd: workDir,
286
+ config: extension,
287
+ },
288
+ { onLast: () => null as any },
289
+ );
290
+ }
291
+
292
+ protected async prepareAPIHandler(
293
+ mode: ApiServerMode,
294
+ extension: APIServerStartInput['config'],
295
+ ) {
296
+ const { workDir, runner, conf } = this;
297
+ const { bff } = conf as ConfWithBFF;
298
+ const prefix = bff?.prefix || '/api';
299
+
300
+ return runner.prepareApiServer(
301
+ {
302
+ pwd: workDir,
303
+ mode,
304
+ config: extension,
305
+ prefix: Array.isArray(prefix) ? prefix[0] : prefix,
306
+ },
307
+ { onLast: () => null as any },
308
+ );
309
+ }
310
+
311
+ protected filterRoutes(routes: ModernRouteInterface[]) {
312
+ return routes;
313
+ }
314
+
315
+ protected async emitRouteHook(
316
+ eventName: 'beforeMatch' | 'afterMatch' | 'beforeRender' | 'afterRender',
317
+ input: {
318
+ context: ModernServerContext;
319
+ [propsName: string]: any;
320
+ },
321
+ ) {
322
+ input.context = clone(input.context);
323
+ return this.runner[eventName](input as any, { onLast: noop as any });
324
+ }
325
+
326
+ // warmup ssr function
327
+ protected warmupSSRBundle() {
328
+ const { distDir } = this;
329
+ const bundles = this.router.getBundles();
330
+
331
+ bundles.forEach(bundle => {
332
+ const filepath = path.join(distDir, bundle as string);
333
+ // if error, just throw and let process die
334
+ require(filepath);
335
+ });
336
+ }
337
+
338
+ protected async preServerInit() {
339
+ const { conf, runner } = this;
340
+ const preMiddleware: ModernServerAsyncHandler[] =
341
+ await runner.preServerInit(conf);
342
+
343
+ preMiddleware.flat().forEach(mid => {
344
+ this.addHandler(mid);
345
+ });
346
+ }
347
+
348
+ protected async handleAPI(context: ModernServerContext) {
349
+ const { req, res } = context;
350
+
351
+ if (!this.frameAPIHandler) {
352
+ throw new Error('can not found api hanlder');
353
+ }
354
+
355
+ await this.frameAPIHandler(req, res);
356
+ }
357
+
358
+ protected async handleWeb(context: ModernServerContext, route: ModernRoute) {
359
+ return this.routeRenderHandler({
360
+ ctx: context,
361
+ route,
362
+ runner: this.runner,
363
+ });
364
+ }
365
+
366
+ protected verifyMatch(_c: ModernServerContext, _m: RouteMatcher) {
367
+ // empty
368
+ }
369
+
370
+ /* —————————————————————— private function —————————————————————— */
371
+ // handler route.json, include api / csr / ssr
372
+ // eslint-disable-next-line max-statements
373
+ private async routeHandler(context: ModernServerContext) {
374
+ const { req, res } = context;
375
+
376
+ await this.emitRouteHook('beforeMatch', { context });
377
+
378
+ // match routes in the route spec
379
+ const matched = this.router.match(context.path);
380
+ if (!matched) {
381
+ this.render404(context);
382
+ return;
383
+ } else {
384
+ this.verifyMatch(context, matched);
385
+ }
386
+
387
+ if (res.headersSent) {
388
+ return;
389
+ }
390
+
391
+ const routeAPI = createRouteAPI(matched, this.router, context.url);
392
+ await this.emitRouteHook('afterMatch', { context, routeAPI });
393
+
394
+ if (res.headersSent) {
395
+ return;
396
+ }
397
+
398
+ const { current }: { current: RouteMatcher } = routeAPI as any;
399
+ const route: ModernRoute = current.generate(context.url);
400
+ context.setParams(route.params);
401
+ context.setServerData('router', {
402
+ baseUrl: route.urlPath,
403
+ params: route.params,
404
+ });
405
+
406
+ // route is api service
407
+ if (route.isApi) {
408
+ await this.handleAPI(context);
409
+ return;
410
+ }
411
+
412
+ if (this.frameWebHandler) {
413
+ await this.frameWebHandler(req, res);
414
+ }
415
+
416
+ // frameWebHandler has process request
417
+ if (res.headersSent) {
418
+ return;
419
+ }
420
+
421
+ if (route.entryName) {
422
+ await this.emitRouteHook('beforeRender', { context });
423
+ }
424
+
425
+ const file = await this.handleWeb(context, route);
426
+ if (!file) {
427
+ this.render404(context);
428
+ return;
429
+ }
430
+
431
+ if (file.redirect) {
432
+ res.statusCode = file.statusCode!;
433
+ res.setHeader('Location', file.content as string);
434
+ res.end();
435
+ return;
436
+ }
437
+
438
+ let response = file.content;
439
+ if (route.entryName) {
440
+ const templateAPI = createTemplateAPI(file.content.toString());
441
+ await this.emitRouteHook('afterRender', { context, templateAPI });
442
+ await this.injectMicroFE(context, templateAPI);
443
+ templateAPI.appendHead(
444
+ `<script>window._SERVER_DATA=${JSON.stringify(
445
+ context.serverData,
446
+ )}</script>`,
447
+ );
448
+ response = templateAPI.get();
449
+ }
450
+
451
+ res.setHeader('content-type', file.contentType);
452
+ res.end(response);
453
+ }
454
+
455
+ // eslint-disable-next-line max-statements
456
+ private async injectMicroFE(
457
+ context: ModernServerContext,
458
+ templateAPI: ReturnType<typeof createTemplateAPI>,
459
+ ) {
460
+ const { conf } = this;
461
+ const masterApp = conf.runtime?.masterApp;
462
+ // no inject if not master App
463
+ if (!masterApp) {
464
+ return;
465
+ }
466
+
467
+ const manifest = masterApp.manifest || {};
468
+ let modules = [];
469
+ const { modules: configModules = [] } = manifest;
470
+
471
+ // while config modules is an string, fetch data from remote
472
+ if (typeof configModules === 'string') {
473
+ const moduleRequestUrl = configModules;
474
+ try {
475
+ const { data: remoteModules } = await axios.get(moduleRequestUrl);
476
+ if (Array.isArray(remoteModules)) {
477
+ modules.push(...remoteModules);
478
+ }
479
+ } catch (e) {
480
+ context.error(ERROR_DIGEST.EMICROINJ, e as Error);
481
+ }
482
+ } else if (Array.isArray(configModules)) {
483
+ modules.push(...configModules);
484
+ }
485
+
486
+ const { headers } = context.req;
487
+
488
+ const debugName =
489
+ headers['x-micro-frontend-module-name'] ||
490
+ context.query['__debug__micro-frontend-module-name'];
491
+
492
+ const debugEntry =
493
+ headers['x-micro-frontend-module-entry'] ||
494
+ context.query['__debug__micro-frontend-module-entry'];
495
+
496
+ // add debug micro App to first
497
+ if (debugName && debugEntry && conf.server?.enableMicroFrontendDebug) {
498
+ modules = modules.map(m => {
499
+ if (m.name === debugName) {
500
+ return {
501
+ name: debugName,
502
+ entry: debugEntry,
503
+ };
504
+ }
505
+
506
+ return m;
507
+ });
508
+ }
509
+
510
+ try {
511
+ // Todo Safety xss
512
+ const injection = JSON.stringify({ ...manifest, modules });
513
+ templateAPI.appendHead(
514
+ `<script>window.modern_manifest=${injection}</script>`,
515
+ );
516
+ } catch (e) {
517
+ context.error(ERROR_DIGEST.EMICROINJ, e as Error);
518
+ }
519
+ }
520
+
521
+ // compose handlers and create the final handler
522
+ private compose() {
523
+ const { handlers } = this;
524
+
525
+ if (!Array.isArray(handlers)) {
526
+ throw new TypeError('Middleware stack must be an array!');
527
+ }
528
+ for (const fn of handlers) {
529
+ if (typeof fn !== 'function') {
530
+ throw new TypeError('Middleware must be composed of functions!');
531
+ }
532
+ }
533
+
534
+ this._handler = (context: ModernServerContext, next: NextFunction) => {
535
+ let i = 0;
536
+ const dispatch = () => {
537
+ const handler = handlers[i++];
538
+ if (!handler) {
539
+ return next();
540
+ }
541
+
542
+ // eslint-disable-next-line promise/prefer-await-to-then
543
+ return handler(context, dispatch as NextFunction).catch(onError);
544
+ };
545
+
546
+ const onError = (err: Error) => {
547
+ this.onError(context, err);
548
+ };
549
+ return dispatch();
550
+ };
551
+ }
552
+
553
+ private requestHandler(
554
+ req: IncomingMessage,
555
+ res: ServerResponse,
556
+ next = () => {
557
+ // empty
558
+ },
559
+ ) {
560
+ res.statusCode = 200;
561
+ req.logger = req.logger || this.logger;
562
+ req.metrics = req.metrics || this.metrics;
563
+ const context: ModernServerContext = createContext(req, res);
564
+
565
+ try {
566
+ this._handler(context, next);
567
+ } catch (err) {
568
+ this.onError(context, err as Error);
569
+ }
570
+ }
571
+
572
+ private onError(context: ModernServerContext, err: Error) {
573
+ context.error(ERROR_DIGEST.EINTER, err);
574
+ this.renderErrorPage(context, 500);
575
+ }
576
+
577
+ private async renderErrorPage(context: ModernServerContext, status: number) {
578
+ const { res } = context;
579
+ context.status = status;
580
+ res.setHeader('content-type', mime.contentType('html') as string);
581
+
582
+ const statusPage = `/${status}`;
583
+ const customErrorPage = `/_error`;
584
+
585
+ const matched =
586
+ this.router.match(statusPage) || this.router.match(customErrorPage);
587
+ // if no custom status page find
588
+ if (matched) {
589
+ const route = matched.generate(context.url);
590
+ const { entryName } = route;
591
+ // check entryName, aviod matched '/' route
592
+ if (entryName === status.toString() || entryName === '_error') {
593
+ try {
594
+ const file = await this.routeRenderHandler({
595
+ route,
596
+ ctx: context,
597
+ runner: this.runner,
598
+ });
599
+ if (file) {
600
+ context.res.end(file.content);
601
+ return;
602
+ }
603
+ } catch (e) {
604
+ // just catch error when the rendering error occurred in the custom error page.
605
+ }
606
+ }
607
+ }
608
+
609
+ const text = ERROR_PAGE_TEXT[status] || ERROR_PAGE_TEXT[500];
610
+ res.end(createErrorDocument(status, text));
611
+ }
612
+ }
613
+ /* eslint-enable max-lines */
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "@modern-js/tsconfig/base",
3
+ "compilerOptions": {
4
+ "declaration": false,
5
+ "jsx": "preserve",
6
+ "baseUrl": "./",
7
+ "isolatedModules": true,
8
+ "sourceMap": true
9
+ },
10
+ "paths": {},
11
+ "exclude": ["src/__test__/fixtures/**"]
12
+ }