alemonjs 2.1.84 → 2.1.85
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/router/dsl.js +1 -1
- package/lib/application/runtime/http/routers/hello.html.js +290 -303
- package/lib/application/runtime/http/routers/router.js +223 -214
- package/lib/application/runtime/http/routers/utils.js +20 -6
- package/lib/common/react.js +22 -3
- package/package.json +1 -1
|
@@ -689,7 +689,7 @@ class Router {
|
|
|
689
689
|
const md = Format.createMarkdown();
|
|
690
690
|
const format = Format.create();
|
|
691
691
|
const routeEntry = (result.eventName && result.matchedPath
|
|
692
|
-
?
|
|
692
|
+
? this.routes.get(result.eventName)?.one.get(result.matchedPath) ?? this.routes.get(result.eventName)?.two.get(result.matchedPath)
|
|
693
693
|
: undefined) ?? undefined;
|
|
694
694
|
const description = formatRouteDescription(routeEntry?.config.description);
|
|
695
695
|
const schemaHints = buildSchemaHints(routeEntry?.config.schema);
|
|
@@ -1,6 +1,218 @@
|
|
|
1
|
+
import { DOCTYPE, renderToString, createElement, Component, Html, Head, Title, Style, Body, Div, P } from '../../../../common/react.js';
|
|
2
|
+
|
|
1
3
|
const escapeHtml = (value) => String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
4
|
+
const styles = `
|
|
5
|
+
:root {
|
|
6
|
+
--bg: #f3efe5;
|
|
7
|
+
--panel: rgba(255, 251, 245, 0.88);
|
|
8
|
+
--panel-strong: #fffaf2;
|
|
9
|
+
--text: #17212b;
|
|
10
|
+
--muted: #6a7684;
|
|
11
|
+
--line: rgba(23, 33, 43, 0.08);
|
|
12
|
+
--accent: #d96c28;
|
|
13
|
+
--accent-strong: #a54a13;
|
|
14
|
+
--shadow: 0 24px 60px rgba(47, 35, 20, 0.12);
|
|
15
|
+
}
|
|
16
|
+
* { box-sizing: border-box; }
|
|
17
|
+
html, body { margin: 0; min-height: 100%; }
|
|
18
|
+
body {
|
|
19
|
+
font-family: "SF Pro Display", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
20
|
+
color: var(--text);
|
|
21
|
+
background:
|
|
22
|
+
radial-gradient(circle at top left, rgba(217, 108, 40, 0.18), transparent 28%),
|
|
23
|
+
radial-gradient(circle at right center, rgba(78, 140, 124, 0.14), transparent 26%),
|
|
24
|
+
linear-gradient(180deg, #f8f4ec 0%, var(--bg) 100%);
|
|
25
|
+
}
|
|
26
|
+
.page {
|
|
27
|
+
width: min(1680px, calc(100vw - 32px));
|
|
28
|
+
margin: 0 auto;
|
|
29
|
+
padding: clamp(28px, 4vw, 56px) 0 64px;
|
|
30
|
+
}
|
|
31
|
+
.hero {
|
|
32
|
+
position: relative;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
padding: clamp(28px, 4vw, 56px);
|
|
35
|
+
border-radius: 32px;
|
|
36
|
+
background: linear-gradient(135deg, rgba(255, 250, 242, 0.95), rgba(255, 244, 231, 0.88));
|
|
37
|
+
border: 1px solid rgba(255, 255, 255, 0.72);
|
|
38
|
+
box-shadow: var(--shadow);
|
|
39
|
+
}
|
|
40
|
+
.hero::after {
|
|
41
|
+
content: "";
|
|
42
|
+
position: absolute;
|
|
43
|
+
inset: auto -8% -48% auto;
|
|
44
|
+
width: min(38vw, 520px);
|
|
45
|
+
aspect-ratio: 1;
|
|
46
|
+
border-radius: 999px;
|
|
47
|
+
background: radial-gradient(circle, rgba(217, 108, 40, 0.18), transparent 62%);
|
|
48
|
+
pointer-events: none;
|
|
49
|
+
}
|
|
50
|
+
.hero-kicker {
|
|
51
|
+
margin: 0 0 12px;
|
|
52
|
+
font-size: clamp(14px, 1.2vw, 18px);
|
|
53
|
+
letter-spacing: 0.18em;
|
|
54
|
+
text-transform: uppercase;
|
|
55
|
+
color: var(--accent-strong);
|
|
56
|
+
}
|
|
57
|
+
.hero-title {
|
|
58
|
+
margin: 0;
|
|
59
|
+
max-width: 12ch;
|
|
60
|
+
font-size: clamp(40px, 7vw, 86px);
|
|
61
|
+
line-height: 0.95;
|
|
62
|
+
letter-spacing: -0.04em;
|
|
63
|
+
}
|
|
64
|
+
.hero-desc {
|
|
65
|
+
margin: 18px 0 0;
|
|
66
|
+
max-width: 56rem;
|
|
67
|
+
font-size: clamp(18px, 2vw, 28px);
|
|
68
|
+
line-height: 1.6;
|
|
69
|
+
color: var(--muted);
|
|
70
|
+
}
|
|
71
|
+
.hero-meta {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-wrap: wrap;
|
|
74
|
+
gap: 14px;
|
|
75
|
+
margin-top: 28px;
|
|
76
|
+
}
|
|
77
|
+
.hero-pill {
|
|
78
|
+
display: inline-flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: 10px;
|
|
81
|
+
padding: 12px 18px;
|
|
82
|
+
border-radius: 999px;
|
|
83
|
+
background: rgba(255, 255, 255, 0.78);
|
|
84
|
+
border: 1px solid rgba(23, 33, 43, 0.08);
|
|
85
|
+
font-size: clamp(14px, 1.4vw, 18px);
|
|
86
|
+
}
|
|
87
|
+
.hero-pill strong {
|
|
88
|
+
color: var(--accent-strong);
|
|
89
|
+
}
|
|
90
|
+
.section-head {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: end;
|
|
93
|
+
justify-content: space-between;
|
|
94
|
+
gap: 20px;
|
|
95
|
+
margin: 28px 0 18px;
|
|
96
|
+
padding: 0 6px;
|
|
97
|
+
}
|
|
98
|
+
.section-title {
|
|
99
|
+
margin: 0;
|
|
100
|
+
font-size: clamp(28px, 3vw, 42px);
|
|
101
|
+
}
|
|
102
|
+
.section-note {
|
|
103
|
+
margin: 0;
|
|
104
|
+
font-size: clamp(14px, 1.3vw, 18px);
|
|
105
|
+
color: var(--muted);
|
|
106
|
+
}
|
|
107
|
+
.app-grid {
|
|
108
|
+
display: grid;
|
|
109
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
|
|
110
|
+
gap: 18px;
|
|
111
|
+
}
|
|
112
|
+
.app-card {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
gap: 18px;
|
|
116
|
+
min-height: 280px;
|
|
117
|
+
padding: 24px;
|
|
118
|
+
border-radius: 28px;
|
|
119
|
+
text-decoration: none;
|
|
120
|
+
color: inherit;
|
|
121
|
+
background: var(--panel);
|
|
122
|
+
border: 1px solid var(--line);
|
|
123
|
+
box-shadow: 0 14px 40px rgba(30, 37, 44, 0.08);
|
|
124
|
+
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
|
125
|
+
}
|
|
126
|
+
.app-card:hover {
|
|
127
|
+
transform: translateY(-4px);
|
|
128
|
+
border-color: rgba(217, 108, 40, 0.28);
|
|
129
|
+
box-shadow: 0 24px 50px rgba(30, 37, 44, 0.12);
|
|
130
|
+
}
|
|
131
|
+
.app-card__top,
|
|
132
|
+
.app-card__bottom {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: space-between;
|
|
136
|
+
gap: 16px;
|
|
137
|
+
}
|
|
138
|
+
.app-eyebrow {
|
|
139
|
+
margin: 0 0 8px;
|
|
140
|
+
font-size: 13px;
|
|
141
|
+
letter-spacing: 0.16em;
|
|
142
|
+
text-transform: uppercase;
|
|
143
|
+
color: var(--accent-strong);
|
|
144
|
+
}
|
|
145
|
+
.app-title {
|
|
146
|
+
margin: 0;
|
|
147
|
+
font-size: clamp(28px, 2.8vw, 38px);
|
|
148
|
+
line-height: 1;
|
|
149
|
+
}
|
|
150
|
+
.app-desc {
|
|
151
|
+
margin: 0;
|
|
152
|
+
font-size: clamp(16px, 1.4vw, 20px);
|
|
153
|
+
line-height: 1.7;
|
|
154
|
+
color: var(--muted);
|
|
155
|
+
}
|
|
156
|
+
.app-tags {
|
|
157
|
+
display: flex;
|
|
158
|
+
flex-wrap: wrap;
|
|
159
|
+
gap: 10px;
|
|
160
|
+
}
|
|
161
|
+
.app-tag {
|
|
162
|
+
padding: 8px 12px;
|
|
163
|
+
border-radius: 999px;
|
|
164
|
+
background: rgba(217, 108, 40, 0.1);
|
|
165
|
+
color: var(--accent-strong);
|
|
166
|
+
font-size: 14px;
|
|
167
|
+
}
|
|
168
|
+
.app-rank {
|
|
169
|
+
min-width: 84px;
|
|
170
|
+
text-align: right;
|
|
171
|
+
}
|
|
172
|
+
.app-rank__label,
|
|
173
|
+
.app-link {
|
|
174
|
+
display: block;
|
|
175
|
+
font-size: 13px;
|
|
176
|
+
color: var(--muted);
|
|
177
|
+
}
|
|
178
|
+
.app-rank__value {
|
|
179
|
+
font-size: clamp(24px, 2vw, 30px);
|
|
180
|
+
font-weight: 700;
|
|
181
|
+
}
|
|
182
|
+
.app-action {
|
|
183
|
+
padding: 12px 18px;
|
|
184
|
+
border-radius: 999px;
|
|
185
|
+
background: #1d2d38;
|
|
186
|
+
color: #fff;
|
|
187
|
+
font-size: 15px;
|
|
188
|
+
}
|
|
189
|
+
.empty-state {
|
|
190
|
+
padding: 32px;
|
|
191
|
+
border-radius: 28px;
|
|
192
|
+
background: var(--panel-strong);
|
|
193
|
+
border: 1px dashed rgba(23, 33, 43, 0.16);
|
|
194
|
+
}
|
|
195
|
+
.empty-state__title {
|
|
196
|
+
margin: 0;
|
|
197
|
+
font-size: 24px;
|
|
198
|
+
}
|
|
199
|
+
.empty-state__desc {
|
|
200
|
+
margin: 10px 0 0;
|
|
201
|
+
color: var(--muted);
|
|
202
|
+
font-size: 16px;
|
|
203
|
+
}
|
|
204
|
+
@media (max-width: 820px) {
|
|
205
|
+
.page { width: min(100vw - 20px, 100%); padding-top: 18px; }
|
|
206
|
+
.hero { border-radius: 24px; padding: 22px; }
|
|
207
|
+
.section-head { align-items: start; flex-direction: column; }
|
|
208
|
+
.app-card { min-height: 0; padding: 20px; border-radius: 22px; }
|
|
209
|
+
.app-card__top,
|
|
210
|
+
.app-card__bottom { align-items: start; flex-direction: column; }
|
|
211
|
+
.app-rank { text-align: left; min-width: 0; }
|
|
212
|
+
}
|
|
213
|
+
`;
|
|
2
214
|
const appHref = (app) => {
|
|
3
|
-
return app.kind === 'main' ? '/app' : `/apps/${app.name}
|
|
215
|
+
return app.kind === 'main' ? '/app/' : `/apps/${app.name}/`;
|
|
4
216
|
};
|
|
5
217
|
const appTags = (app) => {
|
|
6
218
|
const tags = [app.kind === 'main' ? '主应用' : '插件'];
|
|
@@ -18,7 +230,7 @@ const appTags = (app) => {
|
|
|
18
230
|
}
|
|
19
231
|
return tags;
|
|
20
232
|
};
|
|
21
|
-
const
|
|
233
|
+
const renderCardHtml = (app) => {
|
|
22
234
|
const href = appHref(app);
|
|
23
235
|
const tags = appTags(app)
|
|
24
236
|
.map(tag => `<span class="app-tag">${escapeHtml(tag)}</span>`)
|
|
@@ -51,325 +263,100 @@ const renderCard = (app) => {
|
|
|
51
263
|
</a>
|
|
52
264
|
`;
|
|
53
265
|
};
|
|
54
|
-
const
|
|
266
|
+
const renderEmptyHtml = () => {
|
|
55
267
|
return `
|
|
56
268
|
<div class="empty-state">
|
|
57
269
|
<p class="empty-state__title">当前没有可展示的应用。</p>
|
|
58
|
-
<p class="empty-state__desc"
|
|
270
|
+
<p class="empty-state__desc">可能你并启动扩展,或选择的扩展并没有支持WEB应用</p>
|
|
59
271
|
</div>
|
|
60
272
|
`;
|
|
61
273
|
};
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
.filter(app => app.enabled && app.status === 'ready' && (app.capabilities.web || app.capabilities.httpApi))
|
|
65
|
-
.sort((left, right) => {
|
|
66
|
-
if (left.kind === 'main' && right.kind !== 'main') {
|
|
67
|
-
return -1;
|
|
68
|
-
}
|
|
69
|
-
if (left.kind !== 'main' && right.kind === 'main') {
|
|
70
|
-
return 1;
|
|
71
|
-
}
|
|
72
|
-
return left.name.localeCompare(right.name);
|
|
73
|
-
});
|
|
74
|
-
const cards = visibleApps.length ? visibleApps.map(renderCard).join('') : renderEmpty();
|
|
75
|
-
const payload = JSON.stringify(visibleApps.map(app => ({
|
|
274
|
+
const renderLaunchpadScript = (apps) => {
|
|
275
|
+
const payload = JSON.stringify(apps.map(app => ({
|
|
76
276
|
id: app.name,
|
|
77
277
|
href: appHref(app),
|
|
78
278
|
kind: app.kind
|
|
79
279
|
})));
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<style>
|
|
87
|
-
:root {
|
|
88
|
-
--bg: #f3efe5;
|
|
89
|
-
--panel: rgba(255, 251, 245, 0.88);
|
|
90
|
-
--panel-strong: #fffaf2;
|
|
91
|
-
--text: #17212b;
|
|
92
|
-
--muted: #6a7684;
|
|
93
|
-
--line: rgba(23, 33, 43, 0.08);
|
|
94
|
-
--accent: #d96c28;
|
|
95
|
-
--accent-strong: #a54a13;
|
|
96
|
-
--shadow: 0 24px 60px rgba(47, 35, 20, 0.12);
|
|
97
|
-
}
|
|
98
|
-
* { box-sizing: border-box; }
|
|
99
|
-
html, body { margin: 0; min-height: 100%; }
|
|
100
|
-
body {
|
|
101
|
-
font-family: "SF Pro Display", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
102
|
-
color: var(--text);
|
|
103
|
-
background:
|
|
104
|
-
radial-gradient(circle at top left, rgba(217, 108, 40, 0.18), transparent 28%),
|
|
105
|
-
radial-gradient(circle at right center, rgba(78, 140, 124, 0.14), transparent 26%),
|
|
106
|
-
linear-gradient(180deg, #f8f4ec 0%, var(--bg) 100%);
|
|
107
|
-
}
|
|
108
|
-
.page {
|
|
109
|
-
width: min(1680px, calc(100vw - 32px));
|
|
110
|
-
margin: 0 auto;
|
|
111
|
-
padding: clamp(28px, 4vw, 56px) 0 64px;
|
|
112
|
-
}
|
|
113
|
-
.hero {
|
|
114
|
-
position: relative;
|
|
115
|
-
overflow: hidden;
|
|
116
|
-
padding: clamp(28px, 4vw, 56px);
|
|
117
|
-
border-radius: 32px;
|
|
118
|
-
background: linear-gradient(135deg, rgba(255, 250, 242, 0.95), rgba(255, 244, 231, 0.88));
|
|
119
|
-
border: 1px solid rgba(255, 255, 255, 0.72);
|
|
120
|
-
box-shadow: var(--shadow);
|
|
121
|
-
}
|
|
122
|
-
.hero::after {
|
|
123
|
-
content: "";
|
|
124
|
-
position: absolute;
|
|
125
|
-
inset: auto -8% -48% auto;
|
|
126
|
-
width: min(38vw, 520px);
|
|
127
|
-
aspect-ratio: 1;
|
|
128
|
-
border-radius: 999px;
|
|
129
|
-
background: radial-gradient(circle, rgba(217, 108, 40, 0.18), transparent 62%);
|
|
130
|
-
pointer-events: none;
|
|
131
|
-
}
|
|
132
|
-
.hero-kicker {
|
|
133
|
-
margin: 0 0 12px;
|
|
134
|
-
font-size: clamp(14px, 1.2vw, 18px);
|
|
135
|
-
letter-spacing: 0.18em;
|
|
136
|
-
text-transform: uppercase;
|
|
137
|
-
color: var(--accent-strong);
|
|
138
|
-
}
|
|
139
|
-
.hero-title {
|
|
140
|
-
margin: 0;
|
|
141
|
-
max-width: 12ch;
|
|
142
|
-
font-size: clamp(40px, 7vw, 86px);
|
|
143
|
-
line-height: 0.95;
|
|
144
|
-
letter-spacing: -0.04em;
|
|
145
|
-
}
|
|
146
|
-
.hero-desc {
|
|
147
|
-
margin: 18px 0 0;
|
|
148
|
-
max-width: 56rem;
|
|
149
|
-
font-size: clamp(18px, 2vw, 28px);
|
|
150
|
-
line-height: 1.6;
|
|
151
|
-
color: var(--muted);
|
|
152
|
-
}
|
|
153
|
-
.hero-meta {
|
|
154
|
-
display: flex;
|
|
155
|
-
flex-wrap: wrap;
|
|
156
|
-
gap: 14px;
|
|
157
|
-
margin-top: 28px;
|
|
158
|
-
}
|
|
159
|
-
.hero-pill {
|
|
160
|
-
display: inline-flex;
|
|
161
|
-
align-items: center;
|
|
162
|
-
gap: 10px;
|
|
163
|
-
padding: 12px 18px;
|
|
164
|
-
border-radius: 999px;
|
|
165
|
-
background: rgba(255, 255, 255, 0.78);
|
|
166
|
-
border: 1px solid rgba(23, 33, 43, 0.08);
|
|
167
|
-
font-size: clamp(14px, 1.4vw, 18px);
|
|
168
|
-
}
|
|
169
|
-
.hero-pill strong {
|
|
170
|
-
color: var(--accent-strong);
|
|
171
|
-
}
|
|
172
|
-
.section-head {
|
|
173
|
-
display: flex;
|
|
174
|
-
align-items: end;
|
|
175
|
-
justify-content: space-between;
|
|
176
|
-
gap: 20px;
|
|
177
|
-
margin: 28px 0 18px;
|
|
178
|
-
padding: 0 6px;
|
|
179
|
-
}
|
|
180
|
-
.section-title {
|
|
181
|
-
margin: 0;
|
|
182
|
-
font-size: clamp(28px, 3vw, 42px);
|
|
183
|
-
}
|
|
184
|
-
.section-note {
|
|
185
|
-
margin: 0;
|
|
186
|
-
font-size: clamp(14px, 1.3vw, 18px);
|
|
187
|
-
color: var(--muted);
|
|
188
|
-
}
|
|
189
|
-
.app-grid {
|
|
190
|
-
display: grid;
|
|
191
|
-
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
|
|
192
|
-
gap: 18px;
|
|
193
|
-
}
|
|
194
|
-
.app-card {
|
|
195
|
-
display: flex;
|
|
196
|
-
flex-direction: column;
|
|
197
|
-
gap: 18px;
|
|
198
|
-
min-height: 280px;
|
|
199
|
-
padding: 24px;
|
|
200
|
-
border-radius: 28px;
|
|
201
|
-
text-decoration: none;
|
|
202
|
-
color: inherit;
|
|
203
|
-
background: var(--panel);
|
|
204
|
-
border: 1px solid var(--line);
|
|
205
|
-
box-shadow: 0 14px 40px rgba(30, 37, 44, 0.08);
|
|
206
|
-
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
|
207
|
-
}
|
|
208
|
-
.app-card:hover {
|
|
209
|
-
transform: translateY(-4px);
|
|
210
|
-
border-color: rgba(217, 108, 40, 0.28);
|
|
211
|
-
box-shadow: 0 24px 50px rgba(30, 37, 44, 0.12);
|
|
212
|
-
}
|
|
213
|
-
.app-card__top,
|
|
214
|
-
.app-card__bottom {
|
|
215
|
-
display: flex;
|
|
216
|
-
align-items: center;
|
|
217
|
-
justify-content: space-between;
|
|
218
|
-
gap: 16px;
|
|
219
|
-
}
|
|
220
|
-
.app-eyebrow {
|
|
221
|
-
margin: 0 0 8px;
|
|
222
|
-
font-size: 13px;
|
|
223
|
-
letter-spacing: 0.16em;
|
|
224
|
-
text-transform: uppercase;
|
|
225
|
-
color: var(--accent-strong);
|
|
226
|
-
}
|
|
227
|
-
.app-title {
|
|
228
|
-
margin: 0;
|
|
229
|
-
font-size: clamp(28px, 2.8vw, 38px);
|
|
230
|
-
line-height: 1;
|
|
231
|
-
}
|
|
232
|
-
.app-desc {
|
|
233
|
-
margin: 0;
|
|
234
|
-
font-size: clamp(16px, 1.4vw, 20px);
|
|
235
|
-
line-height: 1.7;
|
|
236
|
-
color: var(--muted);
|
|
237
|
-
}
|
|
238
|
-
.app-tags {
|
|
239
|
-
display: flex;
|
|
240
|
-
flex-wrap: wrap;
|
|
241
|
-
gap: 10px;
|
|
242
|
-
}
|
|
243
|
-
.app-tag {
|
|
244
|
-
padding: 8px 12px;
|
|
245
|
-
border-radius: 999px;
|
|
246
|
-
background: rgba(217, 108, 40, 0.1);
|
|
247
|
-
color: var(--accent-strong);
|
|
248
|
-
font-size: 14px;
|
|
249
|
-
}
|
|
250
|
-
.app-rank {
|
|
251
|
-
min-width: 84px;
|
|
252
|
-
text-align: right;
|
|
253
|
-
}
|
|
254
|
-
.app-rank__label,
|
|
255
|
-
.app-link {
|
|
256
|
-
display: block;
|
|
257
|
-
font-size: 13px;
|
|
258
|
-
color: var(--muted);
|
|
259
|
-
}
|
|
260
|
-
.app-rank__value {
|
|
261
|
-
font-size: clamp(24px, 2vw, 30px);
|
|
262
|
-
font-weight: 700;
|
|
263
|
-
}
|
|
264
|
-
.app-action {
|
|
265
|
-
padding: 12px 18px;
|
|
266
|
-
border-radius: 999px;
|
|
267
|
-
background: #1d2d38;
|
|
268
|
-
color: #fff;
|
|
269
|
-
font-size: 15px;
|
|
270
|
-
}
|
|
271
|
-
.empty-state {
|
|
272
|
-
padding: 32px;
|
|
273
|
-
border-radius: 28px;
|
|
274
|
-
background: var(--panel-strong);
|
|
275
|
-
border: 1px dashed rgba(23, 33, 43, 0.16);
|
|
276
|
-
}
|
|
277
|
-
.empty-state__title {
|
|
278
|
-
margin: 0;
|
|
279
|
-
font-size: 24px;
|
|
280
|
-
}
|
|
281
|
-
.empty-state__desc {
|
|
282
|
-
margin: 10px 0 0;
|
|
283
|
-
color: var(--muted);
|
|
284
|
-
font-size: 16px;
|
|
285
|
-
}
|
|
286
|
-
@media (max-width: 820px) {
|
|
287
|
-
.page { width: min(100vw - 20px, 100%); padding-top: 18px; }
|
|
288
|
-
.hero { border-radius: 24px; padding: 22px; }
|
|
289
|
-
.section-head { align-items: start; flex-direction: column; }
|
|
290
|
-
.app-card { min-height: 0; padding: 20px; border-radius: 22px; }
|
|
291
|
-
.app-card__top,
|
|
292
|
-
.app-card__bottom { align-items: start; flex-direction: column; }
|
|
293
|
-
.app-rank { text-align: left; min-width: 0; }
|
|
294
|
-
}
|
|
295
|
-
</style>
|
|
296
|
-
</head>
|
|
297
|
-
<body>
|
|
298
|
-
<main class="page">
|
|
299
|
-
<section class="hero">
|
|
300
|
-
<p class="hero-kicker">ALemonJS Launchpad</p>
|
|
301
|
-
<h1 class="hero-title">阿柠檬机器人</h1>
|
|
302
|
-
</section>
|
|
303
|
-
<section>
|
|
304
|
-
<div class="section-head">
|
|
305
|
-
<div>
|
|
306
|
-
<h2 class="section-title">应用列表</h2>
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
<div class="app-grid" id="app-grid">${cards}</div>
|
|
310
|
-
</section>
|
|
311
|
-
</main>
|
|
312
|
-
<script>
|
|
313
|
-
(() => {
|
|
314
|
-
const storageKey = 'alemonjs:launchpad:clicks';
|
|
315
|
-
const apps = ${payload};
|
|
316
|
-
const grid = document.getElementById('app-grid');
|
|
317
|
-
if (!grid) return;
|
|
280
|
+
return `
|
|
281
|
+
(() => {
|
|
282
|
+
const storageKey = 'alemonjs:launchpad:clicks';
|
|
283
|
+
const apps = ${payload};
|
|
284
|
+
const grid = document.getElementById('app-grid');
|
|
285
|
+
if (!grid) return;
|
|
318
286
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
287
|
+
const readClicks = () => {
|
|
288
|
+
try {
|
|
289
|
+
return JSON.parse(localStorage.getItem(storageKey) || '{}');
|
|
290
|
+
} catch {
|
|
291
|
+
return {};
|
|
292
|
+
}
|
|
293
|
+
};
|
|
326
294
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
295
|
+
const writeClicks = (value) => {
|
|
296
|
+
localStorage.setItem(storageKey, JSON.stringify(value));
|
|
297
|
+
};
|
|
330
298
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
299
|
+
const scoreOf = (id, clicks) => Number(clicks[id] || 0);
|
|
300
|
+
const clicks = readClicks();
|
|
301
|
+
const cards = Array.from(grid.querySelectorAll('[data-app-id]'));
|
|
334
302
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
303
|
+
const refreshCounts = () => {
|
|
304
|
+
cards.forEach((card) => {
|
|
305
|
+
const id = card.getAttribute('data-app-id') || '';
|
|
306
|
+
const node = card.querySelector('[data-click-count]');
|
|
307
|
+
if (node) {
|
|
308
|
+
node.textContent = String(scoreOf(id, clicks));
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
};
|
|
344
312
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
313
|
+
const sortCards = () => {
|
|
314
|
+
cards
|
|
315
|
+
.sort((left, right) => {
|
|
316
|
+
const leftId = left.getAttribute('data-app-id') || '';
|
|
317
|
+
const rightId = right.getAttribute('data-app-id') || '';
|
|
318
|
+
const diff = scoreOf(rightId, clicks) - scoreOf(leftId, clicks);
|
|
319
|
+
if (diff !== 0) return diff;
|
|
320
|
+
return leftId.localeCompare(rightId);
|
|
321
|
+
})
|
|
322
|
+
.forEach(card => grid.appendChild(card));
|
|
323
|
+
};
|
|
356
324
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
});
|
|
325
|
+
cards.forEach((card) => {
|
|
326
|
+
card.addEventListener('click', () => {
|
|
327
|
+
const id = card.getAttribute('data-app-id') || '';
|
|
328
|
+
clicks[id] = scoreOf(id, clicks) + 1;
|
|
329
|
+
writeClicks(clicks);
|
|
330
|
+
refreshCounts();
|
|
331
|
+
sortCards();
|
|
365
332
|
});
|
|
333
|
+
});
|
|
366
334
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
335
|
+
refreshCounts();
|
|
336
|
+
sortCards();
|
|
337
|
+
})();
|
|
338
|
+
`;
|
|
339
|
+
};
|
|
340
|
+
class LaunchpadPage extends Component {
|
|
341
|
+
render() {
|
|
342
|
+
const visibleApps = this.props.apps
|
|
343
|
+
.filter(app => app.enabled && app.status === 'ready' && (app.capabilities.web || app.capabilities.httpApi))
|
|
344
|
+
.sort((left, right) => {
|
|
345
|
+
if (left.kind === 'main' && right.kind !== 'main') {
|
|
346
|
+
return -1;
|
|
347
|
+
}
|
|
348
|
+
if (left.kind !== 'main' && right.kind === 'main') {
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
return left.name.localeCompare(right.name);
|
|
352
|
+
});
|
|
353
|
+
const cards = visibleApps.length ? visibleApps.map(renderCardHtml).join('') : renderEmptyHtml();
|
|
354
|
+
const script = renderLaunchpadScript(visibleApps);
|
|
355
|
+
return Html({ lang: 'zh-CN' }, Head(createElement('meta', { charset: 'utf-8' }), createElement('meta', { name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover' }), Title('ALemonJS 应用入口'), Style(styles)), Body(createElement('main', { className: 'page' }, createElement('section', { className: 'hero' }, P({ className: 'hero-kicker' }, 'ALemonJS Launchpad'), createElement('h1', { className: 'hero-title' }, '阿柠檬机器人'), P({ className: 'hero-desc' }, '这里会自动列出当前已准备好的主应用与插件。默认入口优先走静态页面,缺失时再回落到应用路由。'), Div({ className: 'hero-meta' }, createElement('span', { className: 'hero-pill' }, createElement('strong', null, String(visibleApps.length)), ' 个可访问入口'), createElement('span', { className: 'hero-pill' }, '主应用统一在 ', createElement('strong', null, '/app/')), createElement('span', { className: 'hero-pill' }, '插件统一在 ', createElement('strong', null, '/apps/<name>/')))), createElement('section', null, Div({ className: 'section-head' }, Div(null, createElement('h2', { className: 'section-title' }, '应用列表'), P({ className: 'section-note' }, '点击次数越高,卡片排序越靠前。'))), Div({ className: 'app-grid', id: 'app-grid', dangerouslySetInnerHTML: { __html: cards } }))), createElement('script', { dangerouslySetInnerHTML: { __html: script } })));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const renderHelloHtml = (apps) => {
|
|
359
|
+
return DOCTYPE + renderToString(createElement(LaunchpadPage, { apps }));
|
|
373
360
|
};
|
|
374
361
|
var hello_html = renderHelloHtml([]);
|
|
375
362
|
|
|
@@ -65,6 +65,17 @@ const matchBasePath = (requestPath, basePath) => {
|
|
|
65
65
|
}
|
|
66
66
|
return '';
|
|
67
67
|
};
|
|
68
|
+
const matchApiBasePath = (requestPath, basePath) => {
|
|
69
|
+
return requestPath === basePath || requestPath.startsWith(`${basePath}/`);
|
|
70
|
+
};
|
|
71
|
+
const needsTrailingSlashRedirect = (requestUrl, basePath) => {
|
|
72
|
+
return requestUrl === basePath || requestUrl.startsWith(`${basePath}?`);
|
|
73
|
+
};
|
|
74
|
+
const getOriginalPathname = (ctx) => {
|
|
75
|
+
const rawUrl = typeof ctx.originalUrl === 'string' && ctx.originalUrl ? ctx.originalUrl : ctx.url;
|
|
76
|
+
const [pathname] = String(rawUrl).split('?');
|
|
77
|
+
return pathname || '/';
|
|
78
|
+
};
|
|
68
79
|
const rewriteCtxPath = async (ctx, nextPath, handler) => {
|
|
69
80
|
const search = ctx.querystring ? `?${ctx.querystring}` : '';
|
|
70
81
|
const originalUrl = ctx.url;
|
|
@@ -139,6 +150,99 @@ const denyRuntimeAppAccess = (ctx, appName, capability) => {
|
|
|
139
150
|
}
|
|
140
151
|
return runtimeApp;
|
|
141
152
|
};
|
|
153
|
+
const getRuntimeAppRouters = (appName) => {
|
|
154
|
+
const registerRouters = getChildrenApp(appName)?.register?.koaRouter;
|
|
155
|
+
const storedRouters = getRuntimeAppKoaRouters(appName);
|
|
156
|
+
return (storedRouters.length ? storedRouters : Array.isArray(registerRouters) ? registerRouters : registerRouters ? [registerRouters] : []).filter(Boolean);
|
|
157
|
+
};
|
|
158
|
+
const dispatchAppKoaRouters = async (ctx, appName) => {
|
|
159
|
+
const runtimeApp = getRuntimeApp(appName);
|
|
160
|
+
if (!runtimeApp || !runtimeApp.enabled || runtimeApp.status !== 'ready' || !hasRuntimeAppCapability(appName, 'httpApi')) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const routers = getRuntimeAppRouters(appName);
|
|
164
|
+
const aliasBases = appName === 'main' ? ['/app'] : [`/apps/${appName}`];
|
|
165
|
+
for (const koaRouter of routers) {
|
|
166
|
+
for (const basePath of aliasBases) {
|
|
167
|
+
const rewrittenPath = matchBasePath(ctx.path, basePath);
|
|
168
|
+
if (!rewrittenPath) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const matchedContext = ctx;
|
|
173
|
+
const beforeMatched = Array.isArray(matchedContext.matched) ? matchedContext.matched.length : 0;
|
|
174
|
+
const beforeStatus = ctx.status;
|
|
175
|
+
const beforeBody = ctx.body;
|
|
176
|
+
const beforeMatchedRoute = ctx._matchedRoute;
|
|
177
|
+
const beforeRouterPath = ctx.routerPath;
|
|
178
|
+
let fallthrough = false;
|
|
179
|
+
await rewriteCtxPath(ctx, rewrittenPath, async () => {
|
|
180
|
+
await koaRouter.routes()(ctx, async () => {
|
|
181
|
+
fallthrough = true;
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
const afterMatched = Array.isArray(matchedContext.matched) ? matchedContext.matched.length : 0;
|
|
185
|
+
const afterMatchedRoute = ctx._matchedRoute;
|
|
186
|
+
const afterRouterPath = ctx.routerPath;
|
|
187
|
+
const handled = afterMatched > beforeMatched
|
|
188
|
+
|| afterMatchedRoute !== beforeMatchedRoute
|
|
189
|
+
|| afterRouterPath !== beforeRouterPath
|
|
190
|
+
|| ctx.status !== beforeStatus
|
|
191
|
+
|| ctx.body !== beforeBody
|
|
192
|
+
|| !fallthrough;
|
|
193
|
+
if (!handled) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
await rewriteCtxPath(ctx, rewrittenPath, async () => {
|
|
197
|
+
await koaRouter.allowedMethods()(ctx, async () => { });
|
|
198
|
+
});
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const handled = await dispatchHttpError({
|
|
203
|
+
ctx,
|
|
204
|
+
error,
|
|
205
|
+
appName,
|
|
206
|
+
path: ctx.path,
|
|
207
|
+
method: ctx.method,
|
|
208
|
+
kind: 'koa-router'
|
|
209
|
+
});
|
|
210
|
+
if (handled) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
logger.warn({
|
|
214
|
+
code: ResultCode.Fail,
|
|
215
|
+
message: `Error request ${ctx.path}:`,
|
|
216
|
+
data: error instanceof Error ? error.message : String(error)
|
|
217
|
+
});
|
|
218
|
+
ctx.status = 500;
|
|
219
|
+
ctx.body = {
|
|
220
|
+
code: 500,
|
|
221
|
+
message: '处理 Koa Router 请求时发生错误。',
|
|
222
|
+
error: error instanceof Error ? error.message : String(error)
|
|
223
|
+
};
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
};
|
|
230
|
+
const isNamespacedHtmlRequest = (ctx) => {
|
|
231
|
+
if (ctx.method !== 'GET') {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (ctx.path === '/app' || ctx.path.startsWith('/app/')) {
|
|
235
|
+
return !matchApiBasePath(ctx.path, '/app/api');
|
|
236
|
+
}
|
|
237
|
+
if (!ctx.path.startsWith('/apps/')) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const [, , appName, segment = ''] = ctx.path.split('/');
|
|
241
|
+
if (!appName) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return !matchApiBasePath(ctx.path, `/apps/${appName}/api`);
|
|
245
|
+
};
|
|
142
246
|
const dispatchRegisteredKoaRouters = async (ctx) => {
|
|
143
247
|
const registeredRouters = listRuntimeAppKoaRouters();
|
|
144
248
|
const candidates = new Set(registeredRouters.map(item => item.name));
|
|
@@ -149,80 +253,82 @@ const dispatchRegisteredKoaRouters = async (ctx) => {
|
|
|
149
253
|
}
|
|
150
254
|
});
|
|
151
255
|
for (const appName of candidates) {
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
}
|
|
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}`];
|
|
160
|
-
for (const koaRouter of routers) {
|
|
161
|
-
for (const basePath of aliasBases) {
|
|
162
|
-
const rewrittenPath = matchBasePath(ctx.path, basePath);
|
|
163
|
-
if (!rewrittenPath) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
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
|
-
};
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
256
|
+
const handled = await dispatchAppKoaRouters(ctx, appName);
|
|
257
|
+
if (handled) {
|
|
258
|
+
return true;
|
|
222
259
|
}
|
|
223
260
|
}
|
|
224
261
|
return false;
|
|
225
262
|
};
|
|
263
|
+
const serveStaticResource = async (ctx, appName, rootDir, resourcePath) => {
|
|
264
|
+
const packageRoot = resolvePackageRoot(rootDir);
|
|
265
|
+
let root = '';
|
|
266
|
+
try {
|
|
267
|
+
root = readWebRootConfig(packageRoot);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
const handled = await dispatchHttpError({
|
|
271
|
+
ctx,
|
|
272
|
+
error: err,
|
|
273
|
+
appName,
|
|
274
|
+
path: ctx.path,
|
|
275
|
+
method: ctx.method,
|
|
276
|
+
kind: 'web'
|
|
277
|
+
});
|
|
278
|
+
if (handled) {
|
|
279
|
+
return 'handled';
|
|
280
|
+
}
|
|
281
|
+
ctx.status = 500;
|
|
282
|
+
ctx.body = {
|
|
283
|
+
code: 500,
|
|
284
|
+
message: '加载 package.json 时发生错误。',
|
|
285
|
+
error: err instanceof Error ? err.message : String(err)
|
|
286
|
+
};
|
|
287
|
+
return 'handled';
|
|
288
|
+
}
|
|
289
|
+
const webRoot = root ? path__default.join(packageRoot, root) : packageRoot;
|
|
290
|
+
const fullPath = safePath(webRoot, resourcePath);
|
|
291
|
+
if (!fullPath) {
|
|
292
|
+
ctx.status = 403;
|
|
293
|
+
ctx.body = {
|
|
294
|
+
code: 403,
|
|
295
|
+
message: '非法路径',
|
|
296
|
+
data: null
|
|
297
|
+
};
|
|
298
|
+
return 'handled';
|
|
299
|
+
}
|
|
300
|
+
if (!existsSync(fullPath)) {
|
|
301
|
+
return 'miss';
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const file = await fs__default.promises.readFile(fullPath);
|
|
305
|
+
const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
|
|
306
|
+
ctx.set('Content-Type', mimeType);
|
|
307
|
+
ctx.body = file;
|
|
308
|
+
ctx.status = 200;
|
|
309
|
+
return 'handled';
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
const handled = await dispatchHttpError({
|
|
313
|
+
ctx,
|
|
314
|
+
error: err,
|
|
315
|
+
appName,
|
|
316
|
+
path: ctx.path,
|
|
317
|
+
method: ctx.method,
|
|
318
|
+
kind: 'web'
|
|
319
|
+
});
|
|
320
|
+
if (handled) {
|
|
321
|
+
return 'handled';
|
|
322
|
+
}
|
|
323
|
+
ctx.status = 500;
|
|
324
|
+
ctx.body = {
|
|
325
|
+
code: 500,
|
|
326
|
+
message: appName === 'main' ? '加载资源时发生服务器错误。' : `加载子应用 '${appName}' 资源时发生服务器错误。`,
|
|
327
|
+
error: err instanceof Error ? err.message : String(err)
|
|
328
|
+
};
|
|
329
|
+
return 'handled';
|
|
330
|
+
}
|
|
331
|
+
};
|
|
226
332
|
router.get('/', ctx => {
|
|
227
333
|
ctx.status = 200;
|
|
228
334
|
ctx.set('Content-Type', 'text/html; charset=utf-8');
|
|
@@ -271,6 +377,10 @@ router.get('api/runtime/apps/:app', ctx => {
|
|
|
271
377
|
};
|
|
272
378
|
});
|
|
273
379
|
router.use(async (ctx, next) => {
|
|
380
|
+
if (isNamespacedHtmlRequest(ctx)) {
|
|
381
|
+
await next();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
274
384
|
const handled = await dispatchRegisteredKoaRouters(ctx);
|
|
275
385
|
if (handled) {
|
|
276
386
|
return;
|
|
@@ -278,6 +388,10 @@ router.use(async (ctx, next) => {
|
|
|
278
388
|
await next();
|
|
279
389
|
});
|
|
280
390
|
const handleMainAppRequest = async (ctx) => {
|
|
391
|
+
if (needsTrailingSlashRedirect(getOriginalPathname(ctx), '/app')) {
|
|
392
|
+
ctx.redirect('/app/');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
281
395
|
if (!process.env.input) {
|
|
282
396
|
ctx.status = 400;
|
|
283
397
|
ctx.body = {
|
|
@@ -289,7 +403,7 @@ const handleMainAppRequest = async (ctx) => {
|
|
|
289
403
|
}
|
|
290
404
|
const rootPath = process.cwd();
|
|
291
405
|
const apiPath = '/app/api';
|
|
292
|
-
if (ctx.path
|
|
406
|
+
if (matchApiBasePath(ctx.path, apiPath)) {
|
|
293
407
|
const runtimeApp = denyRuntimeAppAccess(ctx, 'main', 'httpApi');
|
|
294
408
|
if (!runtimeApp) {
|
|
295
409
|
return;
|
|
@@ -370,83 +484,31 @@ const handleMainAppRequest = async (ctx) => {
|
|
|
370
484
|
ctx.status = 405;
|
|
371
485
|
return;
|
|
372
486
|
}
|
|
373
|
-
|
|
487
|
+
const runtimeApp = getRuntimeApp('main');
|
|
374
488
|
const resourcePath = formatPath(ctx.params?.path);
|
|
375
|
-
|
|
376
|
-
|
|
489
|
+
if (!runtimeApp?.enabled || runtimeApp.status !== 'ready') {
|
|
490
|
+
denyRuntimeAppAccess(ctx, 'main', 'web');
|
|
377
491
|
return;
|
|
378
492
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
catch (err) {
|
|
384
|
-
const handled = await dispatchHttpError({
|
|
385
|
-
ctx,
|
|
386
|
-
error: err,
|
|
387
|
-
appName: 'main',
|
|
388
|
-
path: ctx.path,
|
|
389
|
-
method: ctx.method,
|
|
390
|
-
kind: 'web'
|
|
391
|
-
});
|
|
392
|
-
if (handled) {
|
|
493
|
+
if (hasRuntimeAppCapability('main', 'web')) {
|
|
494
|
+
const staticResult = await serveStaticResource(ctx, 'main', runtimeApp.rootDir, resourcePath);
|
|
495
|
+
if (staticResult === 'handled') {
|
|
393
496
|
return;
|
|
394
497
|
}
|
|
395
|
-
ctx.status = 500;
|
|
396
|
-
ctx.body = {
|
|
397
|
-
code: 500,
|
|
398
|
-
message: '加载 package.json 时发生错误。',
|
|
399
|
-
error: err instanceof Error ? err.message : String(err)
|
|
400
|
-
};
|
|
401
|
-
return;
|
|
402
498
|
}
|
|
403
|
-
|
|
404
|
-
const fullPath = safePath(webRoot, resourcePath);
|
|
405
|
-
if (!fullPath) {
|
|
406
|
-
ctx.status = 403;
|
|
407
|
-
ctx.body = {
|
|
408
|
-
code: 403,
|
|
409
|
-
message: '非法路径',
|
|
410
|
-
data: null
|
|
411
|
-
};
|
|
499
|
+
if (await dispatchAppKoaRouters(ctx, 'main')) {
|
|
412
500
|
return;
|
|
413
501
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
ctx.set('Content-Type', mimeType);
|
|
418
|
-
ctx.body = file;
|
|
419
|
-
ctx.status = 200;
|
|
420
|
-
}
|
|
421
|
-
catch (err) {
|
|
422
|
-
const handled = await dispatchHttpError({
|
|
423
|
-
ctx,
|
|
424
|
-
error: err,
|
|
425
|
-
appName: 'main',
|
|
426
|
-
path: ctx.path,
|
|
427
|
-
method: ctx.method,
|
|
428
|
-
kind: 'web'
|
|
429
|
-
});
|
|
430
|
-
if (handled) {
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
if (err?.status === 404) {
|
|
434
|
-
ctx.status = 404;
|
|
435
|
-
ctx.body = {
|
|
436
|
-
code: 404,
|
|
437
|
-
message: '资源中未找到。',
|
|
438
|
-
data: null
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
ctx.status = 500;
|
|
443
|
-
ctx.body = {
|
|
444
|
-
code: 500,
|
|
445
|
-
message: '加载资源时发生服务器错误。',
|
|
446
|
-
error: err instanceof Error ? err.message : String(err)
|
|
447
|
-
};
|
|
448
|
-
}
|
|
502
|
+
if (!hasRuntimeAppCapability('main', 'web')) {
|
|
503
|
+
denyRuntimeAppAccess(ctx, 'main', 'web');
|
|
504
|
+
return;
|
|
449
505
|
}
|
|
506
|
+
ctx.status = 404;
|
|
507
|
+
ctx.body = {
|
|
508
|
+
code: 404,
|
|
509
|
+
message: '资源中未找到。',
|
|
510
|
+
data: null
|
|
511
|
+
};
|
|
450
512
|
};
|
|
451
513
|
router.all('app', handleMainAppRequest);
|
|
452
514
|
router.all('app/', handleMainAppRequest);
|
|
@@ -462,8 +524,12 @@ const handlePluginAppRequest = async (ctx) => {
|
|
|
462
524
|
};
|
|
463
525
|
return;
|
|
464
526
|
}
|
|
527
|
+
if (needsTrailingSlashRedirect(getOriginalPathname(ctx), `/apps/${appName}`)) {
|
|
528
|
+
ctx.redirect(`/apps/${appName}/`);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
465
531
|
const apiPath = `/apps/${appName}/api`;
|
|
466
|
-
if (ctx.path
|
|
532
|
+
if (matchApiBasePath(ctx.path, apiPath)) {
|
|
467
533
|
const runtimeApp = denyRuntimeAppAccess(ctx, appName, 'httpApi');
|
|
468
534
|
if (!runtimeApp) {
|
|
469
535
|
return;
|
|
@@ -538,88 +604,31 @@ const handlePluginAppRequest = async (ctx) => {
|
|
|
538
604
|
ctx.status = 405;
|
|
539
605
|
return;
|
|
540
606
|
}
|
|
541
|
-
const runtimeApp =
|
|
542
|
-
if (!runtimeApp) {
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
const packageRoot = resolvePackageRoot(runtimeApp.rootDir);
|
|
607
|
+
const runtimeApp = getRuntimeApp(appName);
|
|
546
608
|
const resourcePath = formatPath(ctx.params?.path);
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
609
|
+
if (!runtimeApp?.enabled || runtimeApp.status !== 'ready') {
|
|
610
|
+
denyRuntimeAppAccess(ctx, appName, 'web');
|
|
611
|
+
return;
|
|
550
612
|
}
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
error: err,
|
|
555
|
-
appName,
|
|
556
|
-
path: ctx.path,
|
|
557
|
-
method: ctx.method,
|
|
558
|
-
kind: 'web'
|
|
559
|
-
});
|
|
560
|
-
if (handled) {
|
|
613
|
+
if (hasRuntimeAppCapability(appName, 'web')) {
|
|
614
|
+
const staticResult = await serveStaticResource(ctx, appName, runtimeApp.rootDir, resourcePath);
|
|
615
|
+
if (staticResult === 'handled') {
|
|
561
616
|
return;
|
|
562
617
|
}
|
|
563
|
-
ctx.status = 500;
|
|
564
|
-
ctx.body = {
|
|
565
|
-
code: 500,
|
|
566
|
-
message: '加载 package.json 时发生错误。',
|
|
567
|
-
error: err instanceof Error ? err.message : String(err)
|
|
568
|
-
};
|
|
569
|
-
return;
|
|
570
618
|
}
|
|
571
|
-
|
|
572
|
-
const fullPath = safePath(webRoot, resourcePath);
|
|
573
|
-
if (!fullPath) {
|
|
574
|
-
ctx.status = 403;
|
|
575
|
-
ctx.body = {
|
|
576
|
-
code: 403,
|
|
577
|
-
message: '非法路径',
|
|
578
|
-
data: null
|
|
579
|
-
};
|
|
619
|
+
if (await dispatchAppKoaRouters(ctx, appName)) {
|
|
580
620
|
return;
|
|
581
621
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
ctx.set('Content-Type', mimeType);
|
|
586
|
-
ctx.body = file;
|
|
587
|
-
ctx.status = 200;
|
|
588
|
-
}
|
|
589
|
-
catch (err) {
|
|
590
|
-
const handled = await dispatchHttpError({
|
|
591
|
-
ctx,
|
|
592
|
-
error: err,
|
|
593
|
-
appName,
|
|
594
|
-
path: ctx.path,
|
|
595
|
-
method: ctx.method,
|
|
596
|
-
kind: 'web'
|
|
597
|
-
});
|
|
598
|
-
if (handled) {
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
if (err?.status === 404) {
|
|
602
|
-
ctx.status = 404;
|
|
603
|
-
ctx.body = {
|
|
604
|
-
code: 404,
|
|
605
|
-
message: `资源 '${resourcePath}' 在子应用 '${appName}' 中未找到。`,
|
|
606
|
-
data: null
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
logger.warn({
|
|
611
|
-
code: ResultCode.Fail,
|
|
612
|
-
message: `Error request ${ctx.path}:`,
|
|
613
|
-
data: err instanceof Error ? err.message : String(err)
|
|
614
|
-
});
|
|
615
|
-
ctx.status = 500;
|
|
616
|
-
ctx.body = {
|
|
617
|
-
code: 500,
|
|
618
|
-
message: `加载子应用 '${appName}' 资源时发生服务器错误。`,
|
|
619
|
-
error: err instanceof Error ? err.message : String(err)
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
+
if (!hasRuntimeAppCapability(appName, 'web')) {
|
|
623
|
+
denyRuntimeAppAccess(ctx, appName, 'web');
|
|
624
|
+
return;
|
|
622
625
|
}
|
|
626
|
+
ctx.status = 404;
|
|
627
|
+
ctx.body = {
|
|
628
|
+
code: 404,
|
|
629
|
+
message: `资源 '${resourcePath}' 在子应用 '${appName}' 中未找到。`,
|
|
630
|
+
data: null
|
|
631
|
+
};
|
|
623
632
|
};
|
|
624
633
|
router.all('apps/:app', handlePluginAppRequest);
|
|
625
634
|
router.all('apps/:app/', handlePluginAppRequest);
|
|
@@ -35,16 +35,30 @@ const getModuelFile = (dir) => {
|
|
|
35
35
|
return '';
|
|
36
36
|
};
|
|
37
37
|
const formatPath = (pathValue) => {
|
|
38
|
-
|
|
38
|
+
const normalizedPath = String(pathValue || '')
|
|
39
|
+
.replace(/\\/g, '/')
|
|
40
|
+
.replace(/^\/+/, '')
|
|
41
|
+
.replace(/\/{2,}/g, '/')
|
|
42
|
+
.trim();
|
|
43
|
+
if (!normalizedPath) {
|
|
39
44
|
return 'index.html';
|
|
40
45
|
}
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
if (normalizedPath === 'index' || normalizedPath === 'index.html') {
|
|
47
|
+
return 'index.html';
|
|
48
|
+
}
|
|
49
|
+
if (normalizedPath.endsWith('/')) {
|
|
50
|
+
return `${normalizedPath}index.html`;
|
|
51
|
+
}
|
|
52
|
+
const parts = normalizedPath.split('/');
|
|
53
|
+
const lastPath = parts[parts.length - 1];
|
|
54
|
+
if (lastPath === 'index' || lastPath === 'index.html') {
|
|
55
|
+
parts[parts.length - 1] = 'index.html';
|
|
56
|
+
return parts.join('/');
|
|
57
|
+
}
|
|
43
58
|
if (lastPath.includes('.')) {
|
|
44
|
-
return
|
|
59
|
+
return normalizedPath;
|
|
45
60
|
}
|
|
46
|
-
|
|
47
|
-
return pathValue;
|
|
61
|
+
return `${normalizedPath}.html`;
|
|
48
62
|
};
|
|
49
63
|
|
|
50
64
|
export { formatPath, getModuelFile, isValidPackageName, safePath };
|
package/lib/common/react.js
CHANGED
|
@@ -32,7 +32,7 @@ class Component {
|
|
|
32
32
|
function attrsToString(props = {}) {
|
|
33
33
|
const parts = [];
|
|
34
34
|
for (const key of Object.keys(props)) {
|
|
35
|
-
if (key === 'children') {
|
|
35
|
+
if (key === 'children' || key === 'dangerouslySetInnerHTML') {
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
38
|
const val = props[key];
|
|
@@ -59,6 +59,16 @@ function attrsToString(props = {}) {
|
|
|
59
59
|
return parts.length ? ' ' + parts.join(' ') : '';
|
|
60
60
|
}
|
|
61
61
|
const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
62
|
+
const rawTextTags = new Set(['script', 'style']);
|
|
63
|
+
function renderRawText(vnode) {
|
|
64
|
+
if (vnode === null || vnode === undefined || vnode === false) {
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') {
|
|
68
|
+
return String(vnode);
|
|
69
|
+
}
|
|
70
|
+
return renderToString(vnode);
|
|
71
|
+
}
|
|
62
72
|
function renderToString(vnode) {
|
|
63
73
|
if (vnode === null || vnode === undefined || vnode === false) {
|
|
64
74
|
return '';
|
|
@@ -92,12 +102,21 @@ function renderToString(vnode) {
|
|
|
92
102
|
if (voidTags.has(tag.toLowerCase())) {
|
|
93
103
|
return `<${tag}${attrs} />`;
|
|
94
104
|
}
|
|
95
|
-
const children = (vnode.children ?? []).map(renderToString).join('');
|
|
105
|
+
const children = rawTextTags.has(tag.toLowerCase()) ? (vnode.children ?? []).map(renderRawText).join('') : (vnode.children ?? []).map(renderToString).join('');
|
|
96
106
|
return `<${tag}${attrs}>${children}</${tag}>`;
|
|
97
107
|
}
|
|
98
108
|
function makeTag(tag) {
|
|
99
109
|
return function tagFactory(first, ...restChildren) {
|
|
100
|
-
const
|
|
110
|
+
const looksLikeVNode = first !== null &&
|
|
111
|
+
typeof first === 'object' &&
|
|
112
|
+
!Array.isArray(first) &&
|
|
113
|
+
Object.prototype.toString.call(first) === '[object Object]' &&
|
|
114
|
+
('type' in first || 'children' in first);
|
|
115
|
+
const looksLikeProps = first !== null &&
|
|
116
|
+
typeof first === 'object' &&
|
|
117
|
+
!Array.isArray(first) &&
|
|
118
|
+
Object.prototype.toString.call(first) === '[object Object]' &&
|
|
119
|
+
!looksLikeVNode;
|
|
101
120
|
if (looksLikeProps) {
|
|
102
121
|
return createElement(tag, first, ...restChildren);
|
|
103
122
|
}
|