lupine.api 1.1.65 → 1.1.67
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/dev/cp-index-html.js +4 -2
- package/dev/markdown-build.js +14 -12
- package/dev/send-request.js +2 -2
- package/package.json +1 -1
- package/src/app/app-start.ts +2 -1
- package/src/app/web-server.ts +64 -10
- package/src/models/app-start-props.ts +1 -0
package/dev/cp-index-html.js
CHANGED
|
@@ -29,7 +29,7 @@ const readWebConfig = async (outdirData) => {
|
|
|
29
29
|
|
|
30
30
|
const metaTextStart = '<!--META-ENV-START-->';
|
|
31
31
|
const metaTextEnd = '<!--META-ENV-END-->';
|
|
32
|
-
exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThemeName, outdirData) => {
|
|
32
|
+
exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThemeName, outdirData, outdirSub) => {
|
|
33
33
|
const f1 = await fileSizeAndTime(htmlFile);
|
|
34
34
|
const f2 = await fileSizeAndTime(outputFile);
|
|
35
35
|
|
|
@@ -40,7 +40,9 @@ exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThe
|
|
|
40
40
|
// when it's isMobile, need to update env and configs => no configs as mobile app fetches it from api
|
|
41
41
|
if (!f2 || f2.mtime.getTime() !== chgTime || isMobile) {
|
|
42
42
|
const inHtml = await fs.readFile(htmlFile, 'utf-8');
|
|
43
|
-
let outStr = inHtml
|
|
43
|
+
let outStr = inHtml
|
|
44
|
+
.replace(/\{hash\}/gi, new Date().getTime().toString(36))
|
|
45
|
+
.replace(/\{SUBDIR\}/gi, outdirSub && outdirSub !== '/' ? outdirSub : '');
|
|
44
46
|
if (isMobile) {
|
|
45
47
|
// const outStr = inHtml.replace(/{hash}/gi, new Date().getTime().toString(36)).replace('\<\!--META-ENV--\>', JSON.stringify(envWeb));
|
|
46
48
|
// env is replaced here for the mobile app. And the env is replaced again for the web app at each startup
|
package/dev/markdown-build.js
CHANGED
|
@@ -9,7 +9,10 @@ const slugify = (text) =>
|
|
|
9
9
|
.replace(/[^\w\u4e00-\u9fa5]+/g, '-')
|
|
10
10
|
.replace(/^-+|-+$/g, '');
|
|
11
11
|
|
|
12
|
-
const cleanMarkdown = (text) =>
|
|
12
|
+
const cleanMarkdown = (text) =>
|
|
13
|
+
text
|
|
14
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // remove links: [text](url) -> text
|
|
15
|
+
.replace(/[`*?^]/g, ''); // remove other markdown symbols
|
|
13
16
|
|
|
14
17
|
marked.use({
|
|
15
18
|
renderer: {
|
|
@@ -22,9 +25,7 @@ marked.use({
|
|
|
22
25
|
},
|
|
23
26
|
});
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
function processFile(filePath, indir, outdir, relativePath) {
|
|
28
|
+
function processFile(builtFiles, filePath, indir, outdir, relativePath) {
|
|
28
29
|
if (builtFiles.has(filePath)) return builtFiles.get(filePath);
|
|
29
30
|
|
|
30
31
|
// Mark as processing to handle potential cycles (though minimal risk with hierarchy)
|
|
@@ -32,8 +33,7 @@ function processFile(filePath, indir, outdir, relativePath) {
|
|
|
32
33
|
builtFiles.set(filePath, null);
|
|
33
34
|
|
|
34
35
|
if (!fs.existsSync(filePath)) {
|
|
35
|
-
|
|
36
|
-
return null;
|
|
36
|
+
throw new Error(`File not found: ${filePath}`);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const contentRaw = fs.readFileSync(filePath, 'utf8');
|
|
@@ -90,7 +90,7 @@ function processFile(filePath, indir, outdir, relativePath) {
|
|
|
90
90
|
const subAbs = path.join(indir, subRelative);
|
|
91
91
|
|
|
92
92
|
// Recursively build the submenu index
|
|
93
|
-
const subFileInfo = processFile(subAbs, indir, outdir, subRelative);
|
|
93
|
+
const subFileInfo = processFile(builtFiles, subAbs, indir, outdir, subRelative);
|
|
94
94
|
|
|
95
95
|
if (subFileInfo && subFileInfo.data && subFileInfo.data.sidebar) {
|
|
96
96
|
if (subFileInfo.data.title) {
|
|
@@ -120,7 +120,7 @@ function processFile(filePath, indir, outdir, relativePath) {
|
|
|
120
120
|
subRelative += '.md';
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
processFile(subAbs, indir, outdir, subRelative);
|
|
123
|
+
processFile(builtFiles, subAbs, indir, outdir, subRelative);
|
|
124
124
|
result.push(item);
|
|
125
125
|
} else if (typeof item === 'object' && item.items) {
|
|
126
126
|
item.items = expandSidebar(item.items);
|
|
@@ -164,6 +164,8 @@ const markdownProcessOnEnd = async (indir, outdir) => {
|
|
|
164
164
|
try {
|
|
165
165
|
if (!indir || !outdir) return;
|
|
166
166
|
|
|
167
|
+
// 重新变异的时候必须清除缓存,否则会读取到旧的缓存数据
|
|
168
|
+
matter.clearCache();
|
|
167
169
|
const langPath = path.join(indir, 'index.md');
|
|
168
170
|
if (!fs.existsSync(langPath)) {
|
|
169
171
|
console.warn(`[dev-markdown] No index.md found in ${indir}`);
|
|
@@ -171,7 +173,7 @@ const markdownProcessOnEnd = async (indir, outdir) => {
|
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
const langContent = fs.readFileSync(langPath, 'utf8');
|
|
174
|
-
const { data
|
|
176
|
+
const { data } = matter(langContent);
|
|
175
177
|
const lang = data.lang;
|
|
176
178
|
if (!lang || lang.length < 1 || !lang[0].id) {
|
|
177
179
|
console.warn(`[dev-markdown] No lang found in ${langPath}`);
|
|
@@ -183,11 +185,11 @@ const markdownProcessOnEnd = async (indir, outdir) => {
|
|
|
183
185
|
fs.mkdirSync(outdir, { recursive: true });
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
builtFiles
|
|
188
|
+
const builtFiles = new Map();
|
|
187
189
|
|
|
188
190
|
// Process root index.md to provide global config (languages) at '/'
|
|
189
191
|
if (fs.existsSync(langPath)) {
|
|
190
|
-
const rootInfo = processFile(langPath, indir, outdir, 'index.md');
|
|
192
|
+
const rootInfo = processFile(builtFiles, langPath, indir, outdir, 'index.md');
|
|
191
193
|
if (rootInfo) {
|
|
192
194
|
rootInfo.route = '/';
|
|
193
195
|
// Re-key as '/' to ensure it's accessible as markdownConfig['/']
|
|
@@ -200,7 +202,7 @@ const markdownProcessOnEnd = async (indir, outdir) => {
|
|
|
200
202
|
const fullPath = path.join(indir, entry.id, 'index.md');
|
|
201
203
|
if (fs.existsSync(fullPath)) {
|
|
202
204
|
const relativePath = path.join(entry.id, 'index.md');
|
|
203
|
-
processFile(fullPath, indir, outdir, relativePath);
|
|
205
|
+
processFile(builtFiles, fullPath, indir, outdir, relativePath);
|
|
204
206
|
}
|
|
205
207
|
});
|
|
206
208
|
|
package/dev/send-request.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
exports.sendRequest = async (url, waitSeconds) => {
|
|
2
2
|
try {
|
|
3
|
-
// need node 18
|
|
3
|
+
// need node 18+
|
|
4
4
|
const ret = await fetch(url, {
|
|
5
5
|
signal: AbortSignal.timeout(waitSeconds * 1000),
|
|
6
6
|
});
|
|
7
7
|
waitSeconds > 0 && (await new Promise((r) => setTimeout(r, waitSeconds * 1000)));
|
|
8
8
|
return ret;
|
|
9
9
|
} catch (err) {
|
|
10
|
-
console.error(
|
|
10
|
+
console.error(`sendRequest ${url} failed: ${err.message}`);
|
|
11
11
|
}
|
|
12
12
|
};
|
package/package.json
CHANGED
package/src/app/app-start.ts
CHANGED
|
@@ -115,6 +115,7 @@ class AppStart {
|
|
|
115
115
|
const httpsPort = config.httpsPort;
|
|
116
116
|
const sslKeyPath = config.sslKeyPath || '';
|
|
117
117
|
const sslCrtPath = config.sslCrtPath || '';
|
|
118
|
+
const domainCerts = config.domainCerts;
|
|
118
119
|
|
|
119
120
|
console.log(`${process.pid} - Starting Web Server, httpPort: ${httpPort}, httpsPort: ${httpsPort}`);
|
|
120
121
|
// for dev to refresh the FE or stop the server
|
|
@@ -123,7 +124,7 @@ class AppStart {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
const httpServer = httpPort && this.webServer!.startHttp(httpPort, bindIp);
|
|
126
|
-
const heepsServer = httpsPort && this.webServer!.startHttps(httpsPort, bindIp, sslKeyPath, sslCrtPath);
|
|
127
|
+
const heepsServer = httpsPort && this.webServer!.startHttps(httpsPort, bindIp, sslKeyPath, sslCrtPath, domainCerts);
|
|
127
128
|
|
|
128
129
|
process.on('SIGTERM', () => {
|
|
129
130
|
console.log(`${process.pid} - Worker closing servers...`);
|
package/src/app/web-server.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { WebListener } from './web-listener';
|
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as http from 'http';
|
|
6
6
|
import * as https from 'https';
|
|
7
|
+
import * as tls from 'tls';
|
|
7
8
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
8
9
|
import { Duplex } from 'stream';
|
|
9
10
|
import { WebProcessor } from './web-processor';
|
|
@@ -58,7 +59,21 @@ export class WebServer {
|
|
|
58
59
|
|
|
59
60
|
// multi domain https hosting
|
|
60
61
|
// https://stackoverflow.com/questions/38162077/expressjs-multi-domain-https-hosting
|
|
61
|
-
|
|
62
|
+
|
|
63
|
+
// domainCerts = {
|
|
64
|
+
// 'example.com': {
|
|
65
|
+
// key: fs.readFileSync(sslKeyPath, 'utf8'),
|
|
66
|
+
// cert: fs.readFileSync(sslCrtPath, 'utf8'),
|
|
67
|
+
// },
|
|
68
|
+
// };
|
|
69
|
+
startHttps(
|
|
70
|
+
httpsPort: number,
|
|
71
|
+
bindIp?: string,
|
|
72
|
+
sslKeyPath?: string,
|
|
73
|
+
sslCrtPath?: string,
|
|
74
|
+
domainCerts?: Record<string, { key: string; cert: string }>,
|
|
75
|
+
timeout?: number
|
|
76
|
+
) {
|
|
62
77
|
const httpsOptions: ServerOptions = {};
|
|
63
78
|
if (sslKeyPath && fs.existsSync(sslKeyPath) && sslCrtPath && fs.existsSync(sslCrtPath)) {
|
|
64
79
|
logger.info('Load site ssl certificate.');
|
|
@@ -66,16 +81,55 @@ export class WebServer {
|
|
|
66
81
|
// Even though that set won't be used if SNI provides a hostname.
|
|
67
82
|
httpsOptions['key'] = fs.readFileSync(sslKeyPath, 'utf8');
|
|
68
83
|
httpsOptions['cert'] = fs.readFileSync(sslCrtPath, 'utf8');
|
|
69
|
-
// httpsOptions['ca'] = 'pem';
|
|
70
|
-
// httpsOptions['SNICallback'] = function (domain, cb) {
|
|
71
|
-
// if (typeof sites[domain] === "undefined") {
|
|
72
|
-
// cb(new Error("domain not found"), null);
|
|
73
|
-
// console.log("Error: domain not found: " + domain);
|
|
74
84
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
// load domain certificates
|
|
86
|
+
const secureContexts: Record<string, tls.SecureContext> = {};
|
|
87
|
+
if (domainCerts) {
|
|
88
|
+
const start = Date.now();
|
|
89
|
+
let certCount = 0;
|
|
90
|
+
const fileCache = new Map<string, string>();
|
|
91
|
+
|
|
92
|
+
const getFileContent = (path: string) => {
|
|
93
|
+
if (!fileCache.has(path)) {
|
|
94
|
+
fileCache.set(path, fs.readFileSync(path, 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
return fileCache.get(path)!;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
for (const [domain, paths] of Object.entries(domainCerts)) {
|
|
100
|
+
if (paths.key && paths.cert && fs.existsSync(paths.key) && fs.existsSync(paths.cert)) {
|
|
101
|
+
try {
|
|
102
|
+
const key = getFileContent(paths.key);
|
|
103
|
+
const cert = getFileContent(paths.cert);
|
|
104
|
+
secureContexts[domain] = tls.createSecureContext({ key, cert });
|
|
105
|
+
certCount++;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
logger.error(`Failed to load cert for ${domain}`, err as any);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
logger.info(`Loaded ${certCount} domain scopes in ${Date.now() - start}ms`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
httpsOptions.SNICallback = (domain, cb) => {
|
|
115
|
+
let ctx: tls.SecureContext | undefined = secureContexts[domain];
|
|
116
|
+
if (!ctx) {
|
|
117
|
+
// find the closest domain
|
|
118
|
+
const parts = domain.split('.');
|
|
119
|
+
while (parts.length > 0 && !ctx) {
|
|
120
|
+
parts.shift();
|
|
121
|
+
const parent = parts.join('.');
|
|
122
|
+
if (parent) ctx = secureContexts[parent];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Fallback or exact
|
|
126
|
+
if (ctx) {
|
|
127
|
+
cb(null, ctx);
|
|
128
|
+
} else {
|
|
129
|
+
// Fallback to default (options.key/cert)
|
|
130
|
+
cb(null, undefined);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
79
133
|
} else {
|
|
80
134
|
logger.warn(
|
|
81
135
|
`Ssl certificate is not defined or doesn't exist, key file: ${sslKeyPath}, certificate file: ${sslCrtPath}`
|