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.
@@ -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.replace(/{hash}/gi, new Date().getTime().toString(36));
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
@@ -9,7 +9,10 @@ const slugify = (text) =>
9
9
  .replace(/[^\w\u4e00-\u9fa5]+/g, '-')
10
10
  .replace(/^-+|-+$/g, '');
11
11
 
12
- const cleanMarkdown = (text) => text.replace(/[`*?^]/g, '');
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
- const builtFiles = new Map(); // path -> { route, importPath, data, headings }
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
- console.warn(`File not found: ${filePath}`);
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, content: markdown } = matter(langContent);
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.clear();
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
 
@@ -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('sendRequest error: ', err);
10
+ console.error(`sendRequest ${url} failed: ${err.message}`);
11
11
  }
12
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.api",
3
- "version": "1.1.65",
3
+ "version": "1.1.67",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -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...`);
@@ -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
- startHttps(httpsPort: number, bindIp?: string, sslKeyPath?: string, sslCrtPath?: string, timeout?: number) {
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
- // } else {
76
- // cb(null, sites[domain].context);
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}`
@@ -7,6 +7,7 @@ export type InitStartProps = {
7
7
  httpsPort: number;
8
8
  sslKeyPath: string;
9
9
  sslCrtPath: string;
10
+ domainCerts: Record<string, { key: string; cert: string }>;
10
11
  };
11
12
 
12
13
  export type AppStartProps = {