lupine.api 1.0.41

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 (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/admin/admin-about.tsx +16 -0
  4. package/admin/admin-config.tsx +44 -0
  5. package/admin/admin-css.tsx +3 -0
  6. package/admin/admin-db.tsx +74 -0
  7. package/admin/admin-frame-props.tsx +9 -0
  8. package/admin/admin-frame.tsx +466 -0
  9. package/admin/admin-index.tsx +66 -0
  10. package/admin/admin-login.tsx +99 -0
  11. package/admin/admin-menu-edit.tsx +637 -0
  12. package/admin/admin-menu-list.tsx +87 -0
  13. package/admin/admin-page-edit.tsx +564 -0
  14. package/admin/admin-page-list.tsx +83 -0
  15. package/admin/admin-performance.tsx +28 -0
  16. package/admin/admin-release.tsx +320 -0
  17. package/admin/admin-resources.tsx +385 -0
  18. package/admin/admin-shell.tsx +89 -0
  19. package/admin/admin-table-data.tsx +146 -0
  20. package/admin/admin-table-list.tsx +231 -0
  21. package/admin/admin-test-animations.tsx +379 -0
  22. package/admin/admin-test-component.tsx +808 -0
  23. package/admin/admin-test-edit.tsx +319 -0
  24. package/admin/admin-test-themes.tsx +56 -0
  25. package/admin/admin-tokens.tsx +338 -0
  26. package/admin/design/admin-design.tsx +174 -0
  27. package/admin/design/block-grid.tsx +36 -0
  28. package/admin/design/block-grid1.tsx +21 -0
  29. package/admin/design/block-paragraph.tsx +19 -0
  30. package/admin/design/block-title.tsx +19 -0
  31. package/admin/design/design-block-box.tsx +140 -0
  32. package/admin/design/drag-data.tsx +24 -0
  33. package/admin/index.ts +6 -0
  34. package/admin/package.json +15 -0
  35. package/admin/tsconfig.json +127 -0
  36. package/dev/copy-folder.js +32 -0
  37. package/dev/cp-index-html.js +69 -0
  38. package/dev/file-utils.js +12 -0
  39. package/dev/index.js +19 -0
  40. package/dev/package.json +12 -0
  41. package/dev/plugin-gen-versions.js +20 -0
  42. package/dev/plugin-ifelse.js +155 -0
  43. package/dev/plugin-ifelse.test.js +37 -0
  44. package/dev/run-cmd.js +14 -0
  45. package/dev/send-request.js +12 -0
  46. package/package.json +55 -0
  47. package/src/admin-api/admin-api.ts +59 -0
  48. package/src/admin-api/admin-auth.ts +87 -0
  49. package/src/admin-api/admin-config.ts +93 -0
  50. package/src/admin-api/admin-csv.ts +81 -0
  51. package/src/admin-api/admin-db.ts +269 -0
  52. package/src/admin-api/admin-helper.ts +111 -0
  53. package/src/admin-api/admin-menu.ts +135 -0
  54. package/src/admin-api/admin-page.ts +135 -0
  55. package/src/admin-api/admin-performance.ts +128 -0
  56. package/src/admin-api/admin-release.ts +498 -0
  57. package/src/admin-api/admin-resources.ts +318 -0
  58. package/src/admin-api/admin-token-helper.ts +79 -0
  59. package/src/admin-api/admin-tokens.ts +90 -0
  60. package/src/admin-api/index.ts +2 -0
  61. package/src/api/api-cache.ts +103 -0
  62. package/src/api/api-helper.ts +44 -0
  63. package/src/api/api-module.ts +60 -0
  64. package/src/api/api-router.ts +177 -0
  65. package/src/api/api-shared-storage.ts +64 -0
  66. package/src/api/async-storage.ts +5 -0
  67. package/src/api/debug-service.ts +56 -0
  68. package/src/api/encode-html.ts +27 -0
  69. package/src/api/handle-status.ts +71 -0
  70. package/src/api/index.ts +16 -0
  71. package/src/api/mini-web-socket.ts +270 -0
  72. package/src/api/server-content-type.ts +82 -0
  73. package/src/api/server-render.ts +216 -0
  74. package/src/api/shell-service.ts +66 -0
  75. package/src/api/simple-storage.ts +80 -0
  76. package/src/api/static-server.ts +125 -0
  77. package/src/api/to-client-delivery.ts +26 -0
  78. package/src/app/app-cache.ts +55 -0
  79. package/src/app/app-loader.ts +62 -0
  80. package/src/app/app-message.ts +60 -0
  81. package/src/app/app-shared-storage.ts +317 -0
  82. package/src/app/app-start.ts +117 -0
  83. package/src/app/cleanup-exit.ts +12 -0
  84. package/src/app/host-to-path.ts +38 -0
  85. package/src/app/index.ts +11 -0
  86. package/src/app/process-dev-requests.ts +90 -0
  87. package/src/app/web-listener.ts +230 -0
  88. package/src/app/web-processor.ts +42 -0
  89. package/src/app/web-server.ts +86 -0
  90. package/src/common-js/web-env.js +104 -0
  91. package/src/index.ts +7 -0
  92. package/src/lang/api-lang-en.ts +27 -0
  93. package/src/lang/api-lang-zh-cn.ts +28 -0
  94. package/src/lang/index.ts +2 -0
  95. package/src/lang/lang-helper.ts +76 -0
  96. package/src/lang/lang-props.ts +6 -0
  97. package/src/lib/db/db-helper.ts +23 -0
  98. package/src/lib/db/db-mysql.ts +250 -0
  99. package/src/lib/db/db-sqlite.ts +101 -0
  100. package/src/lib/db/db.spec.ts +28 -0
  101. package/src/lib/db/db.ts +304 -0
  102. package/src/lib/db/index.ts +5 -0
  103. package/src/lib/index.ts +3 -0
  104. package/src/lib/logger.spec.ts +214 -0
  105. package/src/lib/logger.ts +274 -0
  106. package/src/lib/runtime-require.ts +37 -0
  107. package/src/lib/utils/cookie-util.ts +34 -0
  108. package/src/lib/utils/crypto.ts +58 -0
  109. package/src/lib/utils/date-utils.ts +317 -0
  110. package/src/lib/utils/deep-merge.ts +37 -0
  111. package/src/lib/utils/delay.ts +12 -0
  112. package/src/lib/utils/file-setting.ts +55 -0
  113. package/src/lib/utils/format-bytes.ts +11 -0
  114. package/src/lib/utils/fs-utils.ts +144 -0
  115. package/src/lib/utils/get-env.ts +27 -0
  116. package/src/lib/utils/index.ts +12 -0
  117. package/src/lib/utils/is-type.ts +48 -0
  118. package/src/lib/utils/load-env.ts +14 -0
  119. package/src/lib/utils/pad.ts +6 -0
  120. package/src/models/api-base.ts +5 -0
  121. package/src/models/api-module-props.ts +11 -0
  122. package/src/models/api-router-props.ts +26 -0
  123. package/src/models/app-cache-props.ts +33 -0
  124. package/src/models/app-data-props.ts +10 -0
  125. package/src/models/app-loader-props.ts +6 -0
  126. package/src/models/app-shared-storage-props.ts +37 -0
  127. package/src/models/app-start-props.ts +18 -0
  128. package/src/models/async-storage-props.ts +13 -0
  129. package/src/models/db-config.ts +30 -0
  130. package/src/models/host-to-path-props.ts +12 -0
  131. package/src/models/index.ts +16 -0
  132. package/src/models/json-object.ts +8 -0
  133. package/src/models/locals-props.ts +36 -0
  134. package/src/models/logger-props.ts +84 -0
  135. package/src/models/simple-storage-props.ts +14 -0
  136. package/src/models/to-client-delivery-props.ts +6 -0
  137. package/tsconfig.json +115 -0
@@ -0,0 +1,82 @@
1
+ export const serverContentType: { [key: string]: string } = {
2
+ txt: 'text/plain',
3
+ htm: 'text/html',
4
+ html: 'text/html',
5
+ xhtml: 'application/xhtml+xml',
6
+ xml: 'text/xml',
7
+ css: 'text/css',
8
+ js: 'text/javascript',
9
+ wasm: 'application/wasm',
10
+ json: 'application/json',
11
+ csv: 'text/csv',
12
+ yaml: 'application/x-yaml',
13
+ md: 'text/markdown',
14
+ yml: 'application/x-yaml',
15
+ map: 'application/json',
16
+ rss: 'application/rss+xml',
17
+
18
+ gif: 'image/gif',
19
+ jpeg: 'image/jpeg',
20
+ jpg: 'image/jpeg',
21
+ tif: 'image/tiff',
22
+ tiff: 'image/tiff',
23
+ png: 'image/png',
24
+ svg: 'image/svg+xml',
25
+ bmp: 'image/bmp',
26
+ webp: 'image/webp',
27
+ heic: 'image/heic',
28
+ heif: 'image/heif',
29
+ avif: 'image/avif',
30
+ swf: 'application/x-shockwave-flash',
31
+ ico: 'image/x-icon',
32
+ cur: 'image/x-icon',
33
+
34
+ pdf: 'application/pdf',
35
+ doc: 'application/msword',
36
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
37
+ xls: 'application/vnd.ms-excel',
38
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
39
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
40
+ odt: 'application/vnd.oasis.opendocument.text',
41
+ ods: 'application/vnd.oasis.opendocument.spreadsheet',
42
+ odp: 'application/vnd.oasis.opendocument.presentation',
43
+ ppt: 'application/vnd.ms-powerpoint',
44
+ rtf: 'application/rtf',
45
+
46
+ woff: 'font/woff',
47
+ woff2: 'font/woff2',
48
+ ttf: 'font/ttf',
49
+ otf: 'font/otf',
50
+ eot: 'application/vnd.ms-fontobject',
51
+
52
+ mp3: 'audio/mpeg',
53
+ wav: 'audio/x-wav',
54
+ wma: 'audio/x-ms-wma',
55
+ mp4: 'video/mp4',
56
+ mpeg: 'video/mpeg',
57
+ avi: 'video/x-msvideo',
58
+ wmv: 'video/x-ms-wmv',
59
+ ogg: 'audio/ogg',
60
+ oga: 'audio/ogg',
61
+ ogv: 'video/ogg',
62
+ webm: 'video/webm',
63
+ m4a: 'audio/mp4',
64
+ aac: 'audio/aac',
65
+ flac: 'audio/flac',
66
+ mid: 'audio/midi',
67
+ midi: 'audio/midi',
68
+ '3gp': 'video/3gpp',
69
+ '3g2': 'video/3gpp2',
70
+ mov: 'video/quicktime',
71
+ mkv: 'video/x-matroska',
72
+ m4v: 'video/mp4',
73
+
74
+ jar: 'application/java-archive',
75
+ gz: 'application/gzip',
76
+ tar: 'application/x-tar',
77
+ rar: 'application/vnd.rar',
78
+ zip: 'application/zip',
79
+ '7z': 'application/x-7z-compressed',
80
+ sh: 'application/x-sh',
81
+ bat: 'application/x-msdownload',
82
+ };
@@ -0,0 +1,216 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { ServerResponse } from 'http';
4
+ import { FsUtils, Logger } from '../lib';
5
+ import { getWebEnv } from '../lib';
6
+ import { ServerRequest } from '../models/locals-props';
7
+ import { ToClientDelivery } from './to-client-delivery';
8
+ import { IToClientDelivery } from '../models/to-client-delivery-props';
9
+ import { JsonObject } from '../models/json-object';
10
+ import { getTemplateCache } from './api-cache';
11
+ import { apiStorage } from './api-shared-storage';
12
+ import { SimpleStorageDataProps } from '../models';
13
+ import { RuntimeRequire } from '../lib/runtime-require';
14
+
15
+ const logger = new Logger('StaticServer');
16
+
17
+ export type RenderPageFunctionsType = {
18
+ fetchData: (url: string, postData: string | JsonObject) => Promise<any>;
19
+ [key: string]: Function;
20
+ };
21
+ let renderPageFunctions: RenderPageFunctionsType = {
22
+ fetchData: async (url: string, postData: string | JsonObject) => {
23
+ throw new Error('Method not implemented');
24
+ },
25
+ };
26
+ export const getRenderPageFunctions = () => renderPageFunctions;
27
+ // for the FE code to fetch data in SSR
28
+ export const bindRenderPageFunctions = (calls: RenderPageFunctionsType) => {
29
+ for (let k in calls) {
30
+ renderPageFunctions[k] = calls[k];
31
+ }
32
+ };
33
+
34
+ export type PageResultType = {
35
+ content: string;
36
+ title: string;
37
+ metaData: string;
38
+ themeName: string;
39
+ globalCss: string;
40
+ };
41
+ type _LupineJs = {
42
+ generatePage: (props: any, toClientDelivery: IToClientDelivery) => Promise<PageResultType>;
43
+ };
44
+
45
+ export const isServerSideRenderUrl = (urlWithoutQuery: string) => {
46
+ /*
47
+ "" --> ""
48
+ "name" --> ""
49
+ "name.txt" --> "txt"
50
+ ".htpasswd" --> ""
51
+ "name.with.many.dots.myext" --> "myext"
52
+ */
53
+ const ext = urlWithoutQuery.slice(((urlWithoutQuery.lastIndexOf('.') - 1) >>> 0) + 2);
54
+ return ext === '' || ext === 'html';
55
+ };
56
+
57
+ // If the folder contains index.html and index.js, then the js will be used to render
58
+ const findNearestRoot = async (cachedHtml: any, webRoot: string, urlWithoutQuery: string) => {
59
+ if (urlWithoutQuery === '/' || urlWithoutQuery === '/index.html') {
60
+ return webRoot;
61
+ }
62
+ if (urlWithoutQuery.endsWith('/')) {
63
+ urlWithoutQuery = urlWithoutQuery.slice(0, -1);
64
+ }
65
+
66
+ // cache sub folders whether it has both index.html and js, or virtual path
67
+ if (!cachedHtml['_sub_:' + webRoot]) {
68
+ cachedHtml['_sub_:' + webRoot] = {};
69
+ }
70
+
71
+ const cacheRoots = cachedHtml['_sub_:' + webRoot];
72
+ let nearRoot = path.join(webRoot, urlWithoutQuery);
73
+ if (cacheRoots[nearRoot] === '1') {
74
+ return nearRoot;
75
+ }
76
+
77
+ while (
78
+ cacheRoots[nearRoot] === '0' ||
79
+ !(await FsUtils.pathExist(path.join(nearRoot, 'index.html'))) ||
80
+ !(await FsUtils.pathExist(path.join(nearRoot, 'index.js')))
81
+ ) {
82
+ cacheRoots[nearRoot] = '0';
83
+ nearRoot = path.dirname(nearRoot);
84
+ if (cacheRoots[nearRoot] === '1' || nearRoot.length <= webRoot.length) {
85
+ break;
86
+ }
87
+ }
88
+ if (nearRoot.length <= webRoot.length) {
89
+ nearRoot = webRoot;
90
+ } else {
91
+ cacheRoots[nearRoot] = '1';
92
+ }
93
+ return nearRoot;
94
+ };
95
+
96
+ const titleText = '<!--META-TITLE-->';
97
+ const metaTextStart = '<!--META-ENV-START-->';
98
+ const metaTextEnd = '<!--META-ENV-END-->';
99
+ const containerText = '<div class="lupine-root">'; // '</div>'
100
+ type CachedHtmlProps = {
101
+ content: string;
102
+ webEnv: { [k: string]: string };
103
+ // serverConfig: { [k: string]: any };
104
+ titleIndex: number;
105
+ metaIndexStart: number;
106
+ metaIndexEnd: number;
107
+ containerIndex: number;
108
+ _lupineJs: _LupineJs;
109
+ };
110
+ export const serverSideRenderPage = async (
111
+ appName: string,
112
+ webRoot: string,
113
+ urlWithoutQuery: string,
114
+ urlQuery: string,
115
+ req: ServerRequest,
116
+ res: ServerResponse
117
+ ) => {
118
+ console.log(`=========SSR, root: ${webRoot}, url: ${urlWithoutQuery}`);
119
+
120
+ // cache multiple folders
121
+ const cachedHtml = getTemplateCache();
122
+
123
+ // in order to support virtual path and also sub folders, here needs to find nearest sub folder which contains index.js
124
+ const nearRoot = await findNearestRoot(cachedHtml, webRoot, urlWithoutQuery);
125
+
126
+ if (!cachedHtml[nearRoot]) {
127
+ // the FE code needs to export _lupineJs
128
+ // const lupinJs = await import(webRoot + '/index.js');
129
+ const gThis = await RuntimeRequire.loadModuleIsolated(path.join(nearRoot, 'index.js'), { _lupineJs: null });
130
+ // const lupinJs = require(path.join(nearRoot, 'index.js'));
131
+ if (!gThis || !gThis._lupineJs) {
132
+ throw new Error('_lupineJs is not defined');
133
+ }
134
+
135
+ console.log(`=========load lupine: `, gThis);
136
+ const _lupineJs = gThis._lupineJs() as _LupineJs;
137
+
138
+ const content = await fs.promises.readFile(path.join(nearRoot, 'index.html'));
139
+ const contentWithEnv = content.toString();
140
+ cachedHtml[nearRoot] = {
141
+ content: contentWithEnv,
142
+ webEnv: getWebEnv(appName),
143
+ titleIndex: contentWithEnv.indexOf(titleText),
144
+ metaIndexStart: contentWithEnv.indexOf(metaTextStart),
145
+ metaIndexEnd: contentWithEnv.indexOf(metaTextEnd),
146
+ containerIndex: contentWithEnv.indexOf(containerText),
147
+ _lupineJs: _lupineJs,
148
+ } as CachedHtmlProps;
149
+ }
150
+
151
+ const props = {
152
+ url: urlWithoutQuery,
153
+ // urlSections: urlWithoutQuery.split('/').filter((i) => !!i),
154
+ query: Object.fromEntries(new URLSearchParams(urlQuery || '')), //new URLSearchParams(urlQuery || ''),
155
+ urlParameters: {},
156
+ renderPageFunctions: renderPageFunctions,
157
+ };
158
+
159
+ const _lupineJs = cachedHtml[nearRoot]._lupineJs;
160
+ const currentCache = cachedHtml[nearRoot] as CachedHtmlProps;
161
+ const webSetting = await apiStorage.getWebAll();
162
+ const webSettingShortKey: SimpleStorageDataProps = {};
163
+ for (let item of Object.keys(webSetting)) {
164
+ const newItem = item.substring(4);
165
+ webSettingShortKey[newItem] = webSetting[item];
166
+ }
167
+ // const webSetting = AppConfig.get(AppConfig.WEB_SETTINGS_KEY) || {};
168
+ const clientDelivery = new ToClientDelivery(currentCache.webEnv, webSettingShortKey, req.locals.cookies());
169
+ const page = await _lupineJs.generatePage(props, clientDelivery);
170
+ // console.log(`=========load lupin: `, content);
171
+
172
+ const allowOrigin = req.headers.origin && req.headers.origin !== 'null' ? req.headers.origin : '*';
173
+ res.setHeader('Access-Control-Allow-Origin', allowOrigin);
174
+ res.writeHead(200, { 'Content-Type': 'text/html' });
175
+ // res.writeHead(200, { 'Content-Type': 'text/html', 'Content-Encoding': 'gzip' });
176
+
177
+ // const s = zlib.createGzip();
178
+ // stream.pipeline(s, res, (err) => {
179
+ // s.write(cachedHtml.content.substring(0, cachedHtml.titleIndex).replace('<!--META-THEME-->', page.themeName));
180
+ // s.write(page.title);
181
+ // s.write(cachedHtml.content.substring(cachedHtml.titleIndex + titleText.length, cachedHtml.metaIndex));
182
+ // s.write(page.metaData);
183
+ // s.write(page.globalCss);
184
+ // s.write(
185
+ // cachedHtml.content.substring(cachedHtml.metaIndex + metaText.length, cachedHtml.containerIndex + containerText.length)
186
+ // )
187
+ // s.write(page.content);
188
+ // s.write(cachedHtml.content.substring(cachedHtml.containerIndex + containerText.length), (err) => {
189
+ // s.flush();
190
+ // res.end();
191
+ // });
192
+ // });
193
+
194
+ // data-theme and title
195
+ res.write(currentCache.content.substring(0, currentCache.titleIndex).replace('<!--META-THEME-->', page.themeName));
196
+ res.write(page.title);
197
+ res.write(currentCache.content.substring(currentCache.titleIndex + titleText.length, currentCache.metaIndexStart));
198
+ // meta data
199
+ res.write(page.metaData);
200
+ res.write(page.globalCss);
201
+ res.write('<script id="web-env" type="application/json">' + JSON.stringify(currentCache.webEnv) + '</script>');
202
+ res.write('<script id="web-setting" type="application/json">' + JSON.stringify(webSettingShortKey) + '</script>');
203
+ res.write(
204
+ currentCache.content.substring(
205
+ currentCache.metaIndexEnd + metaTextEnd.length,
206
+ currentCache.containerIndex + containerText.length
207
+ )
208
+ );
209
+ // content
210
+ res.write(page.content);
211
+ res.write(currentCache.content.substring(currentCache.containerIndex + containerText.length));
212
+
213
+ // const html = index.toString().replace('<div class="lupine-root"></div>', content);
214
+ // handler200(res, html);
215
+ res.end();
216
+ };
@@ -0,0 +1,66 @@
1
+ import { spawn, ChildProcessWithoutNullStreams } from 'child_process';
2
+ import os from 'os';
3
+ import { MiniWebSocket } from './mini-web-socket';
4
+ import { Duplex } from 'stream';
5
+
6
+ // This is only used in debug mode (no clusters)
7
+ export class ShellService {
8
+ private _shell?: ChildProcessWithoutNullStreams;
9
+ private _socket: Duplex;
10
+ private _miniWebSocket: MiniWebSocket;
11
+
12
+ constructor(socket: Duplex, miniWebSocket: MiniWebSocket) {
13
+ this._socket = socket;
14
+ this._miniWebSocket = miniWebSocket;
15
+ try {
16
+ const shellCmd: string = this.getDefaultShell();
17
+ this._shell = spawn(shellCmd, [], {
18
+ stdio: ['pipe', 'pipe', 'pipe'],
19
+ });
20
+ } catch (error) {
21
+ console.error(error);
22
+ this._miniWebSocket.sendMessage(this._socket!, JSON.stringify({ error: error }));
23
+ return;
24
+ }
25
+
26
+ this._shell.stdout.on('data', (data) => {
27
+ this._miniWebSocket.sendMessage(this._socket!, data.toString());
28
+ });
29
+ this._shell.stderr.on('data', (data) => {
30
+ this._miniWebSocket.sendMessage(this._socket!, data.toString());
31
+ });
32
+ this._shell.on('exit', (code, signal) => {
33
+ this._miniWebSocket.sendMessage(this._socket!, `Shell exited with code ${code}, signal ${signal}`);
34
+ this._shell = undefined;
35
+ });
36
+ }
37
+
38
+ getDefaultShell() {
39
+ const platform = os.platform();
40
+ if (platform === 'win32') {
41
+ return process.env.COMSPEC || 'cmd.exe';
42
+ }
43
+ return process.env.SHELL || 'bash';
44
+ }
45
+
46
+ public stop() {
47
+ this._shell?.kill();
48
+ this._shell = undefined;
49
+ }
50
+
51
+ public isRunning() {
52
+ return this._shell !== undefined;
53
+ }
54
+
55
+ public getShell() {
56
+ return this._shell;
57
+ }
58
+
59
+ public cmd(cmd: string) {
60
+ if (this._shell && this._shell.stdin.writable) {
61
+ this._shell.stdin.write(cmd + '\n');
62
+ } else {
63
+ this._miniWebSocket.sendMessage(this._socket!, 'Shell is not available.');
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,80 @@
1
+ import * as fs from 'fs/promises';
2
+ import { ISimpleStorage, SimpleStorageDataProps } from '../models/simple-storage-props';
3
+
4
+ // This class is used by both BE and FE (cookie for SSR).
5
+ export class SimpleStorage implements ISimpleStorage {
6
+ private settings: SimpleStorageDataProps = {};
7
+ private dirty: boolean = false;
8
+
9
+ constructor(settings: SimpleStorageDataProps) {
10
+ this.settings = settings;
11
+ }
12
+
13
+ setContent(settings: SimpleStorageDataProps) {
14
+ this.settings = settings;
15
+ this.dirty = true;
16
+ }
17
+ async saveContent(filePath: string) {
18
+ await fs.writeFile(filePath, JSON.stringify(this.settings));
19
+ this.dirty = false;
20
+ }
21
+
22
+ set Dirty(dirty: boolean) {
23
+ this.dirty = dirty;
24
+ }
25
+ get Dirty(): boolean {
26
+ return this.dirty;
27
+ }
28
+
29
+ contains(key: string): boolean {
30
+ return key in this.settings;
31
+ }
32
+ size(): number {
33
+ return Object.keys(this.settings).length;
34
+ }
35
+ set(key: string, value: string) {
36
+ this.dirty = true;
37
+ if (typeof value === 'undefined') {
38
+ delete this.settings[key];
39
+ } else {
40
+ this.settings[key] = value;
41
+ }
42
+ }
43
+
44
+ getWithPrefix(prefixKey: string): SimpleStorageDataProps {
45
+ // get all key startswith prefixKey
46
+ const result: SimpleStorageDataProps = {};
47
+ for (let key in this.settings) {
48
+ if (key.startsWith(prefixKey)) {
49
+ result[key] = this.settings[key];
50
+ }
51
+ }
52
+ return result;
53
+ }
54
+
55
+ get(key: string, defaultValue: string): string {
56
+ return key in this.settings ? this.settings[key] : defaultValue;
57
+ }
58
+ getInt(key: string, defaultValue: number): number {
59
+ if (key in this.settings) {
60
+ const i = parseInt(this.settings[key]);
61
+ if (!isNaN(i)) {
62
+ return i;
63
+ }
64
+ }
65
+ return defaultValue;
66
+ }
67
+ getBoolean(key: string, defaultValue: boolean): boolean {
68
+ return key in this.settings
69
+ ? this.settings[key] === '1' || this.settings[key].toLowerCase() === 'true'
70
+ : defaultValue;
71
+ }
72
+ getJson(key: string, defaultValue: object): object {
73
+ if (key in this.settings) {
74
+ try {
75
+ return JSON.parse(this.settings[key]);
76
+ } catch (error) {}
77
+ }
78
+ return defaultValue;
79
+ }
80
+ }
@@ -0,0 +1,125 @@
1
+ // const request = require('request');
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { ServerResponse } from 'http';
5
+ import { Logger } from '../lib';
6
+ import { ServerRequest } from '../models/locals-props';
7
+ import { handler200, handler404, handler500 } from './handle-status';
8
+ import { isServerSideRenderUrl, serverSideRenderPage } from './server-render';
9
+ import { serverContentType } from './server-content-type';
10
+ import { apiCache } from './api-cache';
11
+
12
+ export class StaticServer {
13
+ logger = new Logger('StaticServer');
14
+
15
+ private async sendFile(realPath: string, requestPath: string, res: ServerResponse) {
16
+ try {
17
+ // const text = fs.readFileSync(realPath);
18
+ // createReadStream has default autoClose(true)
19
+ // https://nodejs.org/api/fs.html#fscreatereadstreampath-options
20
+ const fileStream = fs.createReadStream(realPath);
21
+ fileStream.on('error', (error) => {
22
+ this.logger.warn(`File not found: ${realPath}`);
23
+ handler404(res);
24
+ return true;
25
+ });
26
+ fileStream.on('open', () => {
27
+ let ext = path.extname(realPath);
28
+ ext = ext ? ext.slice(1) : 'unknown';
29
+ const contentType = serverContentType[ext] || 'application/octet-stream';
30
+ res.writeHead(200, {
31
+ 'Content-Type': contentType + '; charset=UTF-8',
32
+ });
33
+ });
34
+ fileStream.on('end', function () {
35
+ res.end();
36
+ });
37
+ // res.write(text);
38
+ // res.end();
39
+ fileStream.pipe(res);
40
+
41
+ return true;
42
+ } catch (err: any) {
43
+ if (err.code === 'ENOENT') {
44
+ this.logger.warn(`File not found: ${realPath}`);
45
+ handler200(res, `File not found: ${requestPath}`);
46
+ } else {
47
+ this.logger.error(`Error for: ${realPath}`, err);
48
+ handler200(res, 'Service failed: ' + err.message);
49
+ }
50
+ return true;
51
+ }
52
+ }
53
+
54
+ async processRequest(req: ServerRequest, res: ServerResponse, rootUrl?: string) {
55
+ this.logger.info(`StaticServer, url: ${req.locals.url}, host: ${req.locals.host}, rootUrl: ${rootUrl}`);
56
+
57
+ const hostPath = apiCache.getAsyncStore().hostPath;
58
+ const urlSplit = (rootUrl || req.locals.urlWithoutQuery).split('?');
59
+ const fullPath = path.join(hostPath.webPath, urlSplit[0]);
60
+
61
+ const jumpToServerSideRender = () => {
62
+ const error = new Error();
63
+ (error as any).code = 'ENOENT';
64
+ // jump to serverSideRenderPage
65
+ throw error;
66
+ };
67
+ try {
68
+ if (urlSplit[0] === '/' || urlSplit[0] === '/index.html') {
69
+ jumpToServerSideRender();
70
+ }
71
+
72
+ // if fullPath doesn't exist, it will throw ENOENT error
73
+ const realPath = await fs.promises.realpath(fullPath);
74
+ console.log(`request: ${realPath}`);
75
+ // for security reason, the requested file should be inside of wwwRoot
76
+ if (realPath.substring(0, hostPath.webPath.length) !== hostPath.webPath) {
77
+ this.logger.warn(`ACCESS DENIED: ${urlSplit[0]}`);
78
+ handler200(res, `ACCESS DENIED: ${urlSplit[0]}`);
79
+ return true;
80
+ }
81
+
82
+ let finalPath = '';
83
+ if ((await fs.promises.lstat(realPath)).isDirectory()) {
84
+ if ((await fs.promises.lstat(path.join(realPath, 'index.js'))).isFile()) {
85
+ // because it's directory, it means index.html, and if it has index.js, it will jump to serverSideRenderPage
86
+ jumpToServerSideRender();
87
+ }
88
+ // if index.js doesn't exist, it will send index.html
89
+ finalPath = path.join(realPath, 'index.html');
90
+ } else {
91
+ // it's a file, and if it's index.html and the same directory has index.js, it will jump to serverSideRenderPage
92
+ if (realPath.endsWith('/index.html') && (await fs.promises.lstat(path.join(path.dirname(realPath), 'index.js'))).isFile()) {
93
+ jumpToServerSideRender();
94
+ }
95
+ finalPath = realPath;
96
+ }
97
+
98
+ // now we need to send finalPath file. If finalPath doesn't exist, it will cause error and jump to serverSideRenderPage
99
+ try {
100
+ const allowOrigin = (req.headers.origin && req.headers.origin !== 'null') ? req.headers.origin : '*';
101
+ res.setHeader('Access-Control-Allow-Origin', allowOrigin);
102
+
103
+ await this.sendFile(finalPath, urlSplit[0], res);
104
+ } catch (err: any) {
105
+ this.logger.warn(`File not found: ${urlSplit[0]}`);
106
+ handler200(res, `File not found: ${urlSplit[0]}`);
107
+ }
108
+ return true;
109
+ } catch (err: any) {
110
+ // file doesn't exist
111
+ if (err.code === 'ENOENT') {
112
+ if (isServerSideRenderUrl(urlSplit[0])) {
113
+ serverSideRenderPage(hostPath.appName, hostPath.webPath, urlSplit[0], urlSplit[1], req, res);
114
+ } else {
115
+ this.logger.error(`File not found: ${urlSplit[0]}`);
116
+ handler404(res, `File not found: ${urlSplit[0]}`);
117
+ }
118
+ } else {
119
+ this.logger.error(`Error for: ${urlSplit[0]}`, err);
120
+ handler500(res, `processRequest error: ${err.message}`);
121
+ }
122
+ return true;
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,26 @@
1
+ import { IToClientDelivery } from '../models/to-client-delivery-props';
2
+ import { ISimpleStorage } from '../models/simple-storage-props';
3
+
4
+ export class ToClientDelivery implements IToClientDelivery {
5
+ private webEnv: { [k: string]: string };
6
+ private webSetting: { [k: string]: string };
7
+ private cookies: ISimpleStorage;
8
+
9
+ constructor(webEnv: { [k: string]: string }, webSetting: { [k: string]: string }, cookies: ISimpleStorage) {
10
+ this.webEnv = webEnv;
11
+ this.webSetting = webSetting;
12
+ this.cookies = cookies;
13
+ }
14
+
15
+ public getWebEnv() {
16
+ return this.webEnv;
17
+ }
18
+
19
+ public getWebSetting() {
20
+ return this.webSetting;
21
+ }
22
+
23
+ public getServerCookie() {
24
+ return this.cookies;
25
+ }
26
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * A simple settings/config class for storing key/value pairs in memory
3
+ */
4
+
5
+ import { AppCacheGlobal, AppCacheKeys, IAppCache } from '../models/app-cache-props';
6
+ // For cross clusters sharing, use AppSharedStorage
7
+ // ApiCache doesn't share cross clusters
8
+ // Since apis and app are independent, so AppCache in apis and app are different instances.
9
+ // That's why replaceInstance is used to copy data from app to apis,
10
+ // and also AppCache is not shared cross app and apis, so `set` is only supposed to be called when app starts
11
+ export class AppCache implements IAppCache {
12
+ private static instance: AppCache;
13
+
14
+ cacheMap: { [key: string]: any } = {};
15
+
16
+ private constructor() {}
17
+
18
+ public static getInstance(): AppCache {
19
+ if (!AppCache.instance) {
20
+ AppCache.instance = new AppCache();
21
+ }
22
+ return AppCache.instance;
23
+ }
24
+
25
+ clear(appName: string | undefined) {
26
+ const preKey = appName + '.';
27
+ Object.keys(this.cacheMap).forEach((key) => {
28
+ if (!appName || key.startsWith(preKey)) {
29
+ delete this.cacheMap[key];
30
+ }
31
+ });
32
+ }
33
+
34
+ get(appName: string, key: string) {
35
+ return this.cacheMap[`${appName}.${key}`];
36
+ }
37
+
38
+ set(appName: string, key: string, value: any) {
39
+ if (typeof value === 'undefined') {
40
+ delete this.cacheMap[`${appName}.${key}`];
41
+ } else {
42
+ this.cacheMap[`${appName}.${key}`] = value;
43
+ }
44
+ }
45
+
46
+ clearTemplateCache() {
47
+ const appList = this.get(AppCacheGlobal, AppCacheKeys.APP_LIST) as string[];
48
+ appList.forEach((appName) => {
49
+ this.set(appName, AppCacheKeys.TEMPLATE, undefined);
50
+ });
51
+ }
52
+ }
53
+
54
+ // this can be used in app, but in api, it should use getAppCache()
55
+ export const appCache = /* @__PURE__ */ AppCache.getInstance();