alemonjs 2.1.83-alpha.6 → 2.1.83-alpha.8
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/lib/application/runtime/http/routers/hello.html.d.ts +2 -0
- package/lib/application/runtime/http/routers/hello.html.js +387 -28
- package/lib/application/runtime/http/routers/router.js +120 -66
- package/lib/application/runtime/http/routers/utils.js +1 -1
- package/lib/application/runtime/load-modules/loadChild.js +11 -2
- package/package.json +1 -1
|
@@ -1,31 +1,390 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
const escapeHtml = (value) => String(value)
|
|
2
|
+
.replace(/&/g, '&')
|
|
3
|
+
.replace(/</g, '<')
|
|
4
|
+
.replace(/>/g, '>')
|
|
5
|
+
.replace(/"/g, '"')
|
|
6
|
+
.replace(/'/g, ''');
|
|
7
|
+
const appHref = (app) => {
|
|
8
|
+
return app.kind === 'main' ? '/app' : `/apps/${app.name}`;
|
|
9
|
+
};
|
|
10
|
+
const appTags = (app) => {
|
|
11
|
+
const tags = [app.kind === 'main' ? '主应用' : '插件'];
|
|
12
|
+
if (app.capabilities.web) {
|
|
13
|
+
tags.push('页面');
|
|
14
|
+
}
|
|
15
|
+
if (app.capabilities.httpApi) {
|
|
16
|
+
tags.push('接口');
|
|
17
|
+
}
|
|
18
|
+
if (app.capabilities.event) {
|
|
19
|
+
tags.push('事件');
|
|
20
|
+
}
|
|
21
|
+
if (app.capabilities.expose) {
|
|
22
|
+
tags.push('Expose');
|
|
23
|
+
}
|
|
24
|
+
return tags;
|
|
25
|
+
};
|
|
26
|
+
const renderCard = (app) => {
|
|
27
|
+
const href = appHref(app);
|
|
28
|
+
const tags = appTags(app)
|
|
29
|
+
.map(tag => `<span class="app-tag">${escapeHtml(tag)}</span>`)
|
|
30
|
+
.join('');
|
|
31
|
+
const desc = app.kind === 'main'
|
|
32
|
+
? '访问主应用页面、接口与默认资源。'
|
|
33
|
+
: '访问插件页面、接口与公开入口。';
|
|
34
|
+
return `
|
|
35
|
+
<a
|
|
36
|
+
class="app-card"
|
|
37
|
+
href="${escapeHtml(href)}"
|
|
38
|
+
data-app-id="${escapeHtml(app.name)}"
|
|
39
|
+
data-app-kind="${escapeHtml(app.kind)}"
|
|
40
|
+
data-app-href="${escapeHtml(href)}"
|
|
41
|
+
>
|
|
42
|
+
<div class="app-card__top">
|
|
43
|
+
<div>
|
|
44
|
+
<p class="app-eyebrow">${app.kind === 'main' ? 'Main App' : 'Plugin App'}</p>
|
|
45
|
+
<h2 class="app-title">${escapeHtml(app.name)}</h2>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="app-rank">
|
|
48
|
+
<span class="app-rank__label">热度</span>
|
|
49
|
+
<span class="app-rank__value" data-click-count>0</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<p class="app-desc">${escapeHtml(desc)}</p>
|
|
53
|
+
<div class="app-tags">${tags}</div>
|
|
54
|
+
<div class="app-card__bottom">
|
|
55
|
+
<span class="app-link">${escapeHtml(href)}</span>
|
|
56
|
+
<span class="app-action">进入</span>
|
|
57
|
+
</div>
|
|
58
|
+
</a>
|
|
59
|
+
`;
|
|
60
|
+
};
|
|
61
|
+
const renderEmpty = () => {
|
|
62
|
+
return `
|
|
63
|
+
<div class="empty-state">
|
|
64
|
+
<p class="empty-state__title">当前没有可展示的应用。</p>
|
|
65
|
+
<p class="empty-state__desc">请先加载主应用或插件,再刷新本页查看入口。</p>
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
};
|
|
69
|
+
const renderHelloHtml = (apps) => {
|
|
70
|
+
const visibleApps = apps
|
|
71
|
+
.filter(app => app.enabled && app.status === 'ready' && (app.capabilities.web || app.capabilities.httpApi))
|
|
72
|
+
.sort((left, right) => {
|
|
73
|
+
if (left.kind === 'main' && right.kind !== 'main') {
|
|
74
|
+
return -1;
|
|
75
|
+
}
|
|
76
|
+
if (left.kind !== 'main' && right.kind === 'main') {
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
return left.name.localeCompare(right.name);
|
|
80
|
+
});
|
|
81
|
+
const cards = visibleApps.length ? visibleApps.map(renderCard).join('') : renderEmpty();
|
|
82
|
+
const payload = JSON.stringify(visibleApps.map(app => ({
|
|
83
|
+
id: app.name,
|
|
84
|
+
href: appHref(app),
|
|
85
|
+
kind: app.kind
|
|
86
|
+
})));
|
|
87
|
+
return `<!DOCTYPE html>
|
|
88
|
+
<html lang="zh-CN">
|
|
89
|
+
<head>
|
|
90
|
+
<meta charset="utf-8" />
|
|
91
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
92
|
+
<title>ALemonJS 应用入口</title>
|
|
93
|
+
<style>
|
|
94
|
+
:root {
|
|
95
|
+
--bg: #f3efe5;
|
|
96
|
+
--panel: rgba(255, 251, 245, 0.88);
|
|
97
|
+
--panel-strong: #fffaf2;
|
|
98
|
+
--text: #17212b;
|
|
99
|
+
--muted: #6a7684;
|
|
100
|
+
--line: rgba(23, 33, 43, 0.08);
|
|
101
|
+
--accent: #d96c28;
|
|
102
|
+
--accent-strong: #a54a13;
|
|
103
|
+
--shadow: 0 24px 60px rgba(47, 35, 20, 0.12);
|
|
104
|
+
}
|
|
105
|
+
* { box-sizing: border-box; }
|
|
106
|
+
html, body { margin: 0; min-height: 100%; }
|
|
6
107
|
body {
|
|
7
|
-
|
|
108
|
+
font-family: "SF Pro Display", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
109
|
+
color: var(--text);
|
|
110
|
+
background:
|
|
111
|
+
radial-gradient(circle at top left, rgba(217, 108, 40, 0.18), transparent 28%),
|
|
112
|
+
radial-gradient(circle at right center, rgba(78, 140, 124, 0.14), transparent 26%),
|
|
113
|
+
linear-gradient(180deg, #f8f4ec 0%, var(--bg) 100%);
|
|
114
|
+
}
|
|
115
|
+
.page {
|
|
116
|
+
width: min(1680px, calc(100vw - 32px));
|
|
8
117
|
margin: 0 auto;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
118
|
+
padding: clamp(28px, 4vw, 56px) 0 64px;
|
|
119
|
+
}
|
|
120
|
+
.hero {
|
|
121
|
+
position: relative;
|
|
122
|
+
overflow: hidden;
|
|
123
|
+
padding: clamp(28px, 4vw, 56px);
|
|
124
|
+
border-radius: 32px;
|
|
125
|
+
background: linear-gradient(135deg, rgba(255, 250, 242, 0.95), rgba(255, 244, 231, 0.88));
|
|
126
|
+
border: 1px solid rgba(255, 255, 255, 0.72);
|
|
127
|
+
box-shadow: var(--shadow);
|
|
128
|
+
}
|
|
129
|
+
.hero::after {
|
|
130
|
+
content: "";
|
|
131
|
+
position: absolute;
|
|
132
|
+
inset: auto -8% -48% auto;
|
|
133
|
+
width: min(38vw, 520px);
|
|
134
|
+
aspect-ratio: 1;
|
|
135
|
+
border-radius: 999px;
|
|
136
|
+
background: radial-gradient(circle, rgba(217, 108, 40, 0.18), transparent 62%);
|
|
137
|
+
pointer-events: none;
|
|
138
|
+
}
|
|
139
|
+
.hero-kicker {
|
|
140
|
+
margin: 0 0 12px;
|
|
141
|
+
font-size: clamp(14px, 1.2vw, 18px);
|
|
142
|
+
letter-spacing: 0.18em;
|
|
143
|
+
text-transform: uppercase;
|
|
144
|
+
color: var(--accent-strong);
|
|
145
|
+
}
|
|
146
|
+
.hero-title {
|
|
147
|
+
margin: 0;
|
|
148
|
+
max-width: 12ch;
|
|
149
|
+
font-size: clamp(40px, 7vw, 86px);
|
|
150
|
+
line-height: 0.95;
|
|
151
|
+
letter-spacing: -0.04em;
|
|
152
|
+
}
|
|
153
|
+
.hero-desc {
|
|
154
|
+
margin: 18px 0 0;
|
|
155
|
+
max-width: 56rem;
|
|
156
|
+
font-size: clamp(18px, 2vw, 28px);
|
|
157
|
+
line-height: 1.6;
|
|
158
|
+
color: var(--muted);
|
|
159
|
+
}
|
|
160
|
+
.hero-meta {
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-wrap: wrap;
|
|
163
|
+
gap: 14px;
|
|
164
|
+
margin-top: 28px;
|
|
165
|
+
}
|
|
166
|
+
.hero-pill {
|
|
167
|
+
display: inline-flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: 10px;
|
|
170
|
+
padding: 12px 18px;
|
|
171
|
+
border-radius: 999px;
|
|
172
|
+
background: rgba(255, 255, 255, 0.78);
|
|
173
|
+
border: 1px solid rgba(23, 33, 43, 0.08);
|
|
174
|
+
font-size: clamp(14px, 1.4vw, 18px);
|
|
175
|
+
}
|
|
176
|
+
.hero-pill strong {
|
|
177
|
+
color: var(--accent-strong);
|
|
178
|
+
}
|
|
179
|
+
.section-head {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: end;
|
|
182
|
+
justify-content: space-between;
|
|
183
|
+
gap: 20px;
|
|
184
|
+
margin: 28px 0 18px;
|
|
185
|
+
padding: 0 6px;
|
|
186
|
+
}
|
|
187
|
+
.section-title {
|
|
188
|
+
margin: 0;
|
|
189
|
+
font-size: clamp(28px, 3vw, 42px);
|
|
190
|
+
}
|
|
191
|
+
.section-note {
|
|
192
|
+
margin: 0;
|
|
193
|
+
font-size: clamp(14px, 1.3vw, 18px);
|
|
194
|
+
color: var(--muted);
|
|
195
|
+
}
|
|
196
|
+
.app-grid {
|
|
197
|
+
display: grid;
|
|
198
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
|
|
199
|
+
gap: 18px;
|
|
200
|
+
}
|
|
201
|
+
.app-card {
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-direction: column;
|
|
204
|
+
gap: 18px;
|
|
205
|
+
min-height: 280px;
|
|
206
|
+
padding: 24px;
|
|
207
|
+
border-radius: 28px;
|
|
208
|
+
text-decoration: none;
|
|
209
|
+
color: inherit;
|
|
210
|
+
background: var(--panel);
|
|
211
|
+
border: 1px solid var(--line);
|
|
212
|
+
box-shadow: 0 14px 40px rgba(30, 37, 44, 0.08);
|
|
213
|
+
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
|
214
|
+
}
|
|
215
|
+
.app-card:hover {
|
|
216
|
+
transform: translateY(-4px);
|
|
217
|
+
border-color: rgba(217, 108, 40, 0.28);
|
|
218
|
+
box-shadow: 0 24px 50px rgba(30, 37, 44, 0.12);
|
|
219
|
+
}
|
|
220
|
+
.app-card__top,
|
|
221
|
+
.app-card__bottom {
|
|
222
|
+
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
justify-content: space-between;
|
|
225
|
+
gap: 16px;
|
|
226
|
+
}
|
|
227
|
+
.app-eyebrow {
|
|
228
|
+
margin: 0 0 8px;
|
|
229
|
+
font-size: 13px;
|
|
230
|
+
letter-spacing: 0.16em;
|
|
231
|
+
text-transform: uppercase;
|
|
232
|
+
color: var(--accent-strong);
|
|
233
|
+
}
|
|
234
|
+
.app-title {
|
|
235
|
+
margin: 0;
|
|
236
|
+
font-size: clamp(28px, 2.8vw, 38px);
|
|
237
|
+
line-height: 1;
|
|
238
|
+
}
|
|
239
|
+
.app-desc {
|
|
240
|
+
margin: 0;
|
|
241
|
+
font-size: clamp(16px, 1.4vw, 20px);
|
|
242
|
+
line-height: 1.7;
|
|
243
|
+
color: var(--muted);
|
|
244
|
+
}
|
|
245
|
+
.app-tags {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-wrap: wrap;
|
|
248
|
+
gap: 10px;
|
|
249
|
+
}
|
|
250
|
+
.app-tag {
|
|
251
|
+
padding: 8px 12px;
|
|
252
|
+
border-radius: 999px;
|
|
253
|
+
background: rgba(217, 108, 40, 0.1);
|
|
254
|
+
color: var(--accent-strong);
|
|
255
|
+
font-size: 14px;
|
|
256
|
+
}
|
|
257
|
+
.app-rank {
|
|
258
|
+
min-width: 84px;
|
|
259
|
+
text-align: right;
|
|
260
|
+
}
|
|
261
|
+
.app-rank__label,
|
|
262
|
+
.app-link {
|
|
263
|
+
display: block;
|
|
264
|
+
font-size: 13px;
|
|
265
|
+
color: var(--muted);
|
|
266
|
+
}
|
|
267
|
+
.app-rank__value {
|
|
268
|
+
font-size: clamp(24px, 2vw, 30px);
|
|
269
|
+
font-weight: 700;
|
|
270
|
+
}
|
|
271
|
+
.app-action {
|
|
272
|
+
padding: 12px 18px;
|
|
273
|
+
border-radius: 999px;
|
|
274
|
+
background: #1d2d38;
|
|
275
|
+
color: #fff;
|
|
276
|
+
font-size: 15px;
|
|
277
|
+
}
|
|
278
|
+
.empty-state {
|
|
279
|
+
padding: 32px;
|
|
280
|
+
border-radius: 28px;
|
|
281
|
+
background: var(--panel-strong);
|
|
282
|
+
border: 1px dashed rgba(23, 33, 43, 0.16);
|
|
283
|
+
}
|
|
284
|
+
.empty-state__title {
|
|
285
|
+
margin: 0;
|
|
286
|
+
font-size: 24px;
|
|
287
|
+
}
|
|
288
|
+
.empty-state__desc {
|
|
289
|
+
margin: 10px 0 0;
|
|
290
|
+
color: var(--muted);
|
|
291
|
+
font-size: 16px;
|
|
292
|
+
}
|
|
293
|
+
@media (max-width: 820px) {
|
|
294
|
+
.page { width: min(100vw - 20px, 100%); padding-top: 18px; }
|
|
295
|
+
.hero { border-radius: 24px; padding: 22px; }
|
|
296
|
+
.section-head { align-items: start; flex-direction: column; }
|
|
297
|
+
.app-card { min-height: 0; padding: 20px; border-radius: 22px; }
|
|
298
|
+
.app-card__top,
|
|
299
|
+
.app-card__bottom { align-items: start; flex-direction: column; }
|
|
300
|
+
.app-rank { text-align: left; min-width: 0; }
|
|
301
|
+
}
|
|
302
|
+
</style>
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
<main class="page">
|
|
306
|
+
<section class="hero">
|
|
307
|
+
<p class="hero-kicker">ALemonJS Launchpad</p>
|
|
308
|
+
<h1 class="hero-title">欢迎使用阿柠檬机器人应用服务</h1>
|
|
309
|
+
<p class="hero-desc">这里会自动列出当前已准备好的主应用与插件。</p>
|
|
310
|
+
<div class="hero-meta">
|
|
311
|
+
<span class="hero-pill"><strong>${visibleApps.length}</strong> 个可访问入口</span>
|
|
312
|
+
<span class="hero-pill">主应用统一在 <strong>/app</strong></span>
|
|
313
|
+
<span class="hero-pill">插件统一在 <strong>/apps/<name></strong></span>
|
|
314
|
+
</div>
|
|
315
|
+
</section>
|
|
316
|
+
<section>
|
|
317
|
+
<div class="section-head">
|
|
318
|
+
<div>
|
|
319
|
+
<h2 class="section-title">应用列表</h2>
|
|
320
|
+
<p class="section-note">点击次数越高,卡片排序越靠前。排序记录仅保存在当前浏览器本地。</p>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
<div class="app-grid" id="app-grid">${cards}</div>
|
|
324
|
+
</section>
|
|
325
|
+
</main>
|
|
326
|
+
<script>
|
|
327
|
+
(() => {
|
|
328
|
+
const storageKey = 'alemonjs:launchpad:clicks';
|
|
329
|
+
const apps = ${payload};
|
|
330
|
+
const grid = document.getElementById('app-grid');
|
|
331
|
+
if (!grid) return;
|
|
332
|
+
|
|
333
|
+
const readClicks = () => {
|
|
334
|
+
try {
|
|
335
|
+
return JSON.parse(localStorage.getItem(storageKey) || '{}');
|
|
336
|
+
} catch {
|
|
337
|
+
return {};
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const writeClicks = (value) => {
|
|
342
|
+
localStorage.setItem(storageKey, JSON.stringify(value));
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const scoreOf = (id, clicks) => Number(clicks[id] || 0);
|
|
346
|
+
const clicks = readClicks();
|
|
347
|
+
const cards = Array.from(grid.querySelectorAll('[data-app-id]'));
|
|
348
|
+
|
|
349
|
+
const refreshCounts = () => {
|
|
350
|
+
cards.forEach((card) => {
|
|
351
|
+
const id = card.getAttribute('data-app-id') || '';
|
|
352
|
+
const node = card.querySelector('[data-click-count]');
|
|
353
|
+
if (node) {
|
|
354
|
+
node.textContent = String(scoreOf(id, clicks));
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const sortCards = () => {
|
|
360
|
+
cards
|
|
361
|
+
.sort((left, right) => {
|
|
362
|
+
const leftId = left.getAttribute('data-app-id') || '';
|
|
363
|
+
const rightId = right.getAttribute('data-app-id') || '';
|
|
364
|
+
const diff = scoreOf(rightId, clicks) - scoreOf(leftId, clicks);
|
|
365
|
+
if (diff !== 0) return diff;
|
|
366
|
+
return leftId.localeCompare(rightId);
|
|
367
|
+
})
|
|
368
|
+
.forEach(card => grid.appendChild(card));
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
cards.forEach((card) => {
|
|
372
|
+
card.addEventListener('click', () => {
|
|
373
|
+
const id = card.getAttribute('data-app-id') || '';
|
|
374
|
+
clicks[id] = scoreOf(id, clicks) + 1;
|
|
375
|
+
writeClicks(clicks);
|
|
376
|
+
refreshCounts();
|
|
377
|
+
sortCards();
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
refreshCounts();
|
|
382
|
+
sortCards();
|
|
383
|
+
})();
|
|
384
|
+
</script>
|
|
385
|
+
</body>
|
|
386
|
+
</html>`;
|
|
387
|
+
};
|
|
388
|
+
var hello_html = renderHelloHtml([]);
|
|
30
389
|
|
|
31
|
-
export {
|
|
390
|
+
export { hello_html as default, renderHelloHtml };
|
|
@@ -2,7 +2,7 @@ import KoaRouter from 'koa-router';
|
|
|
2
2
|
import fs__default, { existsSync } from 'fs';
|
|
3
3
|
import path__default, { join, dirname } from 'path';
|
|
4
4
|
import mime from 'mime-types';
|
|
5
|
-
import
|
|
5
|
+
import { renderHelloHtml } from './hello.html.js';
|
|
6
6
|
import { safePath, getModuelFile, formatPath, isValidPackageName } from './utils.js';
|
|
7
7
|
import { collectMiddlewares, runMiddlewares } from './middleware.js';
|
|
8
8
|
import module$1 from 'module';
|
|
@@ -16,7 +16,7 @@ import 'os';
|
|
|
16
16
|
import 'flatted';
|
|
17
17
|
import '../../../../common/cbp/runtime.js';
|
|
18
18
|
import 'ws';
|
|
19
|
-
import { listRuntimeApps, getRuntimeApp, toRuntimeAppSnapshot, listRuntimeAppKoaRouters, hasRuntimeAppCapability, getRuntimeAppKoaRouters } from '../../store.js';
|
|
19
|
+
import { listRuntimeApps, getRuntimeApp, toRuntimeAppSnapshot, listRuntimeAppKoaRouters, hasRuntimeAppCapability, getChildrenApp, getRuntimeAppKoaRouters } from '../../store.js';
|
|
20
20
|
import { dispatchHttpError } from '../../lifecycle-callbacks.js';
|
|
21
21
|
|
|
22
22
|
const initRequire = () => { };
|
|
@@ -38,10 +38,46 @@ const resolvePackageRoot = (startDir) => {
|
|
|
38
38
|
const readWebRootConfig = (packageRoot) => {
|
|
39
39
|
const packageJsonPath = path__default.join(packageRoot, 'package.json');
|
|
40
40
|
if (!existsSync(packageJsonPath)) {
|
|
41
|
+
if (existsSync(path__default.join(packageRoot, 'dist', 'index.html'))) {
|
|
42
|
+
return 'dist';
|
|
43
|
+
}
|
|
41
44
|
return '';
|
|
42
45
|
}
|
|
43
46
|
const pkg = require$1(packageJsonPath) ?? {};
|
|
44
|
-
|
|
47
|
+
const configuredRoot = pkg?.alemonjs?.web?.root ?? '';
|
|
48
|
+
if (typeof configuredRoot === 'string' && configuredRoot.trim()) {
|
|
49
|
+
return configuredRoot;
|
|
50
|
+
}
|
|
51
|
+
if (existsSync(path__default.join(packageRoot, 'dist', 'index.html'))) {
|
|
52
|
+
return 'dist';
|
|
53
|
+
}
|
|
54
|
+
return '';
|
|
55
|
+
};
|
|
56
|
+
const matchBasePath = (requestPath, basePath) => {
|
|
57
|
+
if (!basePath) {
|
|
58
|
+
return requestPath;
|
|
59
|
+
}
|
|
60
|
+
if (requestPath === basePath) {
|
|
61
|
+
return '/';
|
|
62
|
+
}
|
|
63
|
+
if (requestPath.startsWith(`${basePath}/`)) {
|
|
64
|
+
return requestPath.slice(basePath.length) || '/';
|
|
65
|
+
}
|
|
66
|
+
return '';
|
|
67
|
+
};
|
|
68
|
+
const rewriteCtxPath = async (ctx, nextPath, handler) => {
|
|
69
|
+
const search = ctx.querystring ? `?${ctx.querystring}` : '';
|
|
70
|
+
const originalUrl = ctx.url;
|
|
71
|
+
const originalReqUrl = ctx.req.url;
|
|
72
|
+
ctx.url = `${nextPath}${search}`;
|
|
73
|
+
ctx.req.url = `${nextPath}${search}`;
|
|
74
|
+
try {
|
|
75
|
+
await handler();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
ctx.url = originalUrl;
|
|
79
|
+
ctx.req.url = originalReqUrl;
|
|
80
|
+
}
|
|
45
81
|
};
|
|
46
82
|
const denyRuntimeAppAccess = (ctx, appName, capability) => {
|
|
47
83
|
const runtimeApp = getRuntimeApp(appName);
|
|
@@ -105,63 +141,83 @@ const denyRuntimeAppAccess = (ctx, appName, capability) => {
|
|
|
105
141
|
};
|
|
106
142
|
const dispatchRegisteredKoaRouters = async (ctx) => {
|
|
107
143
|
const registeredRouters = listRuntimeAppKoaRouters();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
144
|
+
const candidates = new Set(registeredRouters.map(item => item.name));
|
|
145
|
+
const runtimeApps = listRuntimeApps();
|
|
146
|
+
runtimeApps.forEach(item => {
|
|
147
|
+
if (item.capabilities?.httpApi) {
|
|
148
|
+
candidates.add(item.name);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
for (const appName of candidates) {
|
|
152
|
+
const runtimeApp = getRuntimeApp(appName);
|
|
153
|
+
if (!runtimeApp || !runtimeApp.enabled || runtimeApp.status !== 'ready' || !hasRuntimeAppCapability(appName, 'httpApi')) {
|
|
111
154
|
continue;
|
|
112
155
|
}
|
|
113
|
-
const
|
|
156
|
+
const registerRouters = getChildrenApp(appName)?.register?.koaRouter;
|
|
157
|
+
const storedRouters = getRuntimeAppKoaRouters(appName);
|
|
158
|
+
const routers = (storedRouters.length ? storedRouters : Array.isArray(registerRouters) ? registerRouters : registerRouters ? [registerRouters] : []).filter(Boolean);
|
|
159
|
+
const aliasBases = appName === 'main' ? ['', '/app'] : ['', `/apps/${appName}`];
|
|
114
160
|
for (const koaRouter of routers) {
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const beforeStatus = ctx.status;
|
|
119
|
-
const beforeBody = ctx.body;
|
|
120
|
-
const beforeMatchedRoute = ctx._matchedRoute;
|
|
121
|
-
const beforeRouterPath = ctx.routerPath;
|
|
122
|
-
let fallthrough = false;
|
|
123
|
-
await koaRouter.routes()(ctx, async () => {
|
|
124
|
-
fallthrough = true;
|
|
125
|
-
});
|
|
126
|
-
const afterMatched = Array.isArray(matchedContext.matched) ? matchedContext.matched.length : 0;
|
|
127
|
-
const afterMatchedRoute = ctx._matchedRoute;
|
|
128
|
-
const afterRouterPath = ctx.routerPath;
|
|
129
|
-
const handled = afterMatched > beforeMatched
|
|
130
|
-
|| afterMatchedRoute !== beforeMatchedRoute
|
|
131
|
-
|| afterRouterPath !== beforeRouterPath
|
|
132
|
-
|| ctx.status !== beforeStatus
|
|
133
|
-
|| ctx.body !== beforeBody
|
|
134
|
-
|| !fallthrough;
|
|
135
|
-
if (!handled) {
|
|
161
|
+
for (const basePath of aliasBases) {
|
|
162
|
+
const rewrittenPath = matchBasePath(ctx.path, basePath);
|
|
163
|
+
if (!rewrittenPath) {
|
|
136
164
|
continue;
|
|
137
165
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
ctx
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
try {
|
|
167
|
+
const matchedContext = ctx;
|
|
168
|
+
const beforeMatched = Array.isArray(matchedContext.matched) ? matchedContext.matched.length : 0;
|
|
169
|
+
const beforeStatus = ctx.status;
|
|
170
|
+
const beforeBody = ctx.body;
|
|
171
|
+
const beforeMatchedRoute = ctx._matchedRoute;
|
|
172
|
+
const beforeRouterPath = ctx.routerPath;
|
|
173
|
+
let fallthrough = false;
|
|
174
|
+
await rewriteCtxPath(ctx, rewrittenPath, async () => {
|
|
175
|
+
await koaRouter.routes()(ctx, async () => {
|
|
176
|
+
fallthrough = true;
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
const afterMatched = Array.isArray(matchedContext.matched) ? matchedContext.matched.length : 0;
|
|
180
|
+
const afterMatchedRoute = ctx._matchedRoute;
|
|
181
|
+
const afterRouterPath = ctx.routerPath;
|
|
182
|
+
const handled = afterMatched > beforeMatched
|
|
183
|
+
|| afterMatchedRoute !== beforeMatchedRoute
|
|
184
|
+
|| afterRouterPath !== beforeRouterPath
|
|
185
|
+
|| ctx.status !== beforeStatus
|
|
186
|
+
|| ctx.body !== beforeBody
|
|
187
|
+
|| !fallthrough;
|
|
188
|
+
if (!handled) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
await rewriteCtxPath(ctx, rewrittenPath, async () => {
|
|
192
|
+
await koaRouter.allowedMethods()(ctx, async () => { });
|
|
193
|
+
});
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
const handled = await dispatchHttpError({
|
|
198
|
+
ctx,
|
|
199
|
+
error,
|
|
200
|
+
appName,
|
|
201
|
+
path: ctx.path,
|
|
202
|
+
method: ctx.method,
|
|
203
|
+
kind: 'koa-router'
|
|
204
|
+
});
|
|
205
|
+
if (handled) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
logger.warn({
|
|
209
|
+
code: ResultCode.Fail,
|
|
210
|
+
message: `Error request ${ctx.path}:`,
|
|
211
|
+
data: error instanceof Error ? error.message : String(error)
|
|
212
|
+
});
|
|
213
|
+
ctx.status = 500;
|
|
214
|
+
ctx.body = {
|
|
215
|
+
code: 500,
|
|
216
|
+
message: '处理 Koa Router 请求时发生错误。',
|
|
217
|
+
error: error instanceof Error ? error.message : String(error)
|
|
218
|
+
};
|
|
151
219
|
return true;
|
|
152
220
|
}
|
|
153
|
-
logger.warn({
|
|
154
|
-
code: ResultCode.Fail,
|
|
155
|
-
message: `Error request ${ctx.path}:`,
|
|
156
|
-
data: error instanceof Error ? error.message : String(error)
|
|
157
|
-
});
|
|
158
|
-
ctx.status = 500;
|
|
159
|
-
ctx.body = {
|
|
160
|
-
code: 500,
|
|
161
|
-
message: '处理 Koa Router 请求时发生错误。',
|
|
162
|
-
error: error instanceof Error ? error.message : String(error)
|
|
163
|
-
};
|
|
164
|
-
return true;
|
|
165
221
|
}
|
|
166
222
|
}
|
|
167
223
|
}
|
|
@@ -170,7 +226,7 @@ const dispatchRegisteredKoaRouters = async (ctx) => {
|
|
|
170
226
|
router.get('/', ctx => {
|
|
171
227
|
ctx.status = 200;
|
|
172
228
|
ctx.set('Content-Type', 'text/html; charset=utf-8');
|
|
173
|
-
ctx.body =
|
|
229
|
+
ctx.body = renderHelloHtml(listRuntimeApps());
|
|
174
230
|
});
|
|
175
231
|
router.get('api/online', ctx => {
|
|
176
232
|
ctx.status = 200;
|
|
@@ -221,7 +277,7 @@ router.use(async (ctx, next) => {
|
|
|
221
277
|
}
|
|
222
278
|
await next();
|
|
223
279
|
});
|
|
224
|
-
|
|
280
|
+
const handleMainAppRequest = async (ctx) => {
|
|
225
281
|
if (!process.env.input) {
|
|
226
282
|
ctx.status = 400;
|
|
227
283
|
ctx.body = {
|
|
@@ -391,11 +447,11 @@ router.all('app/{*path}', async (ctx) => {
|
|
|
391
447
|
};
|
|
392
448
|
}
|
|
393
449
|
}
|
|
394
|
-
}
|
|
395
|
-
router.all('app',
|
|
396
|
-
|
|
397
|
-
});
|
|
398
|
-
|
|
450
|
+
};
|
|
451
|
+
router.all('app', handleMainAppRequest);
|
|
452
|
+
router.all('app/', handleMainAppRequest);
|
|
453
|
+
router.all('app/{*path}', handleMainAppRequest);
|
|
454
|
+
const handlePluginAppRequest = async (ctx) => {
|
|
399
455
|
const appName = ctx.params.app;
|
|
400
456
|
if (!isValidPackageName(appName)) {
|
|
401
457
|
ctx.status = 400;
|
|
@@ -564,11 +620,9 @@ router.all('apps/:app/{*path}', async (ctx) => {
|
|
|
564
620
|
};
|
|
565
621
|
}
|
|
566
622
|
}
|
|
567
|
-
}
|
|
568
|
-
router.all('apps/:
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
});
|
|
623
|
+
};
|
|
624
|
+
router.all('apps/:app', handlePluginAppRequest);
|
|
625
|
+
router.all('apps/:app/', handlePluginAppRequest);
|
|
626
|
+
router.all('apps/:app/{*path}', handlePluginAppRequest);
|
|
573
627
|
|
|
574
628
|
export { router as default };
|
|
@@ -21,11 +21,20 @@ const resolvePackageRoot = (startDir) => {
|
|
|
21
21
|
}
|
|
22
22
|
return startDir;
|
|
23
23
|
};
|
|
24
|
+
const detectDefaultWebRoot = (packageRoot) => {
|
|
25
|
+
if (existsSync(join(packageRoot, 'dist', 'index.html'))) {
|
|
26
|
+
return 'dist';
|
|
27
|
+
}
|
|
28
|
+
if (existsSync(join(packageRoot, 'index.html'))) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
24
33
|
const detectWebCapability = (startDir) => {
|
|
25
34
|
const packageRoot = resolvePackageRoot(startDir);
|
|
26
35
|
const packageJsonPath = join(packageRoot, 'package.json');
|
|
27
36
|
if (!existsSync(packageJsonPath)) {
|
|
28
|
-
return
|
|
37
|
+
return Boolean(detectDefaultWebRoot(packageRoot));
|
|
29
38
|
}
|
|
30
39
|
try {
|
|
31
40
|
const pkg = require$1(packageJsonPath) ?? {};
|
|
@@ -36,7 +45,7 @@ const detectWebCapability = (startDir) => {
|
|
|
36
45
|
}
|
|
37
46
|
catch {
|
|
38
47
|
}
|
|
39
|
-
return
|
|
48
|
+
return Boolean(detectDefaultWebRoot(packageRoot));
|
|
40
49
|
};
|
|
41
50
|
const loadChildren = async (mainPath, appName) => {
|
|
42
51
|
if (!mainPath || typeof mainPath !== 'string') {
|