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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/admin/admin-about.tsx +16 -0
- package/admin/admin-config.tsx +44 -0
- package/admin/admin-css.tsx +3 -0
- package/admin/admin-db.tsx +74 -0
- package/admin/admin-frame-props.tsx +9 -0
- package/admin/admin-frame.tsx +466 -0
- package/admin/admin-index.tsx +66 -0
- package/admin/admin-login.tsx +99 -0
- package/admin/admin-menu-edit.tsx +637 -0
- package/admin/admin-menu-list.tsx +87 -0
- package/admin/admin-page-edit.tsx +564 -0
- package/admin/admin-page-list.tsx +83 -0
- package/admin/admin-performance.tsx +28 -0
- package/admin/admin-release.tsx +320 -0
- package/admin/admin-resources.tsx +385 -0
- package/admin/admin-shell.tsx +89 -0
- package/admin/admin-table-data.tsx +146 -0
- package/admin/admin-table-list.tsx +231 -0
- package/admin/admin-test-animations.tsx +379 -0
- package/admin/admin-test-component.tsx +808 -0
- package/admin/admin-test-edit.tsx +319 -0
- package/admin/admin-test-themes.tsx +56 -0
- package/admin/admin-tokens.tsx +338 -0
- package/admin/design/admin-design.tsx +174 -0
- package/admin/design/block-grid.tsx +36 -0
- package/admin/design/block-grid1.tsx +21 -0
- package/admin/design/block-paragraph.tsx +19 -0
- package/admin/design/block-title.tsx +19 -0
- package/admin/design/design-block-box.tsx +140 -0
- package/admin/design/drag-data.tsx +24 -0
- package/admin/index.ts +6 -0
- package/admin/package.json +15 -0
- package/admin/tsconfig.json +127 -0
- package/dev/copy-folder.js +32 -0
- package/dev/cp-index-html.js +69 -0
- package/dev/file-utils.js +12 -0
- package/dev/index.js +19 -0
- package/dev/package.json +12 -0
- package/dev/plugin-gen-versions.js +20 -0
- package/dev/plugin-ifelse.js +155 -0
- package/dev/plugin-ifelse.test.js +37 -0
- package/dev/run-cmd.js +14 -0
- package/dev/send-request.js +12 -0
- package/package.json +55 -0
- package/src/admin-api/admin-api.ts +59 -0
- package/src/admin-api/admin-auth.ts +87 -0
- package/src/admin-api/admin-config.ts +93 -0
- package/src/admin-api/admin-csv.ts +81 -0
- package/src/admin-api/admin-db.ts +269 -0
- package/src/admin-api/admin-helper.ts +111 -0
- package/src/admin-api/admin-menu.ts +135 -0
- package/src/admin-api/admin-page.ts +135 -0
- package/src/admin-api/admin-performance.ts +128 -0
- package/src/admin-api/admin-release.ts +498 -0
- package/src/admin-api/admin-resources.ts +318 -0
- package/src/admin-api/admin-token-helper.ts +79 -0
- package/src/admin-api/admin-tokens.ts +90 -0
- package/src/admin-api/index.ts +2 -0
- package/src/api/api-cache.ts +103 -0
- package/src/api/api-helper.ts +44 -0
- package/src/api/api-module.ts +60 -0
- package/src/api/api-router.ts +177 -0
- package/src/api/api-shared-storage.ts +64 -0
- package/src/api/async-storage.ts +5 -0
- package/src/api/debug-service.ts +56 -0
- package/src/api/encode-html.ts +27 -0
- package/src/api/handle-status.ts +71 -0
- package/src/api/index.ts +16 -0
- package/src/api/mini-web-socket.ts +270 -0
- package/src/api/server-content-type.ts +82 -0
- package/src/api/server-render.ts +216 -0
- package/src/api/shell-service.ts +66 -0
- package/src/api/simple-storage.ts +80 -0
- package/src/api/static-server.ts +125 -0
- package/src/api/to-client-delivery.ts +26 -0
- package/src/app/app-cache.ts +55 -0
- package/src/app/app-loader.ts +62 -0
- package/src/app/app-message.ts +60 -0
- package/src/app/app-shared-storage.ts +317 -0
- package/src/app/app-start.ts +117 -0
- package/src/app/cleanup-exit.ts +12 -0
- package/src/app/host-to-path.ts +38 -0
- package/src/app/index.ts +11 -0
- package/src/app/process-dev-requests.ts +90 -0
- package/src/app/web-listener.ts +230 -0
- package/src/app/web-processor.ts +42 -0
- package/src/app/web-server.ts +86 -0
- package/src/common-js/web-env.js +104 -0
- package/src/index.ts +7 -0
- package/src/lang/api-lang-en.ts +27 -0
- package/src/lang/api-lang-zh-cn.ts +28 -0
- package/src/lang/index.ts +2 -0
- package/src/lang/lang-helper.ts +76 -0
- package/src/lang/lang-props.ts +6 -0
- package/src/lib/db/db-helper.ts +23 -0
- package/src/lib/db/db-mysql.ts +250 -0
- package/src/lib/db/db-sqlite.ts +101 -0
- package/src/lib/db/db.spec.ts +28 -0
- package/src/lib/db/db.ts +304 -0
- package/src/lib/db/index.ts +5 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/logger.spec.ts +214 -0
- package/src/lib/logger.ts +274 -0
- package/src/lib/runtime-require.ts +37 -0
- package/src/lib/utils/cookie-util.ts +34 -0
- package/src/lib/utils/crypto.ts +58 -0
- package/src/lib/utils/date-utils.ts +317 -0
- package/src/lib/utils/deep-merge.ts +37 -0
- package/src/lib/utils/delay.ts +12 -0
- package/src/lib/utils/file-setting.ts +55 -0
- package/src/lib/utils/format-bytes.ts +11 -0
- package/src/lib/utils/fs-utils.ts +144 -0
- package/src/lib/utils/get-env.ts +27 -0
- package/src/lib/utils/index.ts +12 -0
- package/src/lib/utils/is-type.ts +48 -0
- package/src/lib/utils/load-env.ts +14 -0
- package/src/lib/utils/pad.ts +6 -0
- package/src/models/api-base.ts +5 -0
- package/src/models/api-module-props.ts +11 -0
- package/src/models/api-router-props.ts +26 -0
- package/src/models/app-cache-props.ts +33 -0
- package/src/models/app-data-props.ts +10 -0
- package/src/models/app-loader-props.ts +6 -0
- package/src/models/app-shared-storage-props.ts +37 -0
- package/src/models/app-start-props.ts +18 -0
- package/src/models/async-storage-props.ts +13 -0
- package/src/models/db-config.ts +30 -0
- package/src/models/host-to-path-props.ts +12 -0
- package/src/models/index.ts +16 -0
- package/src/models/json-object.ts +8 -0
- package/src/models/locals-props.ts +36 -0
- package/src/models/logger-props.ts +84 -0
- package/src/models/simple-storage-props.ts +14 -0
- package/src/models/to-client-delivery-props.ts +6 -0
- 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();
|