@akinon/ui-shell-dev 1.0.0
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/dist/assets/index-CuIRnKVE.css +1 -0
- package/dist/assets/index-DUUd718F.js +193 -0
- package/dist/index.html +33 -0
- package/index.html +32 -0
- package/index.mjs +1 -0
- package/package.json +49 -0
- package/src/components/AppLayout.tsx +64 -0
- package/src/components/Header.tsx +66 -0
- package/src/components/PluginLoader.tsx +112 -0
- package/src/components/Sidebar.tsx +74 -0
- package/src/index.tsx +4 -0
- package/src/main.tsx +29 -0
- package/src/server/dev-server.mjs +193 -0
- package/src/server/dev-server.ts +142 -0
- package/src/shell-app.tsx +17 -0
- package/src/themes/index.ts +37 -0
- package/src/themes/omnitron.ts +66 -0
- package/src/types.ts +40 -0
- package/tsconfig.json +11 -0
- package/vite.config.ts +25 -0
package/dist/index.html
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Akinon UI Shell Development</title>
|
|
8
|
+
<!--SHELL_CONFIG-->
|
|
9
|
+
<style>
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
font-family: 'Jost Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
13
|
+
-webkit-font-smoothing: antialiased;
|
|
14
|
+
-moz-osx-font-smoothing: grayscale;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#root {
|
|
18
|
+
width: 100vw;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* {
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
<script type="module" crossorigin src="/assets/index-DUUd718F.js"></script>
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CuIRnKVE.css">
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<div id="root"></div>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
package/index.html
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Akinon UI Shell Development</title>
|
|
8
|
+
<!--SHELL_CONFIG-->
|
|
9
|
+
<style>
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
font-family: 'Jost Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
13
|
+
-webkit-font-smoothing: antialiased;
|
|
14
|
+
-moz-osx-font-smoothing: grayscale;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#root {
|
|
18
|
+
width: 100vw;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* {
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<div id="root"></div>
|
|
30
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
package/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ShellDevServer } from './src/server/dev-server.mjs';
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@akinon/ui-shell-dev",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Development shell application for Akinon UI Protocol plugins",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.mjs",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"index.html",
|
|
12
|
+
"index.mjs",
|
|
13
|
+
"vite.config.ts",
|
|
14
|
+
"tsconfig.json"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@akinon/ui-system": "^1.0.0",
|
|
18
|
+
"@akinon/ui-layout": "^1.0.0",
|
|
19
|
+
"@akinon/ui-menu": "^1.0.0",
|
|
20
|
+
"@akinon/ui-breadcrumb": "^1.0.0",
|
|
21
|
+
"@akinon/ui-button": "^1.0.0",
|
|
22
|
+
"@akinon/icons": "^1.0.0",
|
|
23
|
+
"@akinon/fonts-jost-variable": "^1.0.0",
|
|
24
|
+
"react": "^18.2.0",
|
|
25
|
+
"react-dom": "^18.2.0",
|
|
26
|
+
"express": "^4.18.2",
|
|
27
|
+
"cors": "^2.8.5",
|
|
28
|
+
"vite": "^5.2.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@akinon/typescript-config": "^1.0.0",
|
|
32
|
+
"@types/react": "^18.2.66",
|
|
33
|
+
"@types/react-dom": "^18.2.22",
|
|
34
|
+
"@types/express": "^4.17.21",
|
|
35
|
+
"@types/cors": "^2.8.17",
|
|
36
|
+
"@vitejs/plugin-react-swc": "^3.7.2",
|
|
37
|
+
"typescript": "^5.2.2"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "vite build",
|
|
44
|
+
"dev": "vite",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"lint": "eslint",
|
|
47
|
+
"clean": "rm -rf dist node_modules"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Layout } from '@akinon/ui-layout';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { getSidebarItems, getTheme } from '../themes';
|
|
5
|
+
import type { ShellConfig } from '../types';
|
|
6
|
+
import { Header } from './Header';
|
|
7
|
+
import { PluginLoader } from './PluginLoader';
|
|
8
|
+
import { Sidebar } from './Sidebar';
|
|
9
|
+
|
|
10
|
+
interface AppLayoutProps {
|
|
11
|
+
config: ShellConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const AppLayout: React.FC<AppLayoutProps> = ({ config }) => {
|
|
15
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
16
|
+
const theme = getTheme(config.shell.theme);
|
|
17
|
+
const sidebarItems =
|
|
18
|
+
config.sidebar?.items || getSidebarItems(config.shell.theme);
|
|
19
|
+
|
|
20
|
+
const sidebarWidth = collapsed
|
|
21
|
+
? theme.sidebar.collapsedWidth
|
|
22
|
+
: theme.sidebar.width;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Layout
|
|
26
|
+
style={{ minHeight: '100vh', backgroundColor: theme.colors.background }}
|
|
27
|
+
>
|
|
28
|
+
<Sidebar items={sidebarItems} theme={theme} collapsed={collapsed} />
|
|
29
|
+
|
|
30
|
+
<Layout
|
|
31
|
+
style={{ marginLeft: sidebarWidth, transition: 'margin-left 0.2s' }}
|
|
32
|
+
>
|
|
33
|
+
<Header
|
|
34
|
+
theme={theme}
|
|
35
|
+
title={config.shell.title}
|
|
36
|
+
collapsed={collapsed}
|
|
37
|
+
onToggleCollapse={() => setCollapsed(!collapsed)}
|
|
38
|
+
sidebarWidth={sidebarWidth}
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
<Layout.Content
|
|
42
|
+
style={{
|
|
43
|
+
marginTop: theme.header.height,
|
|
44
|
+
padding: '24px',
|
|
45
|
+
minHeight: `calc(100vh - ${theme.header.height}px)`,
|
|
46
|
+
backgroundColor: theme.colors.background
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
style={{
|
|
51
|
+
backgroundColor: 'white',
|
|
52
|
+
borderRadius: '8px',
|
|
53
|
+
height: 'calc(100vh - 112px)',
|
|
54
|
+
overflow: 'hidden',
|
|
55
|
+
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)'
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<PluginLoader config={config} />
|
|
59
|
+
</div>
|
|
60
|
+
</Layout.Content>
|
|
61
|
+
</Layout>
|
|
62
|
+
</Layout>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Button } from '@akinon/ui-button';
|
|
2
|
+
import { Layout } from '@akinon/ui-layout';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import type { ThemeConfig } from '../types';
|
|
6
|
+
|
|
7
|
+
interface HeaderProps {
|
|
8
|
+
theme: ThemeConfig;
|
|
9
|
+
title: string;
|
|
10
|
+
collapsed: boolean;
|
|
11
|
+
onToggleCollapse: () => void;
|
|
12
|
+
sidebarWidth: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Header: React.FC<HeaderProps> = ({
|
|
16
|
+
theme,
|
|
17
|
+
title,
|
|
18
|
+
collapsed,
|
|
19
|
+
onToggleCollapse,
|
|
20
|
+
sidebarWidth
|
|
21
|
+
}) => {
|
|
22
|
+
return (
|
|
23
|
+
<Layout.Header
|
|
24
|
+
style={{
|
|
25
|
+
backgroundColor: theme.colors.header,
|
|
26
|
+
padding: '0 24px',
|
|
27
|
+
height: theme.header.height,
|
|
28
|
+
lineHeight: `${theme.header.height}px`,
|
|
29
|
+
position: 'fixed',
|
|
30
|
+
top: 0,
|
|
31
|
+
right: 0,
|
|
32
|
+
left: sidebarWidth,
|
|
33
|
+
zIndex: 100,
|
|
34
|
+
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
|
|
35
|
+
display: 'flex',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
justifyContent: 'space-between',
|
|
38
|
+
transition: 'left 0.2s'
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
|
42
|
+
<Button
|
|
43
|
+
type="text"
|
|
44
|
+
onClick={onToggleCollapse}
|
|
45
|
+
style={{ fontSize: '16px' }}
|
|
46
|
+
>
|
|
47
|
+
{collapsed ? 'โฐ' : 'โฌ
'}
|
|
48
|
+
</Button>
|
|
49
|
+
<h1 style={{ margin: 0, fontSize: '18px', fontWeight: '500' }}>
|
|
50
|
+
{title}
|
|
51
|
+
</h1>
|
|
52
|
+
</div>
|
|
53
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
|
54
|
+
<span style={{ color: '#666' }}>Development Mode</span>
|
|
55
|
+
<div
|
|
56
|
+
style={{
|
|
57
|
+
width: 8,
|
|
58
|
+
height: 8,
|
|
59
|
+
borderRadius: '50%',
|
|
60
|
+
backgroundColor: '#52c41a'
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</Layout.Header>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { ShellConfig } from '../types';
|
|
4
|
+
|
|
5
|
+
interface PluginLoaderProps {
|
|
6
|
+
config: ShellConfig;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const PluginLoader: React.FC<PluginLoaderProps> = ({ config }) => {
|
|
10
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const timer = setTimeout(() => {
|
|
15
|
+
setIsLoading(false);
|
|
16
|
+
}, 1000);
|
|
17
|
+
|
|
18
|
+
return () => clearTimeout(timer);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const handleIframeLoad = () => {
|
|
22
|
+
setIsLoading(false);
|
|
23
|
+
setError(null);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleIframeError = () => {
|
|
27
|
+
setIsLoading(false);
|
|
28
|
+
setError('Failed to load plugin. Make sure your plugin is running.');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (error) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
padding: '48px',
|
|
36
|
+
textAlign: 'center',
|
|
37
|
+
backgroundColor: '#fff',
|
|
38
|
+
borderRadius: '8px',
|
|
39
|
+
margin: '24px',
|
|
40
|
+
border: '1px solid #f0f0f0'
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<div style={{ fontSize: '48px', marginBottom: '16px' }}>โ ๏ธ</div>
|
|
44
|
+
<h3 style={{ margin: '0 0 8px 0', color: '#ff4d4f' }}>
|
|
45
|
+
Plugin Load Error
|
|
46
|
+
</h3>
|
|
47
|
+
<p style={{ margin: '0 0 16px 0', color: '#666' }}>{error}</p>
|
|
48
|
+
<p style={{ margin: 0, fontSize: '14px', color: '#999' }}>
|
|
49
|
+
Expected plugin URL: <code>{config.plugin.url}</code>
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
|
57
|
+
{isLoading && (
|
|
58
|
+
<div
|
|
59
|
+
style={{
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
top: 0,
|
|
62
|
+
left: 0,
|
|
63
|
+
right: 0,
|
|
64
|
+
bottom: 0,
|
|
65
|
+
display: 'flex',
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
justifyContent: 'center',
|
|
68
|
+
backgroundColor: '#fff',
|
|
69
|
+
zIndex: 10
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<div style={{ textAlign: 'center' }}>
|
|
73
|
+
<div
|
|
74
|
+
style={{
|
|
75
|
+
width: '32px',
|
|
76
|
+
height: '32px',
|
|
77
|
+
border: '3px solid #f3f3f3',
|
|
78
|
+
borderTop: '3px solid #1890ff',
|
|
79
|
+
borderRadius: '50%',
|
|
80
|
+
animation: 'spin 1s linear infinite',
|
|
81
|
+
margin: '0 auto 16px'
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
<p style={{ margin: 0, color: '#666' }}>Loading plugin...</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
<iframe
|
|
89
|
+
src={config.plugin.url}
|
|
90
|
+
style={{
|
|
91
|
+
width: '100%',
|
|
92
|
+
height: '100%',
|
|
93
|
+
border: 'none',
|
|
94
|
+
backgroundColor: 'white'
|
|
95
|
+
}}
|
|
96
|
+
onLoad={handleIframeLoad}
|
|
97
|
+
onError={handleIframeError}
|
|
98
|
+
title="Plugin Content"
|
|
99
|
+
/>
|
|
100
|
+
<style>{`
|
|
101
|
+
@keyframes spin {
|
|
102
|
+
0% {
|
|
103
|
+
transform: rotate(0deg);
|
|
104
|
+
}
|
|
105
|
+
100% {
|
|
106
|
+
transform: rotate(360deg);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`}</style>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Layout } from '@akinon/ui-layout';
|
|
2
|
+
import { Menu } from '@akinon/ui-menu';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import type { SidebarItem, ThemeConfig } from '../types';
|
|
6
|
+
|
|
7
|
+
interface SidebarProps {
|
|
8
|
+
items: SidebarItem[];
|
|
9
|
+
theme: ThemeConfig;
|
|
10
|
+
collapsed?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Sidebar: React.FC<SidebarProps> = ({
|
|
14
|
+
items,
|
|
15
|
+
theme,
|
|
16
|
+
collapsed = false
|
|
17
|
+
}) => {
|
|
18
|
+
const menuItems = items.map(item => ({
|
|
19
|
+
key: item.key,
|
|
20
|
+
label: item.label,
|
|
21
|
+
icon: item.icon,
|
|
22
|
+
children: item.children?.map(child => ({
|
|
23
|
+
key: child.key,
|
|
24
|
+
label: child.label
|
|
25
|
+
}))
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const selectedKeys = items.filter(item => item.active).map(item => item.key);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Layout.Sider
|
|
32
|
+
width={theme.sidebar.width}
|
|
33
|
+
collapsedWidth={theme.sidebar.collapsedWidth}
|
|
34
|
+
collapsed={collapsed}
|
|
35
|
+
style={{
|
|
36
|
+
backgroundColor: theme.colors.sidebar,
|
|
37
|
+
height: '100vh',
|
|
38
|
+
position: 'fixed',
|
|
39
|
+
left: 0,
|
|
40
|
+
top: 0,
|
|
41
|
+
bottom: 0,
|
|
42
|
+
zIndex: 200
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
46
|
+
style={{
|
|
47
|
+
height: theme.header.height,
|
|
48
|
+
display: 'flex',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
|
52
|
+
color: 'white',
|
|
53
|
+
fontSize: '18px',
|
|
54
|
+
fontWeight: 'bold'
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{collapsed ? 'A' : 'Akinon'}
|
|
58
|
+
</div>
|
|
59
|
+
<Menu
|
|
60
|
+
theme="dark"
|
|
61
|
+
mode="inline"
|
|
62
|
+
selectedKeys={selectedKeys}
|
|
63
|
+
defaultOpenKeys={items
|
|
64
|
+
.filter(item => item.children)
|
|
65
|
+
.map(item => item.key)}
|
|
66
|
+
items={menuItems}
|
|
67
|
+
style={{
|
|
68
|
+
backgroundColor: 'transparent',
|
|
69
|
+
border: 'none'
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
</Layout.Sider>
|
|
73
|
+
);
|
|
74
|
+
};
|
package/src/index.tsx
ADDED
package/src/main.tsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
|
|
4
|
+
import { ShellApp } from './shell-app';
|
|
5
|
+
import type { ShellConfig } from './types';
|
|
6
|
+
|
|
7
|
+
// Get configuration injected by the server
|
|
8
|
+
const config: ShellConfig = (
|
|
9
|
+
window as unknown as { __SHELL_CONFIG__?: ShellConfig }
|
|
10
|
+
).__SHELL_CONFIG__ || {
|
|
11
|
+
plugin: {
|
|
12
|
+
url: 'http://localhost:4002',
|
|
13
|
+
path: './src'
|
|
14
|
+
},
|
|
15
|
+
shell: {
|
|
16
|
+
port: 4000,
|
|
17
|
+
theme: 'omnitron',
|
|
18
|
+
title: 'Development Shell'
|
|
19
|
+
},
|
|
20
|
+
sidebar: {
|
|
21
|
+
items: []
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
26
|
+
<React.StrictMode>
|
|
27
|
+
<ShellApp config={config} />
|
|
28
|
+
</React.StrictMode>
|
|
29
|
+
);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import cors from 'cors';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createServer as createViteServer } from 'vite';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
export class ShellDevServer {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.app = express();
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.viteServer = null;
|
|
16
|
+
this.setupMiddleware();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setupMiddleware() {
|
|
20
|
+
// Enable CORS for plugin communication
|
|
21
|
+
this.app.use(
|
|
22
|
+
cors({
|
|
23
|
+
origin: true,
|
|
24
|
+
credentials: true
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Parse JSON bodies
|
|
29
|
+
this.app.use(express.json());
|
|
30
|
+
|
|
31
|
+
// Serve static assets
|
|
32
|
+
this.app.use('/assets', express.static(path.join(__dirname, '../assets')));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async start() {
|
|
36
|
+
try {
|
|
37
|
+
console.log('๐ง Starting shell server initialization...');
|
|
38
|
+
|
|
39
|
+
const shellRoot = path.resolve(__dirname, '../..');
|
|
40
|
+
const distPath = path.resolve(shellRoot, 'dist');
|
|
41
|
+
// Only use production mode if explicitly set via env var
|
|
42
|
+
const isProduction = process.env.NODE_ENV === 'production' && fs.existsSync(distPath);
|
|
43
|
+
|
|
44
|
+
console.log(`๐ Shell root: ${shellRoot}`);
|
|
45
|
+
console.log(`๐ญ Production mode: ${isProduction}`);
|
|
46
|
+
|
|
47
|
+
// API endpoints
|
|
48
|
+
this.setupApiRoutes();
|
|
49
|
+
|
|
50
|
+
if (isProduction) {
|
|
51
|
+
// Production: serve built assets
|
|
52
|
+
this.app.use('/assets', express.static(path.join(distPath, 'assets')));
|
|
53
|
+
|
|
54
|
+
this.app.get('*', (req, res, next) => {
|
|
55
|
+
if (req.path.startsWith('/api')) {
|
|
56
|
+
return next();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`๐ Serving production HTML for: ${req.url}`);
|
|
60
|
+
|
|
61
|
+
let template = fs.readFileSync(
|
|
62
|
+
path.resolve(distPath, 'index.html'),
|
|
63
|
+
'utf-8'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Inject config
|
|
67
|
+
template = template.replace(
|
|
68
|
+
'<!--SHELL_CONFIG-->',
|
|
69
|
+
`<script>window.__SHELL_CONFIG__ = ${JSON.stringify(this.config)};</script>`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
res.status(200).set({ 'Content-Type': 'text/html' }).end(template);
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
// Development: use Vite middleware
|
|
76
|
+
this.viteServer = await createViteServer({
|
|
77
|
+
root: shellRoot,
|
|
78
|
+
configFile: path.resolve(shellRoot, 'vite.config.ts'),
|
|
79
|
+
server: {
|
|
80
|
+
middlewareMode: true,
|
|
81
|
+
cors: true
|
|
82
|
+
},
|
|
83
|
+
appType: 'spa'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
console.log('โ
Vite server created successfully');
|
|
87
|
+
|
|
88
|
+
// Use Vite middleware for assets and HMR
|
|
89
|
+
this.app.use(this.viteServer.middlewares);
|
|
90
|
+
|
|
91
|
+
console.log('โ
Vite middleware configured');
|
|
92
|
+
|
|
93
|
+
// Serve the shell app (only for HTML requests)
|
|
94
|
+
this.app.get('*', async (req, res, next) => {
|
|
95
|
+
if (req.path.startsWith('/api') || req.path.startsWith('/src') || req.path.includes('.')) {
|
|
96
|
+
return next();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
console.log(`๐ Serving development HTML for: ${req.url}`);
|
|
101
|
+
|
|
102
|
+
let template = fs.readFileSync(
|
|
103
|
+
path.resolve(__dirname, '../../index.html'),
|
|
104
|
+
'utf-8'
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
template = await this.viteServer.transformIndexHtml(
|
|
108
|
+
req.url,
|
|
109
|
+
template
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Inject config
|
|
113
|
+
template = template.replace(
|
|
114
|
+
'<!--SHELL_CONFIG-->',
|
|
115
|
+
`<script>window.__SHELL_CONFIG__ = ${JSON.stringify(this.config)};</script>`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
console.log(`โ
HTML processed successfully for ${req.url}`);
|
|
119
|
+
res.status(200).set({ 'Content-Type': 'text/html' }).end(template);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`โ Error serving ${req.url}:`, error);
|
|
122
|
+
this.viteServer.ssrFixStacktrace(error);
|
|
123
|
+
next(error);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Start server
|
|
129
|
+
const server = this.app.listen(this.config.shell.port, '0.0.0.0', () => {
|
|
130
|
+
console.log(
|
|
131
|
+
`๐ Shell development server running on http://localhost:${this.config.shell.port}`
|
|
132
|
+
);
|
|
133
|
+
console.log(`๐ฑ Plugin expected at: ${this.config.plugin.url}`);
|
|
134
|
+
console.log(`๐จ Theme: ${this.config.shell.theme}`);
|
|
135
|
+
console.log(`๐ Server bound to 0.0.0.0:${this.config.shell.port}`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Handle graceful shutdown
|
|
139
|
+
process.on('SIGTERM', () => {
|
|
140
|
+
console.log('๐ Shutting down shell server...');
|
|
141
|
+
server.close(() => {
|
|
142
|
+
if (this.viteServer) {
|
|
143
|
+
this.viteServer.close();
|
|
144
|
+
}
|
|
145
|
+
process.exit(0);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return server;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('โ Failed to start shell server:', error);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
setupApiRoutes() {
|
|
157
|
+
// Health check
|
|
158
|
+
this.app.get('/api/health', (req, res) => {
|
|
159
|
+
console.log('๐ฅ Health check accessed');
|
|
160
|
+
res.json({
|
|
161
|
+
status: 'ok',
|
|
162
|
+
config: this.config,
|
|
163
|
+
timestamp: new Date().toISOString()
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Simple test endpoint
|
|
168
|
+
this.app.get('/test', (req, res) => {
|
|
169
|
+
console.log('๐งช Test endpoint accessed');
|
|
170
|
+
res.send('Shell server is working!');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Configuration endpoint
|
|
174
|
+
this.app.get('/api/config', (req, res) => {
|
|
175
|
+
res.json(this.config);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Plugin proxy endpoint (for development)
|
|
179
|
+
this.app.get('/api/plugin/health', async (req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
const response = await fetch(`${this.config.plugin.url}/api/health`);
|
|
182
|
+
const data = await response.json();
|
|
183
|
+
res.json({ plugin: data, shell: { status: 'ok' } });
|
|
184
|
+
} catch (error) {
|
|
185
|
+
res.status(502).json({
|
|
186
|
+
error: 'Plugin not available',
|
|
187
|
+
plugin_url: this.config.plugin.url,
|
|
188
|
+
message: error.message
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|