alemonjs 2.1.0-alpha.53 → 2.1.0-alpha.55
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/cbp/connects/client.js +0 -1
- package/lib/cbp/routers/hello.html.d.ts +2 -0
- package/lib/cbp/routers/hello.html.js +31 -0
- package/lib/cbp/routers/router.js +2 -236
- package/lib/cbp/server/main.js +0 -1
- package/lib/client.d.ts +1 -2
- package/lib/client.js +12 -2
- package/lib/core/react.d.ts +28 -0
- package/lib/core/react.js +147 -0
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/main.js +2 -0
- package/lib/server/main.d.ts +1 -0
- package/lib/server/main.js +30 -0
- package/lib/server/routers/hello.html.d.ts +2 -0
- package/lib/server/routers/hello.html.js +31 -0
- package/lib/server/routers/middleware.d.ts +2 -0
- package/lib/server/routers/middleware.js +40 -0
- package/lib/server/routers/router.d.ts +3 -0
- package/lib/server/routers/router.js +255 -0
- package/lib/server/routers/utils.d.ts +2 -0
- package/lib/server/routers/utils.js +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Component, Head, Title, Style, Body, H1, P, A, Div, Html } from '../../core/react.js';
|
|
2
|
+
|
|
3
|
+
class App extends Component {
|
|
4
|
+
render() {
|
|
5
|
+
const style = `
|
|
6
|
+
body {
|
|
7
|
+
width: 35em;
|
|
8
|
+
margin: 0 auto;
|
|
9
|
+
font-family: Tahoma, Verdana, Arial, sans-serif;
|
|
10
|
+
background-color: #f8f8f8;
|
|
11
|
+
color: #333;
|
|
12
|
+
}
|
|
13
|
+
h1 {
|
|
14
|
+
color: #0099cc;
|
|
15
|
+
margin-top: 2em;
|
|
16
|
+
}
|
|
17
|
+
.footer {
|
|
18
|
+
margin-top: 3em;
|
|
19
|
+
font-size: 0.9em;
|
|
20
|
+
color: #888;
|
|
21
|
+
}
|
|
22
|
+
a { color: #0099cc; }
|
|
23
|
+
`;
|
|
24
|
+
const head = Head(null, Title('欢迎使用 AlemonJS!'), Style(style));
|
|
25
|
+
const body = Body(null, H1('AlemonJS 启动成功!'), P(null, '已成功通过 ', A({ href: 'https://alemonjs.com', target: '_blank' }, 'AlemonJS 框架'), ' 启动。'), Div({ className: 'footer' }, '— 感谢选择 AlemonJS。'));
|
|
26
|
+
return Html(null, head, body);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var hello = App.renderToHtml();
|
|
30
|
+
|
|
31
|
+
export { hello as default };
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import KoaRouter from 'koa-router';
|
|
2
|
-
import
|
|
3
|
-
import path, { join, dirname } from 'path';
|
|
4
|
-
import mime from 'mime-types';
|
|
5
|
-
import { createRequire } from 'module';
|
|
6
|
-
import { html } from '../processor/hello.html.js';
|
|
7
|
-
import { getModuelFile, formatPath } from './utils.js';
|
|
8
|
-
import { collectMiddlewares, runMiddlewares } from './middleware.js';
|
|
2
|
+
import hello from './hello.html.js';
|
|
9
3
|
|
|
10
|
-
const require = createRequire(import.meta.url);
|
|
11
|
-
const mainDirMap = new Map();
|
|
12
4
|
const router = new KoaRouter({
|
|
13
5
|
prefix: '/'
|
|
14
6
|
});
|
|
15
7
|
router.get('/', ctx => {
|
|
16
8
|
ctx.status = 200;
|
|
17
9
|
ctx.set('Content-Type', 'text/html; charset=utf-8');
|
|
18
|
-
ctx.body =
|
|
10
|
+
ctx.body = hello;
|
|
19
11
|
});
|
|
20
12
|
router.get('api/online', ctx => {
|
|
21
13
|
ctx.status = 200;
|
|
@@ -25,231 +17,5 @@ router.get('api/online', ctx => {
|
|
|
25
17
|
data: null
|
|
26
18
|
};
|
|
27
19
|
});
|
|
28
|
-
router.all('app/{*path}', async (ctx) => {
|
|
29
|
-
if (!process.env.input) {
|
|
30
|
-
ctx.status = 400;
|
|
31
|
-
ctx.body = {
|
|
32
|
-
code: 400,
|
|
33
|
-
message: '没有主要入口文件',
|
|
34
|
-
data: null
|
|
35
|
-
};
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const rootPath = process.cwd();
|
|
39
|
-
const apiPath = '/app/api';
|
|
40
|
-
if (ctx.path.startsWith(apiPath)) {
|
|
41
|
-
const mainPath = join(rootPath, process.env.input);
|
|
42
|
-
if (!existsSync(mainPath)) {
|
|
43
|
-
ctx.status = 400;
|
|
44
|
-
ctx.body = {
|
|
45
|
-
code: 400,
|
|
46
|
-
message: '未找到主要入口文件',
|
|
47
|
-
data: 'existsSync input'
|
|
48
|
-
};
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
const mainDir = dirname(mainPath);
|
|
52
|
-
try {
|
|
53
|
-
const dir = join(mainDir, 'route', ctx.path?.replace(apiPath, '/api') || '');
|
|
54
|
-
if (existsSync(dir) && fs.statSync(dir).isFile()) {
|
|
55
|
-
ctx.status = 404;
|
|
56
|
-
ctx.body = {
|
|
57
|
-
code: 404,
|
|
58
|
-
message: `API '${ctx.path}' 未找到。`,
|
|
59
|
-
data: 'route path is file not directory'
|
|
60
|
-
};
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const modulePath = getModuelFile(dir);
|
|
64
|
-
if (!modulePath) {
|
|
65
|
-
ctx.status = 404;
|
|
66
|
-
ctx.body = {
|
|
67
|
-
code: 404,
|
|
68
|
-
message: `API '${ctx.path}' 未找到。`,
|
|
69
|
-
data: 'existsSync modulePath'
|
|
70
|
-
};
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const apiModule = await import(`file://${modulePath}`);
|
|
74
|
-
const handler = apiModule[ctx.method];
|
|
75
|
-
if (!handler || typeof handler !== 'function') {
|
|
76
|
-
ctx.status = 405;
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const middlewares = await collectMiddlewares(modulePath);
|
|
80
|
-
await runMiddlewares(middlewares, ctx, handler);
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
console.error(`Error handling API request ${ctx.path}`);
|
|
84
|
-
ctx.status = 500;
|
|
85
|
-
ctx.body = {
|
|
86
|
-
code: 500,
|
|
87
|
-
message: '处理 API 请求时发生错误。',
|
|
88
|
-
error: err.message
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
if (ctx.method !== 'GET') {
|
|
94
|
-
ctx.status = 405;
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
let root = '';
|
|
98
|
-
const resourcePath = formatPath(ctx.params?.path);
|
|
99
|
-
try {
|
|
100
|
-
const pkg = require(path.join(rootPath, 'package.json')) ?? {};
|
|
101
|
-
root = pkg.alemonjs?.web?.root ?? '';
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
ctx.status = 500;
|
|
105
|
-
ctx.body = {
|
|
106
|
-
code: 500,
|
|
107
|
-
message: '加载 package.json 时发生错误。',
|
|
108
|
-
error: err.message
|
|
109
|
-
};
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const fullPath = root ? path.join(rootPath, root, resourcePath) : path.join(rootPath, resourcePath);
|
|
113
|
-
try {
|
|
114
|
-
const file = await fs.promises.readFile(fullPath);
|
|
115
|
-
const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
|
|
116
|
-
ctx.set('Content-Type', mimeType);
|
|
117
|
-
ctx.body = file;
|
|
118
|
-
ctx.status = 200;
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
if (err?.status === 404) {
|
|
122
|
-
ctx.status = 404;
|
|
123
|
-
ctx.body = {
|
|
124
|
-
code: 404,
|
|
125
|
-
message: '资源中未找到。',
|
|
126
|
-
data: null
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
ctx.status = 500;
|
|
131
|
-
ctx.body = {
|
|
132
|
-
code: 500,
|
|
133
|
-
message: '加载资源时发生服务器错误。',
|
|
134
|
-
error: err.message
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
router.all('app', ctx => {
|
|
140
|
-
ctx.redirect('/app/');
|
|
141
|
-
});
|
|
142
|
-
router.all('apps/:app/{*path}', async (ctx) => {
|
|
143
|
-
const appName = ctx.params.app;
|
|
144
|
-
const apiPath = `/apps/${appName}/api`;
|
|
145
|
-
if (ctx.path.startsWith(apiPath)) {
|
|
146
|
-
try {
|
|
147
|
-
if (!mainDirMap.has(appName)) {
|
|
148
|
-
const mainPath = require.resolve(appName);
|
|
149
|
-
if (!existsSync(mainPath)) {
|
|
150
|
-
ctx.status = 400;
|
|
151
|
-
ctx.body = {
|
|
152
|
-
code: 400,
|
|
153
|
-
message: '未找到主要入口文件',
|
|
154
|
-
data: null
|
|
155
|
-
};
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const mainDir = dirname(mainPath);
|
|
159
|
-
mainDirMap.set(appName, mainDir);
|
|
160
|
-
}
|
|
161
|
-
const dir = join(mainDirMap.get(appName), 'route', ctx.path?.replace(apiPath, '/api') || '');
|
|
162
|
-
if (existsSync(dir) && fs.statSync(dir).isFile()) {
|
|
163
|
-
ctx.status = 404;
|
|
164
|
-
ctx.body = {
|
|
165
|
-
code: 404,
|
|
166
|
-
message: `API 'route/${ctx.path}' 未找到。`,
|
|
167
|
-
data: 'route path is file not directory'
|
|
168
|
-
};
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
const modulePath = getModuelFile(dir);
|
|
172
|
-
if (!modulePath) {
|
|
173
|
-
ctx.status = 404;
|
|
174
|
-
ctx.body = {
|
|
175
|
-
code: 404,
|
|
176
|
-
message: `API '${ctx.path}' 未找到。`,
|
|
177
|
-
data: 'existsSync modulePath'
|
|
178
|
-
};
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const apiModule = await import(`file://${modulePath}`);
|
|
182
|
-
const handler = apiModule[ctx.method];
|
|
183
|
-
if (!handler || typeof handler !== 'function') {
|
|
184
|
-
ctx.status = 405;
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const middlewares = await collectMiddlewares(modulePath);
|
|
188
|
-
await runMiddlewares(middlewares, ctx, handler);
|
|
189
|
-
}
|
|
190
|
-
catch (err) {
|
|
191
|
-
logger.warn(`Error request ${ctx.path}:`, err?.message || '');
|
|
192
|
-
ctx.status = 500;
|
|
193
|
-
ctx.body = {
|
|
194
|
-
code: 500,
|
|
195
|
-
message: '处理 API 请求时发生错误。',
|
|
196
|
-
error: err.message
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (ctx.method !== 'GET') {
|
|
202
|
-
ctx.status = 405;
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const rootPath = path.join(process.cwd(), 'node_modules', appName);
|
|
206
|
-
const resourcePath = formatPath(ctx.params?.path);
|
|
207
|
-
let root = '';
|
|
208
|
-
try {
|
|
209
|
-
const pkg = require(`${appName}/package`) ?? {};
|
|
210
|
-
root = pkg?.alemonjs?.web?.root ?? '';
|
|
211
|
-
}
|
|
212
|
-
catch (err) {
|
|
213
|
-
ctx.status = 500;
|
|
214
|
-
ctx.body = {
|
|
215
|
-
code: 500,
|
|
216
|
-
message: '加载 package.json 时发生错误。',
|
|
217
|
-
error: err.message
|
|
218
|
-
};
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
const fullPath = root ? path.join(rootPath, root, resourcePath) : path.join(rootPath, resourcePath);
|
|
222
|
-
try {
|
|
223
|
-
const file = await fs.promises.readFile(fullPath);
|
|
224
|
-
const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
|
|
225
|
-
ctx.set('Content-Type', mimeType);
|
|
226
|
-
ctx.body = file;
|
|
227
|
-
ctx.status = 200;
|
|
228
|
-
}
|
|
229
|
-
catch (err) {
|
|
230
|
-
if (err?.status === 404) {
|
|
231
|
-
ctx.status = 404;
|
|
232
|
-
ctx.body = {
|
|
233
|
-
code: 404,
|
|
234
|
-
message: `资源 '${resourcePath}' 在子应用 '${appName}' 中未找到。`,
|
|
235
|
-
data: null
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
logger.warn(`Error request ${ctx.path}:`, err?.message || '');
|
|
240
|
-
ctx.status = 500;
|
|
241
|
-
ctx.body = {
|
|
242
|
-
code: 500,
|
|
243
|
-
message: `加载子应用 '${appName}' 资源时发生服务器错误。`,
|
|
244
|
-
error: err.message
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
router.all('apps/:name', ctx => {
|
|
250
|
-
if (ctx.path === `/apps/${ctx.params.name}`) {
|
|
251
|
-
ctx.redirect(`/apps/${ctx.params.name}/`);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
20
|
|
|
255
21
|
export { router as default };
|
package/lib/cbp/server/main.js
CHANGED
package/lib/client.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export default main;
|
|
1
|
+
export {};
|
package/lib/client.js
CHANGED
|
@@ -29,7 +29,18 @@ import './app/message-api.js';
|
|
|
29
29
|
import './app/message-format.js';
|
|
30
30
|
import './process/platform.js';
|
|
31
31
|
import './process/module.js';
|
|
32
|
+
import { createServer } from './server/main.js';
|
|
32
33
|
|
|
34
|
+
const mainServer = () => {
|
|
35
|
+
const port = process.env.serverPort;
|
|
36
|
+
if (!port) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
createServer(port, () => {
|
|
40
|
+
const httpURL = `http://127.0.0.1:${port}`;
|
|
41
|
+
logger.info(`应用服务器: ${httpURL}`);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
33
44
|
const main = () => {
|
|
34
45
|
const login = process.env.login ?? '';
|
|
35
46
|
const platform = process.env.platform ?? '';
|
|
@@ -61,6 +72,7 @@ const mainProcess = () => {
|
|
|
61
72
|
const data = typeof msg === 'string' ? JSON.parse(msg) : msg;
|
|
62
73
|
if (data?.type === 'start') {
|
|
63
74
|
main();
|
|
75
|
+
mainServer();
|
|
64
76
|
}
|
|
65
77
|
else if (data?.type === 'stop') {
|
|
66
78
|
process.exit(0);
|
|
@@ -73,5 +85,3 @@ const mainProcess = () => {
|
|
|
73
85
|
}
|
|
74
86
|
};
|
|
75
87
|
mainProcess();
|
|
76
|
-
|
|
77
|
-
export { main as default };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type Props = Record<string, any>;
|
|
2
|
+
type VNode = string | number | null | boolean | {
|
|
3
|
+
type: string | any;
|
|
4
|
+
props: Props;
|
|
5
|
+
children: any[];
|
|
6
|
+
};
|
|
7
|
+
export declare const DOCTYPE = "<!DOCTYPE html>";
|
|
8
|
+
export declare function createElement(type: string | any, props?: Props | null, ...children: any[]): VNode;
|
|
9
|
+
export declare class Component<P = object, S = object> {
|
|
10
|
+
props: P;
|
|
11
|
+
state: S;
|
|
12
|
+
constructor(props: P);
|
|
13
|
+
setState(partial: Partial<S>): void;
|
|
14
|
+
render(): VNode;
|
|
15
|
+
static renderToString(this: any): string;
|
|
16
|
+
static renderToHtml(this: any): string;
|
|
17
|
+
}
|
|
18
|
+
export declare function renderToString(vnode: any): string;
|
|
19
|
+
export declare const Html: (first?: any, ...restChildren: any[]) => VNode;
|
|
20
|
+
export declare const Head: (first?: any, ...restChildren: any[]) => VNode;
|
|
21
|
+
export declare const Body: (first?: any, ...restChildren: any[]) => VNode;
|
|
22
|
+
export declare const Title: (first?: any, ...restChildren: any[]) => VNode;
|
|
23
|
+
export declare const Style: (first?: any, ...restChildren: any[]) => VNode;
|
|
24
|
+
export declare const H1: (first?: any, ...restChildren: any[]) => VNode;
|
|
25
|
+
export declare const P: (first?: any, ...restChildren: any[]) => VNode;
|
|
26
|
+
export declare const A: (first?: any, ...restChildren: any[]) => VNode;
|
|
27
|
+
export declare const Div: (first?: any, ...restChildren: any[]) => VNode;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const DOCTYPE = '<!DOCTYPE html>';
|
|
2
|
+
const escapeHtml = (s) => String(s)
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''');
|
|
8
|
+
function createElement(type, props, ...children) {
|
|
9
|
+
props = props || {};
|
|
10
|
+
const flatChildren = children.flat(Infinity).filter((c) => c !== false && c !== null && c !== undefined);
|
|
11
|
+
return {
|
|
12
|
+
type,
|
|
13
|
+
props,
|
|
14
|
+
children: flatChildren
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
class Component {
|
|
18
|
+
props;
|
|
19
|
+
state;
|
|
20
|
+
constructor(props) {
|
|
21
|
+
this.props = props;
|
|
22
|
+
this.state = {};
|
|
23
|
+
}
|
|
24
|
+
setState(partial) {
|
|
25
|
+
this.state = Object.assign({}, this.state, partial);
|
|
26
|
+
}
|
|
27
|
+
render() {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
static renderToString() {
|
|
31
|
+
return renderToString(createElement(this, null));
|
|
32
|
+
}
|
|
33
|
+
static renderToHtml() {
|
|
34
|
+
return DOCTYPE + this.renderToString();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function attrsToString(props = {}) {
|
|
38
|
+
const parts = [];
|
|
39
|
+
for (const key of Object.keys(props)) {
|
|
40
|
+
if (key === 'children') {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const val = props[key];
|
|
44
|
+
if (val === false || val === undefined || val === null) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (key === 'className') {
|
|
48
|
+
parts.push(`class="${escapeHtml(String(val))}"`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (key === 'style' && typeof val === 'object') {
|
|
52
|
+
const styleStr = Object.entries(val)
|
|
53
|
+
.map(([k, v]) => `${k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase())}:${v}`)
|
|
54
|
+
.join(';');
|
|
55
|
+
parts.push(`style="${escapeHtml(styleStr)}"`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (val === true) {
|
|
59
|
+
parts.push(`${key}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
parts.push(`${key}="${escapeHtml(String(val))}"`);
|
|
63
|
+
}
|
|
64
|
+
return parts.length ? ' ' + parts.join(' ') : '';
|
|
65
|
+
}
|
|
66
|
+
const voidTags = new Set([
|
|
67
|
+
'area',
|
|
68
|
+
'base',
|
|
69
|
+
'br',
|
|
70
|
+
'col',
|
|
71
|
+
'embed',
|
|
72
|
+
'hr',
|
|
73
|
+
'img',
|
|
74
|
+
'input',
|
|
75
|
+
'link',
|
|
76
|
+
'meta',
|
|
77
|
+
'param',
|
|
78
|
+
'source',
|
|
79
|
+
'track',
|
|
80
|
+
'wbr'
|
|
81
|
+
]);
|
|
82
|
+
function renderToString(vnode) {
|
|
83
|
+
if (vnode === null || vnode === undefined || vnode === false) {
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') {
|
|
87
|
+
return escapeHtml(String(vnode));
|
|
88
|
+
}
|
|
89
|
+
if (typeof vnode === 'object' && vnode.type === undefined) {
|
|
90
|
+
return (vnode.children ?? []).map(renderToString).join('');
|
|
91
|
+
}
|
|
92
|
+
if (typeof vnode.type === 'function') {
|
|
93
|
+
const Comp = vnode.type;
|
|
94
|
+
if (Comp.prototype && typeof Comp.prototype.render === 'function') {
|
|
95
|
+
const inst = new (Comp)(Object.assign({}, vnode.props, { children: vnode.children }));
|
|
96
|
+
const rendered = inst.render();
|
|
97
|
+
return renderToString(rendered);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const rendered = (Comp)(Object.assign({}, vnode.props, { children: vnode.children }));
|
|
101
|
+
return renderToString(rendered);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const tag = String(vnode.type);
|
|
105
|
+
const props = vnode.props ?? {};
|
|
106
|
+
if (props?.dangerouslySetInnerHTML && typeof props.dangerouslySetInnerHTML === 'object' && props.dangerouslySetInnerHTML.__html !== null) {
|
|
107
|
+
const attrs = attrsToString(props);
|
|
108
|
+
const inner = String(props.dangerouslySetInnerHTML.__html);
|
|
109
|
+
return `<${tag}${attrs}>${inner}</${tag}>`;
|
|
110
|
+
}
|
|
111
|
+
const attrs = attrsToString(props);
|
|
112
|
+
if (voidTags.has(tag.toLowerCase())) {
|
|
113
|
+
return `<${tag}${attrs} />`;
|
|
114
|
+
}
|
|
115
|
+
const children = (vnode.children ?? []).map(renderToString).join('');
|
|
116
|
+
return `<${tag}${attrs}>${children}</${tag}>`;
|
|
117
|
+
}
|
|
118
|
+
function makeTag(tag) {
|
|
119
|
+
return function tagFactory(first, ...restChildren) {
|
|
120
|
+
const looksLikeProps = first !== null
|
|
121
|
+
&& typeof first === 'object'
|
|
122
|
+
&& !Array.isArray(first)
|
|
123
|
+
&& Object.prototype.toString.call(first) === '[object Object]';
|
|
124
|
+
if (looksLikeProps) {
|
|
125
|
+
return createElement(tag, first, ...restChildren);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const children = [];
|
|
129
|
+
if (first !== undefined && first !== null) {
|
|
130
|
+
children.push(first);
|
|
131
|
+
}
|
|
132
|
+
children.push(...restChildren);
|
|
133
|
+
return createElement(tag, null, ...children);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const Html = makeTag('html');
|
|
138
|
+
const Head = makeTag('head');
|
|
139
|
+
const Body = makeTag('body');
|
|
140
|
+
const Title = makeTag('title');
|
|
141
|
+
const Style = makeTag('style');
|
|
142
|
+
const H1 = makeTag('h1');
|
|
143
|
+
const P = makeTag('p');
|
|
144
|
+
const A = makeTag('a');
|
|
145
|
+
const Div = makeTag('div');
|
|
146
|
+
|
|
147
|
+
export { A, Body, Component, DOCTYPE, Div, H1, Head, Html, P, Style, Title, createElement, renderToString };
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -25,4 +25,3 @@ export { useObserver, useSubscribe } from './app/hook-use-subscribe.js';
|
|
|
25
25
|
export { createDataFormat, createEventValue, format, sendToChannel, sendToUser } from './app/message-api.js';
|
|
26
26
|
export { Ark, BT, Image, ImageFile, ImageURL, Link, MD, Mention, Text } from './app/message-format.js';
|
|
27
27
|
export { start } from './main.js';
|
|
28
|
-
export { App } from './app.js';
|
package/lib/main.js
CHANGED
|
@@ -50,7 +50,9 @@ const start = (options = {}) => {
|
|
|
50
50
|
options = { input: options };
|
|
51
51
|
}
|
|
52
52
|
const port = createOptionsByKey(options, 'port', defaultPort);
|
|
53
|
+
const serverPort = createOptionsByKey(options, 'serverPort', '');
|
|
53
54
|
process.env.port = port;
|
|
55
|
+
process.env.serverPort = serverPort;
|
|
54
56
|
cbpServer(port, () => {
|
|
55
57
|
const httpURL = `http://127.0.0.1:${port}`;
|
|
56
58
|
const wsURL = `ws://127.0.0.1:${port}`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const createServer: (port: any, listeningListener: any) => void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Koa from 'koa';
|
|
2
|
+
import koaCors from '@koa/cors';
|
|
3
|
+
import router from './routers/router.js';
|
|
4
|
+
import { ResultCode } from '../core/variable.js';
|
|
5
|
+
import 'fs';
|
|
6
|
+
import 'path';
|
|
7
|
+
import 'yaml';
|
|
8
|
+
import '../core/utils.js';
|
|
9
|
+
|
|
10
|
+
const createServer = (port, listeningListener) => {
|
|
11
|
+
try {
|
|
12
|
+
const app = new Koa();
|
|
13
|
+
app.use(router.routes());
|
|
14
|
+
app.use(router.allowedMethods());
|
|
15
|
+
app.use(koaCors({
|
|
16
|
+
origin: '*',
|
|
17
|
+
allowMethods: ['GET', 'POST', 'PUT', 'DELETE']
|
|
18
|
+
}));
|
|
19
|
+
app.listen(port, listeningListener);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
logger.error({
|
|
23
|
+
code: ResultCode.FailInternal,
|
|
24
|
+
message: '创建应用服务器失败',
|
|
25
|
+
data: error
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { createServer };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Component, Head, Title, Style, Body, H1, P, A, Div, Html } from '../../core/react.js';
|
|
2
|
+
|
|
3
|
+
class App extends Component {
|
|
4
|
+
render() {
|
|
5
|
+
const style = `
|
|
6
|
+
body {
|
|
7
|
+
width: 35em;
|
|
8
|
+
margin: 0 auto;
|
|
9
|
+
font-family: Tahoma, Verdana, Arial, sans-serif;
|
|
10
|
+
background-color: #f8f8f8;
|
|
11
|
+
color: #333;
|
|
12
|
+
}
|
|
13
|
+
h1 {
|
|
14
|
+
color: #0099cc;
|
|
15
|
+
margin-top: 2em;
|
|
16
|
+
}
|
|
17
|
+
.footer {
|
|
18
|
+
margin-top: 3em;
|
|
19
|
+
font-size: 0.9em;
|
|
20
|
+
color: #888;
|
|
21
|
+
}
|
|
22
|
+
a { color: #0099cc; }
|
|
23
|
+
`;
|
|
24
|
+
const head = Head(null, Title('欢迎使用 AlemonJS!'), Style(style));
|
|
25
|
+
const body = Body(null, H1('AlemonJS 启动成功!'), P(null, '已成功通过 ', A({ href: 'https://alemonjs.com', target: '_blank' }, 'AlemonJS 框架'), ' 启动。'), P(null, '如果想访问主应用,请访问, ', A({ href: '/app', target: '_blank' }, '/app'), '(对应根目录index.html)'), P(null, '如果想访问其他应用,请访问 ', A({ href: '/apps/[package-name]', target: '_blank' }, '/apps/[package-name]'), '。(对应/packages/[package-name]/index.html)'), Div({ className: 'footer' }, '— 感谢选择 AlemonJS。'));
|
|
26
|
+
return Html(null, head, body);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var hello = App.renderToHtml();
|
|
30
|
+
|
|
31
|
+
export { hello as default };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare function collectMiddlewares(routeFile: string): Promise<Array<(ctx: any, next: () => Promise<void>) => Promise<void>>>;
|
|
2
|
+
export declare function runMiddlewares(middlewares: Array<(ctx: any, next: () => Promise<void>) => Promise<void>>, ctx: any, handler: (ctx: any) => Promise<void>): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import path, { dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
async function collectMiddlewares(routeFile) {
|
|
5
|
+
const middlewares = [];
|
|
6
|
+
let dir = dirname(routeFile);
|
|
7
|
+
const suffixes = ['.ts', '.js', '.cjs', '.mjs', '.tsx', '.jsx'];
|
|
8
|
+
while (true) {
|
|
9
|
+
for (const ext of suffixes) {
|
|
10
|
+
const mwPath = path.join(dir, `_middleware${ext}`);
|
|
11
|
+
if (existsSync(mwPath)) {
|
|
12
|
+
const module = await import(`file://${mwPath}`);
|
|
13
|
+
const mw = module?.default ?? {};
|
|
14
|
+
if (typeof mw === 'function') {
|
|
15
|
+
middlewares.unshift(mw);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const parent = dirname(dir);
|
|
20
|
+
if (parent === dir) {
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
return middlewares;
|
|
26
|
+
}
|
|
27
|
+
async function runMiddlewares(middlewares, ctx, handler) {
|
|
28
|
+
let idx = 0;
|
|
29
|
+
async function dispatch() {
|
|
30
|
+
if (idx < middlewares.length) {
|
|
31
|
+
await middlewares[idx++](ctx, dispatch);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
await handler(ctx);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await dispatch();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { collectMiddlewares, runMiddlewares };
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import KoaRouter from 'koa-router';
|
|
2
|
+
import fs, { existsSync } from 'fs';
|
|
3
|
+
import path, { join, dirname } from 'path';
|
|
4
|
+
import mime from 'mime-types';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import hello from './hello.html.js';
|
|
7
|
+
import { getModuelFile, formatPath } from './utils.js';
|
|
8
|
+
import { collectMiddlewares, runMiddlewares } from './middleware.js';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const mainDirMap = new Map();
|
|
12
|
+
const router = new KoaRouter({
|
|
13
|
+
prefix: '/'
|
|
14
|
+
});
|
|
15
|
+
router.get('/', ctx => {
|
|
16
|
+
ctx.status = 200;
|
|
17
|
+
ctx.set('Content-Type', 'text/html; charset=utf-8');
|
|
18
|
+
ctx.body = hello;
|
|
19
|
+
});
|
|
20
|
+
router.get('api/online', ctx => {
|
|
21
|
+
ctx.status = 200;
|
|
22
|
+
ctx.body = {
|
|
23
|
+
code: 200,
|
|
24
|
+
message: 'service online',
|
|
25
|
+
data: null
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
router.all('app/{*path}', async (ctx) => {
|
|
29
|
+
if (!process.env.input) {
|
|
30
|
+
ctx.status = 400;
|
|
31
|
+
ctx.body = {
|
|
32
|
+
code: 400,
|
|
33
|
+
message: '没有主要入口文件',
|
|
34
|
+
data: null
|
|
35
|
+
};
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const rootPath = process.cwd();
|
|
39
|
+
const apiPath = '/app/api';
|
|
40
|
+
if (ctx.path.startsWith(apiPath)) {
|
|
41
|
+
const mainPath = join(rootPath, process.env.input);
|
|
42
|
+
if (!existsSync(mainPath)) {
|
|
43
|
+
ctx.status = 400;
|
|
44
|
+
ctx.body = {
|
|
45
|
+
code: 400,
|
|
46
|
+
message: '未找到主要入口文件',
|
|
47
|
+
data: 'existsSync input'
|
|
48
|
+
};
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const mainDir = dirname(mainPath);
|
|
52
|
+
try {
|
|
53
|
+
const dir = join(mainDir, 'route', ctx.path?.replace(apiPath, '/api') || '');
|
|
54
|
+
if (existsSync(dir) && fs.statSync(dir).isFile()) {
|
|
55
|
+
ctx.status = 404;
|
|
56
|
+
ctx.body = {
|
|
57
|
+
code: 404,
|
|
58
|
+
message: `API '${ctx.path}' 未找到。`,
|
|
59
|
+
data: 'route path is file not directory'
|
|
60
|
+
};
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const modulePath = getModuelFile(dir);
|
|
64
|
+
if (!modulePath) {
|
|
65
|
+
ctx.status = 404;
|
|
66
|
+
ctx.body = {
|
|
67
|
+
code: 404,
|
|
68
|
+
message: `API '${ctx.path}' 未找到。`,
|
|
69
|
+
data: 'existsSync modulePath'
|
|
70
|
+
};
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const apiModule = await import(`file://${modulePath}`);
|
|
74
|
+
const handler = apiModule[ctx.method];
|
|
75
|
+
if (!handler || typeof handler !== 'function') {
|
|
76
|
+
ctx.status = 405;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const middlewares = await collectMiddlewares(modulePath);
|
|
80
|
+
await runMiddlewares(middlewares, ctx, handler);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.error(`Error handling API request ${ctx.path}`);
|
|
84
|
+
ctx.status = 500;
|
|
85
|
+
ctx.body = {
|
|
86
|
+
code: 500,
|
|
87
|
+
message: '处理 API 请求时发生错误。',
|
|
88
|
+
error: err.message
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (ctx.method !== 'GET') {
|
|
94
|
+
ctx.status = 405;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let root = '';
|
|
98
|
+
const resourcePath = formatPath(ctx.params?.path);
|
|
99
|
+
try {
|
|
100
|
+
const pkg = require(path.join(rootPath, 'package.json')) ?? {};
|
|
101
|
+
root = pkg.alemonjs?.web?.root ?? '';
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
ctx.status = 500;
|
|
105
|
+
ctx.body = {
|
|
106
|
+
code: 500,
|
|
107
|
+
message: '加载 package.json 时发生错误。',
|
|
108
|
+
error: err.message
|
|
109
|
+
};
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const fullPath = root ? path.join(rootPath, root, resourcePath) : path.join(rootPath, resourcePath);
|
|
113
|
+
try {
|
|
114
|
+
const file = await fs.promises.readFile(fullPath);
|
|
115
|
+
const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
|
|
116
|
+
ctx.set('Content-Type', mimeType);
|
|
117
|
+
ctx.body = file;
|
|
118
|
+
ctx.status = 200;
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (err?.status === 404) {
|
|
122
|
+
ctx.status = 404;
|
|
123
|
+
ctx.body = {
|
|
124
|
+
code: 404,
|
|
125
|
+
message: '资源中未找到。',
|
|
126
|
+
data: null
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
ctx.status = 500;
|
|
131
|
+
ctx.body = {
|
|
132
|
+
code: 500,
|
|
133
|
+
message: '加载资源时发生服务器错误。',
|
|
134
|
+
error: err.message
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
router.all('app', ctx => {
|
|
140
|
+
ctx.redirect('/app/');
|
|
141
|
+
});
|
|
142
|
+
router.all('apps/:app/{*path}', async (ctx) => {
|
|
143
|
+
const appName = ctx.params.app;
|
|
144
|
+
const apiPath = `/apps/${appName}/api`;
|
|
145
|
+
if (ctx.path.startsWith(apiPath)) {
|
|
146
|
+
try {
|
|
147
|
+
if (!mainDirMap.has(appName)) {
|
|
148
|
+
const mainPath = require.resolve(appName);
|
|
149
|
+
if (!existsSync(mainPath)) {
|
|
150
|
+
ctx.status = 400;
|
|
151
|
+
ctx.body = {
|
|
152
|
+
code: 400,
|
|
153
|
+
message: '未找到主要入口文件',
|
|
154
|
+
data: null
|
|
155
|
+
};
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const mainDir = dirname(mainPath);
|
|
159
|
+
mainDirMap.set(appName, mainDir);
|
|
160
|
+
}
|
|
161
|
+
const dir = join(mainDirMap.get(appName), 'route', ctx.path?.replace(apiPath, '/api') || '');
|
|
162
|
+
if (existsSync(dir) && fs.statSync(dir).isFile()) {
|
|
163
|
+
ctx.status = 404;
|
|
164
|
+
ctx.body = {
|
|
165
|
+
code: 404,
|
|
166
|
+
message: `API 'route/${ctx.path}' 未找到。`,
|
|
167
|
+
data: 'route path is file not directory'
|
|
168
|
+
};
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const modulePath = getModuelFile(dir);
|
|
172
|
+
if (!modulePath) {
|
|
173
|
+
ctx.status = 404;
|
|
174
|
+
ctx.body = {
|
|
175
|
+
code: 404,
|
|
176
|
+
message: `API '${ctx.path}' 未找到。`,
|
|
177
|
+
data: 'existsSync modulePath'
|
|
178
|
+
};
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const apiModule = await import(`file://${modulePath}`);
|
|
182
|
+
const handler = apiModule[ctx.method];
|
|
183
|
+
if (!handler || typeof handler !== 'function') {
|
|
184
|
+
ctx.status = 405;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const middlewares = await collectMiddlewares(modulePath);
|
|
188
|
+
await runMiddlewares(middlewares, ctx, handler);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
logger.warn(`Error request ${ctx.path}:`, err?.message || '');
|
|
192
|
+
ctx.status = 500;
|
|
193
|
+
ctx.body = {
|
|
194
|
+
code: 500,
|
|
195
|
+
message: '处理 API 请求时发生错误。',
|
|
196
|
+
error: err.message
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (ctx.method !== 'GET') {
|
|
202
|
+
ctx.status = 405;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const rootPath = path.join(process.cwd(), 'node_modules', appName);
|
|
206
|
+
const resourcePath = formatPath(ctx.params?.path);
|
|
207
|
+
let root = '';
|
|
208
|
+
try {
|
|
209
|
+
const pkg = require(`${appName}/package`) ?? {};
|
|
210
|
+
root = pkg?.alemonjs?.web?.root ?? '';
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
ctx.status = 500;
|
|
214
|
+
ctx.body = {
|
|
215
|
+
code: 500,
|
|
216
|
+
message: '加载 package.json 时发生错误。',
|
|
217
|
+
error: err.message
|
|
218
|
+
};
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const fullPath = root ? path.join(rootPath, root, resourcePath) : path.join(rootPath, resourcePath);
|
|
222
|
+
try {
|
|
223
|
+
const file = await fs.promises.readFile(fullPath);
|
|
224
|
+
const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
|
|
225
|
+
ctx.set('Content-Type', mimeType);
|
|
226
|
+
ctx.body = file;
|
|
227
|
+
ctx.status = 200;
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
if (err?.status === 404) {
|
|
231
|
+
ctx.status = 404;
|
|
232
|
+
ctx.body = {
|
|
233
|
+
code: 404,
|
|
234
|
+
message: `资源 '${resourcePath}' 在子应用 '${appName}' 中未找到。`,
|
|
235
|
+
data: null
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
logger.warn(`Error request ${ctx.path}:`, err?.message || '');
|
|
240
|
+
ctx.status = 500;
|
|
241
|
+
ctx.body = {
|
|
242
|
+
code: 500,
|
|
243
|
+
message: `加载子应用 '${appName}' 资源时发生服务器错误。`,
|
|
244
|
+
error: err.message
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
router.all('apps/:name', ctx => {
|
|
250
|
+
if (ctx.path === `/apps/${ctx.params.name}`) {
|
|
251
|
+
ctx.redirect(`/apps/${ctx.params.name}/`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
export { router as default };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs, { existsSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
const getModuelFile = (dir) => {
|
|
4
|
+
const dirMap = {
|
|
5
|
+
'.js': `${dir}.js`,
|
|
6
|
+
'.jsx': `${dir}.jsx`,
|
|
7
|
+
'.mjs': `${dir}.mjs`,
|
|
8
|
+
'.cjs': `${dir}.cjs`,
|
|
9
|
+
'/index.js': `${dir}/index.js`,
|
|
10
|
+
'/index.jsx': `${dir}/index.jsx`,
|
|
11
|
+
'/index.mjs': `${dir}/index.mjs`,
|
|
12
|
+
'/index.cjs': `${dir}/index.cjs`,
|
|
13
|
+
'.ts': `${dir}.ts`,
|
|
14
|
+
'.tsx': `${dir}.tsx`,
|
|
15
|
+
'/index.ts': `${dir}/index.ts`,
|
|
16
|
+
'/index.tsx': `${dir}/index.tsx`
|
|
17
|
+
};
|
|
18
|
+
for (const key in dirMap) {
|
|
19
|
+
const filePath = dirMap[key];
|
|
20
|
+
if (existsSync(filePath) && fs.statSync(filePath)) {
|
|
21
|
+
return filePath;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return '';
|
|
25
|
+
};
|
|
26
|
+
const formatPath = (path) => {
|
|
27
|
+
if (!path || path === '/') {
|
|
28
|
+
return '/index.html';
|
|
29
|
+
}
|
|
30
|
+
const pates = path.split('/');
|
|
31
|
+
const lastPath = pates[pates.length - 1];
|
|
32
|
+
if (lastPath.includes('.')) {
|
|
33
|
+
return path;
|
|
34
|
+
}
|
|
35
|
+
path += '.html';
|
|
36
|
+
return path;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export { formatPath, getModuelFile };
|