wildpig 1.5.9 → 2.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/entry/client.tsx +6 -5
- package/entry/server.tsx +6 -7
- package/index.ts +9 -0
- package/package.json +3 -5
- package/router/ServerDataGuard.tsx +2 -3
- package/router/index.ts +4 -5
- package/scripts/apiRoutes.ts +5 -5
- package/scripts/build.ts +48 -21
- package/scripts/prodServer.ts +25 -27
- package/scripts/server.ts +149 -28
- package/scripts/types.ts +33 -0
- package/bin/cli.ts +0 -55
- package/scripts/devServer.ts +0 -134
package/entry/client.tsx
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createBrowserRouter } from "react-router";
|
|
2
|
+
import { routes } from "../router";
|
|
3
3
|
import { hydrateRoot } from "react-dom/client"
|
|
4
|
-
|
|
4
|
+
// 这个文件由Vite加载
|
|
5
|
+
const { App } = await import('/src/App'!);
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
const render = () => {
|
|
8
9
|
// 获取serverData
|
|
9
10
|
const serverData = (window as any).__SERVER_DATA__ || undefined;
|
|
10
11
|
// 水合
|
|
11
|
-
hydrateRoot(document.getElementById('root')!, <App router={
|
|
12
|
+
hydrateRoot(document.getElementById('root')!, <App router={createBrowserRouter(routes)} serverData={serverData} />)
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
render();
|
package/entry/server.tsx
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import { App } from "@/App"
|
|
2
|
-
import routes from "@/router/routes"
|
|
3
1
|
import { renderToString } from "react-dom/server"
|
|
4
2
|
import { createStaticHandler, createStaticRouter } from "react-router"
|
|
3
|
+
import { routes } from "../router";
|
|
5
4
|
|
|
5
|
+
// 这个文件由Vite加载
|
|
6
|
+
const { App } = await import("../../../src/App"!);
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export const render = async (req: Request, serverData?: any) => {
|
|
8
|
+
export const render = async (req: Request, serverData?: any): Promise<string> => {
|
|
10
9
|
// 1. 创建处理器
|
|
11
|
-
const { query, dataRoutes } = createStaticHandler(routes)
|
|
10
|
+
const { query, dataRoutes } = createStaticHandler(routes);
|
|
12
11
|
|
|
13
12
|
// 2. 生成 context(自动执行所有 loader)
|
|
14
13
|
const context = await query(new Request(req.url))
|
|
15
14
|
|
|
16
15
|
// 3. 处理重定向/错误
|
|
17
16
|
if (context instanceof Response) {
|
|
18
|
-
|
|
17
|
+
throw "异常,请检查路由配置,确保前后端路由无冲突";
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
// 4. 创建静态路由
|
package/index.ts
ADDED
package/package.json
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wildpig",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"author": "eriktse",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"wildpig": "./bin/cli.ts"
|
|
8
|
-
},
|
|
9
6
|
"peerDependencies": {
|
|
10
7
|
"@nanostores/react": "^1.0.0",
|
|
11
8
|
"@types/bun": "^1.3.4",
|
|
@@ -30,6 +27,7 @@
|
|
|
30
27
|
"scripts": {
|
|
31
28
|
"dev": "wildpig dev",
|
|
32
29
|
"build": "wildpig build",
|
|
33
|
-
"start": "wildpig start"
|
|
30
|
+
"start": "wildpig start",
|
|
31
|
+
"local-publish": "yalc publish"
|
|
34
32
|
}
|
|
35
33
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect } from "react"
|
|
2
2
|
import { matchRoutes, Outlet, useLocation, useNavigate } from "react-router"
|
|
3
|
-
import pageRoutes from "@/router/routes";
|
|
4
3
|
import { serverDataStore } from "../store/serverDataStore";
|
|
4
|
+
import { routes } from ".";
|
|
5
5
|
|
|
6
6
|
export const ServerDataGuard = () => {
|
|
7
7
|
const location = useLocation();
|
|
@@ -11,7 +11,7 @@ export const ServerDataGuard = () => {
|
|
|
11
11
|
serverDataStore.set(undefined);
|
|
12
12
|
|
|
13
13
|
const pathname = location.pathname;
|
|
14
|
-
const matches = matchRoutes(
|
|
14
|
+
const matches = matchRoutes(routes, pathname);
|
|
15
15
|
const lastMatch = matches?.at(-1);
|
|
16
16
|
if(!lastMatch) {
|
|
17
17
|
// 404
|
|
@@ -34,7 +34,6 @@ export const ServerDataGuard = () => {
|
|
|
34
34
|
if(serverDataApi.includes(key + "="))continue;
|
|
35
35
|
serverDataApi += (serverDataApi.includes("?") ? "&" : "?") + key + "=" + value;
|
|
36
36
|
}
|
|
37
|
-
|
|
38
37
|
|
|
39
38
|
fetch(serverDataApi).then(res => res.json()).then(data => {
|
|
40
39
|
serverDataStore.set(data);
|
package/router/index.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { createBrowserRouter } from "react-router";
|
|
2
1
|
import { ServerDataGuard } from "./ServerDataGuard";
|
|
2
|
+
import { WildPigRouteObject } from "./types";
|
|
3
3
|
|
|
4
4
|
// 用户代码
|
|
5
|
-
|
|
5
|
+
let pageRoutes: WildPigRouteObject[] = (await import("../../../src/router/routes"!)).default;
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
export const browserRouter = createBrowserRouter([
|
|
8
|
+
export const routes = [
|
|
10
9
|
{
|
|
11
10
|
path: "/",
|
|
12
11
|
Component: ServerDataGuard,
|
|
13
12
|
children: pageRoutes,
|
|
14
13
|
},
|
|
15
|
-
]
|
|
14
|
+
] as WildPigRouteObject[];
|
package/scripts/apiRoutes.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
const __dirname = import.meta.dirname;
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const middleware = (await import("@/endpoints/middleware"!)).middleware;
|
|
7
7
|
|
|
8
8
|
const getFilePaths = (dir: string) => {
|
|
9
9
|
const res: string[] = [];
|
|
@@ -25,14 +25,14 @@ const makeDynamicRoute = (route: string) => route.replaceAll(/\[([^\]]*)\]/g, ':
|
|
|
25
25
|
|
|
26
26
|
export const makeApiRoutePathObj = () => {
|
|
27
27
|
// 扫描用户代码路径
|
|
28
|
-
const apiDir = "./src/
|
|
28
|
+
const apiDir = "./src/endpoints";
|
|
29
29
|
const apiPaths = getFilePaths(apiDir);
|
|
30
30
|
const result: Record<string, string> = {};
|
|
31
31
|
|
|
32
32
|
for(const apiPath of apiPaths) {
|
|
33
|
-
const importPath = apiPath.replace("./src/
|
|
33
|
+
const importPath = apiPath.replace("./src/endpoints", "@/endpoints");
|
|
34
34
|
if(!apiPath.includes("index.ts")) continue;
|
|
35
|
-
const route = apiPath.replace("./src/
|
|
35
|
+
const route = apiPath.replace("./src/endpoints", "").replace("/index.ts", "");
|
|
36
36
|
result[route] = importPath.replace(".ts", "");
|
|
37
37
|
}
|
|
38
38
|
return result;
|
|
@@ -44,7 +44,7 @@ export const makeApiRoutePathObj = () => {
|
|
|
44
44
|
export const packageApiRoutes = async () => {
|
|
45
45
|
const apiRoutes = makeApiRoutePathObj();
|
|
46
46
|
let identId = 0;
|
|
47
|
-
let importsText = `import { middleware } from "@/
|
|
47
|
+
let importsText = `import { middleware } from "@/endpoints/middleware" \n`;
|
|
48
48
|
let routesText = "export default {\n";
|
|
49
49
|
for(const route of Object.keys(apiRoutes)) {
|
|
50
50
|
const importPath = apiRoutes[route];
|
package/scripts/build.ts
CHANGED
|
@@ -1,43 +1,70 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { packageApiRoutes } from "./apiRoutes";
|
|
3
3
|
import { build as viteBuild } from "vite";
|
|
4
|
+
import { IBuildOptions } from "./types";
|
|
5
|
+
import chalk from "chalk";
|
|
4
6
|
|
|
5
|
-
const
|
|
7
|
+
const __rootdir = path.resolve(__dirname, "../../../"); // 项目根目录
|
|
8
|
+
|
|
9
|
+
const prebuild = async (options: IBuildOptions) => {
|
|
6
10
|
const promises = [];
|
|
7
11
|
// 先编译客户端代码
|
|
8
12
|
promises.push(viteBuild({
|
|
9
|
-
configFile: path.resolve(
|
|
10
|
-
build: {
|
|
11
|
-
outDir: "./dist/client",
|
|
12
|
-
},
|
|
13
|
-
}));
|
|
14
|
-
// 编译服务端入口文件
|
|
15
|
-
promises.push(viteBuild({
|
|
16
|
-
configFile: path.resolve(__dirname, "../../../vite.config.ts"),
|
|
13
|
+
configFile: path.resolve(__rootdir, "vite.config.ts"),
|
|
17
14
|
build: {
|
|
18
|
-
|
|
19
|
-
input: path.resolve(__dirname, "../entry/server.tsx"),
|
|
20
|
-
},
|
|
21
|
-
outDir: "./dist/server",
|
|
22
|
-
ssr: true,
|
|
15
|
+
outDir: path.resolve(__rootdir, options.outdir || "dist", "client"), // 输出目录
|
|
23
16
|
},
|
|
24
17
|
}));
|
|
18
|
+
// promises.push(viteBuild({
|
|
19
|
+
// configFile: path.resolve(__rootdir, "vite.config.ts"),
|
|
20
|
+
// build: {
|
|
21
|
+
// rollupOptions:{
|
|
22
|
+
// input: path.resolve(__dirname, "../entry/server.tsx"),
|
|
23
|
+
// },
|
|
24
|
+
// outDir: path.resolve(__rootdir, options.outdir || "dist", "server"), // 输出目录
|
|
25
|
+
// ssr: true, // 开启ssr
|
|
26
|
+
// },
|
|
27
|
+
// }));
|
|
25
28
|
promises.push(packageApiRoutes());
|
|
26
29
|
await Promise.all(promises);
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
|
|
30
|
-
export const build = async () => {
|
|
33
|
+
export const build = async (options?: IBuildOptions) => {
|
|
34
|
+
options = Object.assign({
|
|
35
|
+
entry: "server.ts",
|
|
36
|
+
outdir: "dist",
|
|
37
|
+
minify: true,
|
|
38
|
+
target: "bun",
|
|
39
|
+
external: [],
|
|
40
|
+
}, options || {});
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
console.log(chalk.green("开始构建..."));
|
|
44
|
+
console.log("构建参数:", options);
|
|
45
|
+
|
|
46
|
+
// 准备阶段
|
|
47
|
+
process.env.NODE_ENV = "production";
|
|
48
|
+
const st = performance.now();
|
|
49
|
+
|
|
50
|
+
|
|
31
51
|
// 前处理
|
|
32
|
-
await prebuild();
|
|
52
|
+
await prebuild(options);
|
|
33
53
|
// 正式编译
|
|
34
|
-
Bun.build({
|
|
35
|
-
entrypoints: [path.resolve(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
await Bun.build({
|
|
55
|
+
entrypoints: [path.resolve(__rootdir, options.entry || "server.ts")],
|
|
56
|
+
minify: options.minify || true, // 压缩
|
|
57
|
+
target: options.target || "bun",
|
|
58
|
+
outdir: options.outdir || "./dist",
|
|
59
|
+
format: "esm",
|
|
60
|
+
external: ["vite", ...(options.external || [])],
|
|
39
61
|
define: {
|
|
40
62
|
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
41
63
|
},
|
|
42
64
|
});
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
console.log(chalk.green("🐗 [Wildpig] Build done, time:"), chalk.blue(performance.now() - st, "ms"));
|
|
68
|
+
console.log(chalk.green(`✨ [Wildpig] Start by command:`), chalk.blue(`bun run start`));
|
|
69
|
+
return;
|
|
43
70
|
}
|
package/scripts/prodServer.ts
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
import { getApiRouteModules } from "./apiRoutes";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { matchRoutes } from "react-router";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const pageRoutes = (await import("@/router/routes"!)).default as WildPigRouteObject[];
|
|
8
|
-
import chalk from "chalk";
|
|
9
|
-
import { WildPigRouteObject } from "router/types";
|
|
10
|
-
|
|
4
|
+
import packageJson from "../package.json";
|
|
5
|
+
import { ICreateServerOptions } from "./types";
|
|
6
|
+
import { routes } from "../router";
|
|
11
7
|
const env = process.env;
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
// 用户代码(动态导入)
|
|
9
|
+
import chalk from "chalk";
|
|
14
10
|
|
|
15
11
|
const getPackageInfo = async () => {
|
|
16
|
-
const packageJson = await Bun.file(path.resolve(__dirname, "../package.json")).json();
|
|
17
12
|
return packageJson;
|
|
18
13
|
}
|
|
19
14
|
const packageInfo = await getPackageInfo();
|
|
20
15
|
|
|
21
16
|
/** 启动后的描述性文字 */
|
|
22
|
-
const afterStart = () => {
|
|
17
|
+
const afterStart = (options: ICreateServerOptions) => {
|
|
23
18
|
// 启动后的文字
|
|
24
19
|
console.log(` __ __ _ _ _ ____ _
|
|
25
20
|
\\ \\ / /(_)| | __| | | _ \\ (_) __ _
|
|
@@ -31,17 +26,21 @@ console.log(chalk.blue.bgGreen(` 🐗 WildPig version ${packageInfo?.ver
|
|
|
31
26
|
console.log(chalk.green(" Strong & Fast Fullstack Framework\n"));
|
|
32
27
|
console.log(chalk.green("✨ WildPig is running on port " + env.PORT || 3000));
|
|
33
28
|
console.log(chalk.green("💻 Wildpig is Running in production mode."));
|
|
34
|
-
console.log(chalk.green(`🔗 Click to
|
|
29
|
+
console.log(chalk.green(`🔗 Click to play in Browser: http://localhost:${options.port}`));
|
|
35
30
|
}
|
|
36
31
|
|
|
32
|
+
export default async (options?: ICreateServerOptions) => {
|
|
33
|
+
options = Object.assign({
|
|
34
|
+
port: 3000,
|
|
35
|
+
host: "0.0.0.0",
|
|
36
|
+
showInfo: true,
|
|
37
|
+
}, options || {});
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
export const startServer = async () => {
|
|
40
|
-
// 确保重启后可以重新拿到路由
|
|
39
|
+
|
|
41
40
|
const apiModules = await getApiRouteModules("prod") as any;
|
|
42
|
-
|
|
43
|
-
port,
|
|
44
|
-
hostname,
|
|
41
|
+
const server = Bun.serve({
|
|
42
|
+
port: options.port,
|
|
43
|
+
hostname: options.host,
|
|
45
44
|
routes:{
|
|
46
45
|
...apiModules,
|
|
47
46
|
"/*": async (request: Request) => {
|
|
@@ -62,7 +61,7 @@ export const startServer = async () => {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
// 请求服务端数据
|
|
65
|
-
const matches = matchRoutes(
|
|
64
|
+
const matches = matchRoutes(routes, url.pathname);
|
|
66
65
|
if(!matches)return new Response("Not Found", {status: 404});
|
|
67
66
|
|
|
68
67
|
const matchRoute = matches.at(-1)!;
|
|
@@ -85,7 +84,9 @@ export const startServer = async () => {
|
|
|
85
84
|
});
|
|
86
85
|
serverRequest.headers.set("wildpig-server-data-api", serverDataApi);
|
|
87
86
|
const pathname = serverDataApi.split("?")[0]; // 获取路径
|
|
88
|
-
const
|
|
87
|
+
const handler = apiModules?.[pathname]?.GET;
|
|
88
|
+
if(!handler)return undefined; // 没有对应的handler
|
|
89
|
+
const serverData = await handler(serverRequest).then((r: Response) => r.json());
|
|
89
90
|
return serverData;
|
|
90
91
|
};
|
|
91
92
|
let serverData = await getServerData();
|
|
@@ -112,10 +113,7 @@ export const startServer = async () => {
|
|
|
112
113
|
},
|
|
113
114
|
},
|
|
114
115
|
development: false,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
startServer();
|
|
121
|
-
afterStart();
|
|
116
|
+
});
|
|
117
|
+
if(options.showInfo)afterStart(options);
|
|
118
|
+
return server;
|
|
119
|
+
}
|
package/scripts/server.ts
CHANGED
|
@@ -1,37 +1,158 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
1
|
+
import { getApiRouteModules } from "./apiRoutes";
|
|
2
|
+
import { createServer as createViteServer, type ViteDevServer} from "vite";
|
|
3
|
+
import { matchRoutes } from "react-router";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { WildPigRouteObject } from "../router/types";
|
|
6
|
+
import packageInfo from "../package.json";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const __dirname = import.meta.dirname;
|
|
10
|
+
const __rootdir = path.resolve(__dirname, "../../../");
|
|
11
|
+
import { ICreateServerOptions } from "./types";
|
|
12
|
+
|
|
6
13
|
const env = process.env;
|
|
7
14
|
|
|
8
15
|
|
|
9
|
-
const port = env.PORT || 3000;
|
|
10
|
-
const hostname = env.HOSTNAME || "localhost";
|
|
11
|
-
const isDev = env.NODE_ENV === "development";
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
// 用户代码
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import chalk from "chalk";
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// 启动vite server
|
|
23
|
+
let viteServer: ViteDevServer;
|
|
24
|
+
if(env.NODE_ENV === "development"){
|
|
25
|
+
viteServer = await createViteServer({
|
|
26
|
+
configFile: path.resolve(__rootdir, "./vite.config.ts"),
|
|
27
|
+
});
|
|
28
|
+
await viteServer.listen(viteServer.config.server.port);
|
|
16
29
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
const frontHandler = (apiModules: any) => async (request: Request) => {
|
|
33
|
+
// 判断pathname是否匹配pageRoutes
|
|
34
|
+
const url = new URL(request.url);
|
|
35
|
+
|
|
36
|
+
// 判断是否是vite请求
|
|
37
|
+
if(url.pathname.includes(".") || url.pathname.startsWith("/@") || url.pathname.startsWith("/assets")){
|
|
38
|
+
if(env.NODE_ENV === "development"){// 交给vite
|
|
39
|
+
const viteURL = new URL(request.url);
|
|
40
|
+
viteURL.port = viteServer.config.server.port.toString();
|
|
41
|
+
const response = await fetch(viteURL.toString(), {
|
|
42
|
+
method: request.method,
|
|
43
|
+
headers: request.headers,
|
|
44
|
+
body: request.body,
|
|
45
|
+
});
|
|
46
|
+
return response.clone();
|
|
47
|
+
}else{// production环境,直接返回文件
|
|
48
|
+
const filepath = "./client" + url.pathname;
|
|
49
|
+
// 检查文件是否存在
|
|
50
|
+
if(fs.existsSync(filepath) && fs.statSync(filepath).isFile()){
|
|
51
|
+
return new Response(Bun.file(filepath), {
|
|
52
|
+
headers: {
|
|
53
|
+
"Cache-Control": "public, max-age=864000" // 10 天缓存
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// 文件不存在
|
|
58
|
+
return new Response("Not Found", {status: 404});
|
|
59
|
+
}
|
|
33
60
|
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
// 服务端请求,获取服务端数据
|
|
65
|
+
const routes = viteServer ? (await viteServer.ssrLoadModule("/node_modules/wildpig/router/index.ts"!)).routes as WildPigRouteObject[] : (await import("../router")).routes;
|
|
66
|
+
const matches = matchRoutes(routes, url.pathname);
|
|
67
|
+
if(!matches)return new Response("404 Not Found", { status: 404 });
|
|
68
|
+
|
|
69
|
+
// 请求服务端数据
|
|
70
|
+
const matchRoute = matches.at(-1)!;
|
|
71
|
+
let serverDataApi = matchRoute.route.serverDataApi;
|
|
72
|
+
let serverData = await (async () => {
|
|
73
|
+
if(!serverDataApi)return undefined;
|
|
74
|
+
const prefixUrl = request.url.split("/")[0] + "//" + request.url.split("/")[2];
|
|
75
|
+
// 需要请求服务端数据, 替换动态参数
|
|
76
|
+
for(const [key, value] of Object.entries(matchRoute.params)){
|
|
77
|
+
if(value)serverDataApi = serverDataApi.replace(":" + key, value);
|
|
78
|
+
}
|
|
79
|
+
// 加上当前request的query参数
|
|
80
|
+
for(const [key, value] of new URLSearchParams(request.url.split("?")[1]).entries()){
|
|
81
|
+
if(serverDataApi.includes(key + "="))continue; // 已经有这个参数了
|
|
82
|
+
serverDataApi += (serverDataApi.includes("?") ? "&" : "?") + key + "=" + value;
|
|
83
|
+
}
|
|
84
|
+
const serverRequest = new Request({
|
|
85
|
+
...request.clone(),
|
|
86
|
+
url: prefixUrl + serverDataApi, // 替换url
|
|
87
|
+
});
|
|
88
|
+
serverRequest.headers.set("wildpig-server-data-api", serverDataApi);
|
|
89
|
+
const pathname = serverDataApi.split("?")[0]; // 获取路径
|
|
90
|
+
const handler = apiModules?.[pathname]?.GET;
|
|
91
|
+
if(!handler)return undefined; // 没有对应的handler
|
|
92
|
+
const serverData = await handler(serverRequest).then((r: Response) => r.json());
|
|
93
|
+
return serverData;
|
|
94
|
+
})();
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// 构造响应页面
|
|
100
|
+
// 1. 读取 index.html
|
|
101
|
+
const template = viteServer ? await viteServer.transformIndexHtml(request.url, fs.readFileSync('./index.html', 'utf-8')) : fs.readFileSync(path.resolve(__dirname, './client/index.html'), 'utf-8');
|
|
102
|
+
// 2. 获取渲染函数
|
|
103
|
+
const render = (await import('../entry/server')).render
|
|
104
|
+
// 3. 获取应用程序 HTML
|
|
105
|
+
const appHtml = await render(request, serverData);
|
|
106
|
+
|
|
107
|
+
// 4. 注入渲染后的应用程序 HTML 到模板中。
|
|
108
|
+
const html = template
|
|
109
|
+
.replace(`<!--ssr-outlet-->`, () => appHtml)
|
|
110
|
+
.replace(`<!--title-->`, () => serverData?.title || "title")
|
|
111
|
+
.replace(`<!--server-data-->`, () => `<script>window.__SERVER_DATA__ = ${JSON.stringify(serverData)};</script>`);
|
|
112
|
+
|
|
113
|
+
return new Response(html, {
|
|
114
|
+
headers: {
|
|
115
|
+
"content-type": "text/html; charset=utf-8",
|
|
116
|
+
"Access-Control-Allow-Origin": "*",
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 启动后的描述性文字 */
|
|
122
|
+
const afterStart = async (options: ICreateServerOptions) => {
|
|
123
|
+
// 启动后的文字
|
|
124
|
+
console.log(` __ __ _ _ _ ____ _
|
|
125
|
+
\\ \\ / /(_)| | __| | | _ \\ (_) __ _
|
|
126
|
+
\\ \\ /\\ / / | || | / _\` | | |_) || | / _\` |
|
|
127
|
+
\\ V V / | || || (_| | | __/ | || (_| |
|
|
128
|
+
\\_/\\_/ |_||_| \\__,_| |_| |_| \\__, |
|
|
129
|
+
|___/ `)
|
|
130
|
+
console.log(chalk.blue.bgGreen(` 🐗 WildPig version ${packageInfo?.version} by ${packageInfo?.author} `));
|
|
131
|
+
console.log(chalk.green(" Strong & Fast Fullstack Framework\n"));
|
|
132
|
+
console.log(chalk.green("✨ WildPig is running on port " + options.port || 3000));
|
|
133
|
+
console.log(chalk.yellow(`💻 Wildpig is Running in ${chalk.yellow.bold(env.NODE_ENV)} mode.`));
|
|
134
|
+
if(viteServer)console.log(chalk.green("⚡ Vite server is running on port " + viteServer.config.server?.port));
|
|
135
|
+
console.log(chalk.green(`🔗 Click to debug in Browser: http://localhost:${options.port}`));
|
|
34
136
|
}
|
|
35
|
-
await startHotServer();
|
|
36
137
|
|
|
138
|
+
export const createServer = async (options?: ICreateServerOptions) => {
|
|
139
|
+
options = Object.assign({
|
|
140
|
+
port: 3000,
|
|
141
|
+
host: "0.0.0.0",
|
|
142
|
+
showInfo: true,
|
|
143
|
+
}, options || {});
|
|
37
144
|
|
|
145
|
+
// 确保重启后可以重新拿到路由
|
|
146
|
+
const apiModules = await getApiRouteModules(env.NODE_ENV === "development" ? "dev" : "prod") as any;
|
|
147
|
+
const server = Bun.serve({
|
|
148
|
+
port: options.port,
|
|
149
|
+
hostname: options.host,
|
|
150
|
+
routes:{
|
|
151
|
+
...apiModules,
|
|
152
|
+
"/*": frontHandler(apiModules),
|
|
153
|
+
},
|
|
154
|
+
development: env.NODE_ENV === "development",
|
|
155
|
+
})
|
|
156
|
+
if(options.showInfo)await afterStart(options);
|
|
157
|
+
return server;
|
|
158
|
+
}
|
package/scripts/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ICreateServerOptions {
|
|
2
|
+
host?: string,
|
|
3
|
+
port?: number,
|
|
4
|
+
showInfo?: boolean,
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export interface IBuildOptions {
|
|
9
|
+
/**
|
|
10
|
+
* 入口文件的路径,默认是./server.ts
|
|
11
|
+
*/
|
|
12
|
+
entry?: string,
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 是否压缩代码,默认为true
|
|
16
|
+
*/
|
|
17
|
+
minify?: boolean,
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 输出目录,默认是./dist
|
|
21
|
+
*/
|
|
22
|
+
outdir?: string,
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 目标平台,默认是bun,如果需要打包成二进制文件,可以自行将打包出的js文件compile一次
|
|
26
|
+
*/
|
|
27
|
+
target?: "node" | "bun",
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 额外的外部依赖,默认是[]
|
|
31
|
+
*/
|
|
32
|
+
external?: string[],
|
|
33
|
+
}
|
package/bin/cli.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import { build } from "../scripts/build";
|
|
4
|
-
import { spawn } from "bun";
|
|
5
|
-
import fs from "node:fs";
|
|
6
|
-
const command = process.argv[2];
|
|
7
|
-
|
|
8
|
-
if(command === "start"){
|
|
9
|
-
// 判断系统平台
|
|
10
|
-
const platform = process.platform;
|
|
11
|
-
let serverBin = "./prodServer"; // linux
|
|
12
|
-
if(platform === "win32")serverBin = "prodServer";
|
|
13
|
-
// 设置一些环境变量
|
|
14
|
-
process.env.NODE_ENV = "production";
|
|
15
|
-
console.log(chalk.green("✨ [Wildpig] Start production server..."));
|
|
16
|
-
const st = performance.now();
|
|
17
|
-
// 启动二进制文件
|
|
18
|
-
spawn([serverBin], {
|
|
19
|
-
cwd: "./dist",
|
|
20
|
-
stdout: "inherit",
|
|
21
|
-
env: {
|
|
22
|
-
...process.env
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
const ed = performance.now();
|
|
26
|
-
setTimeout(() => {
|
|
27
|
-
console.log(chalk.green("✨ [Wildpig] Production server started in " + Math.floor(ed - st) + "ms"));
|
|
28
|
-
}, 300);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if(command === "dev"){
|
|
33
|
-
// 设置一些环境变量
|
|
34
|
-
process.env.NODE_ENV = "development";
|
|
35
|
-
// 监测是否有node_modules/wildpig
|
|
36
|
-
const wildpigExist = fs.existsSync("./node_modules/wildpig");
|
|
37
|
-
const serverPath = wildpigExist ? "./node_modules/wildpig/scripts/devServer.ts" : "./scripts/devServer.ts";
|
|
38
|
-
spawn(["bun", "run", "--watch", serverPath], {
|
|
39
|
-
cwd: ".",
|
|
40
|
-
stdout: "inherit",
|
|
41
|
-
env: {
|
|
42
|
-
...process.env
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if(command === "build"){
|
|
49
|
-
// 设置一些环境变量
|
|
50
|
-
process.env.NODE_ENV = "production";
|
|
51
|
-
const st = performance.now();
|
|
52
|
-
await build();
|
|
53
|
-
console.log(chalk.green("🐗 [Wildpig] Build done, time:"), chalk.blue(performance.now() - st, "ms"));
|
|
54
|
-
console.log(chalk.green(`✨ [Wildpig] Start by command:`), chalk.blue(`bun run start`));
|
|
55
|
-
}
|
package/scripts/devServer.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { getApiRouteModules } from "./apiRoutes";
|
|
2
|
-
import { createServer as createViteServer } from "vite";
|
|
3
|
-
import { matchRoutes } from "react-router";
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
|
|
6
|
-
const __dirname = import.meta.dirname;
|
|
7
|
-
|
|
8
|
-
// 用户代码
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import chalk from "chalk";
|
|
11
|
-
import { WildPigRouteObject } from "router/types";
|
|
12
|
-
|
|
13
|
-
const env = process.env;
|
|
14
|
-
const port = env.PORT || 3000;
|
|
15
|
-
const hostname = env.HOST || env.HOSTNAME || "localhost";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// 启动vite server
|
|
19
|
-
const viteServer = await createViteServer({
|
|
20
|
-
configFile: path.resolve(__dirname, "../../../vite.config.ts"),
|
|
21
|
-
});
|
|
22
|
-
await viteServer.listen(viteServer.config.server.port);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const viteHandler = (apiModules: any) => async (request: Request) => {
|
|
26
|
-
// 判断pathname是否匹配pageRoutes
|
|
27
|
-
const url = new URL(request.url);
|
|
28
|
-
|
|
29
|
-
// 判断是否是vite请求
|
|
30
|
-
if(url.pathname.includes(".") || url.pathname.startsWith("/@")){
|
|
31
|
-
// 没有匹配的route,说明是一些资源什么的
|
|
32
|
-
const viteURL = new URL(request.url);
|
|
33
|
-
viteURL.port = viteServer.config.server.port.toString();
|
|
34
|
-
// console.log("转发请求:" + viteURL.toString());
|
|
35
|
-
const response = await fetch(viteURL.toString(), {
|
|
36
|
-
method: request.method,
|
|
37
|
-
headers: request.headers,
|
|
38
|
-
body: request.body,
|
|
39
|
-
});
|
|
40
|
-
return response.clone();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const pageRoutes = (await viteServer.ssrLoadModule("@/router/routes.ts"!)).default as WildPigRouteObject[];
|
|
44
|
-
const matches = matchRoutes(pageRoutes, url.pathname);
|
|
45
|
-
if(!matches)return new Response("404 Not Found", { status: 404 });
|
|
46
|
-
|
|
47
|
-
// 请求服务端数据
|
|
48
|
-
const matchRoute = matches.at(-1)!;
|
|
49
|
-
let serverDataApi = matchRoute.route.serverDataApi;
|
|
50
|
-
const getServerData = async () => {
|
|
51
|
-
if(!serverDataApi)return undefined;
|
|
52
|
-
const prefixUrl = request.url.split("/")[0] + "//" + request.url.split("/")[2];
|
|
53
|
-
// 需要请求服务端数据, 替换动态参数
|
|
54
|
-
for(const [key, value] of Object.entries(matchRoute.params)){
|
|
55
|
-
if(value)serverDataApi = serverDataApi.replace(":" + key, value);
|
|
56
|
-
}
|
|
57
|
-
// 加上当前request的query参数
|
|
58
|
-
for(const [key, value] of new URLSearchParams(request.url.split("?")[1]).entries()){
|
|
59
|
-
if(serverDataApi.includes(key + "="))continue; // 已经有这个参数了
|
|
60
|
-
serverDataApi += (serverDataApi.includes("?") ? "&" : "?") + key + "=" + value;
|
|
61
|
-
}
|
|
62
|
-
const serverRequest = new Request({
|
|
63
|
-
...request.clone(),
|
|
64
|
-
url: prefixUrl + serverDataApi, // 替换url
|
|
65
|
-
});
|
|
66
|
-
serverRequest.headers.set("wildpig-server-data-api", serverDataApi);
|
|
67
|
-
const pathname = serverDataApi.split("?")[0]; // 获取路径
|
|
68
|
-
const serverData = await apiModules[pathname].GET(serverRequest).then((r: Response) => r.json());
|
|
69
|
-
return serverData;
|
|
70
|
-
};
|
|
71
|
-
let serverData = await getServerData();
|
|
72
|
-
|
|
73
|
-
// 1. 读取 index.html
|
|
74
|
-
const template = await viteServer.transformIndexHtml(request.url, fs.readFileSync('./index.html', 'utf-8'));
|
|
75
|
-
// 2. 获取渲染函数
|
|
76
|
-
const { render } = await viteServer.ssrLoadModule('/node_modules/wildpig/entry/server.tsx')
|
|
77
|
-
// 3. 获取应用程序 HTML
|
|
78
|
-
const appHtml = await render(request, serverData)
|
|
79
|
-
|
|
80
|
-
// 4. 注入渲染后的应用程序 HTML 到模板中。
|
|
81
|
-
const html = template
|
|
82
|
-
.replace(`<!--ssr-outlet-->`, () => appHtml)
|
|
83
|
-
.replace(`<!--title-->`, () => serverData?.title || "title")
|
|
84
|
-
.replace(`<!--server-data-->`, () => `<script>window.__SERVER_DATA__ = ${JSON.stringify(serverData)};</script>`);
|
|
85
|
-
|
|
86
|
-
return new Response(html, {
|
|
87
|
-
headers: {
|
|
88
|
-
"content-type": "text/html; charset=utf-8",
|
|
89
|
-
"Access-Control-Allow-Origin": "*",
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const getPackageInfo = async () => {
|
|
96
|
-
const packageJson = await Bun.file(path.resolve(__dirname, "../package.json")).json();
|
|
97
|
-
return packageJson;
|
|
98
|
-
}
|
|
99
|
-
const packageInfo = await getPackageInfo();
|
|
100
|
-
|
|
101
|
-
/** 启动后的描述性文字 */
|
|
102
|
-
const afterStart = () => {
|
|
103
|
-
// 启动后的文字
|
|
104
|
-
console.log(` __ __ _ _ _ ____ _
|
|
105
|
-
\\ \\ / /(_)| | __| | | _ \\ (_) __ _
|
|
106
|
-
\\ \\ /\\ / / | || | / _\` | | |_) || | / _\` |
|
|
107
|
-
\\ V V / | || || (_| | | __/ | || (_| |
|
|
108
|
-
\\_/\\_/ |_||_| \\__,_| |_| |_| \\__, |
|
|
109
|
-
|___/ `)
|
|
110
|
-
console.log(chalk.blue.bgGreen(` 🐗 WildPig version ${packageInfo?.version} by ${packageInfo?.author} `));
|
|
111
|
-
console.log(chalk.green(" Strong & Fast Fullstack Framework\n"));
|
|
112
|
-
console.log(chalk.green("✨ WildPig is running on port " + env.PORT || 3000));
|
|
113
|
-
console.log(chalk.yellow("💻 Wildpig is Running in development mode."));
|
|
114
|
-
console.log(chalk.green("⚡ Vite server is running on port " + viteServer.config.server?.port));
|
|
115
|
-
console.log(chalk.green(`🔗 Click to debug in Browser: http://${hostname}:${port}`));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export const startServer = async () => {
|
|
119
|
-
// 确保重启后可以重新拿到路由
|
|
120
|
-
const apiModules = await getApiRouteModules("dev") as any;
|
|
121
|
-
console.log(apiModules)
|
|
122
|
-
const server = Bun.serve({
|
|
123
|
-
port,
|
|
124
|
-
hostname,
|
|
125
|
-
routes:{
|
|
126
|
-
...apiModules,
|
|
127
|
-
"/*": viteHandler(apiModules),
|
|
128
|
-
},
|
|
129
|
-
development: true,
|
|
130
|
-
})
|
|
131
|
-
afterStart();
|
|
132
|
-
return server;
|
|
133
|
-
}
|
|
134
|
-
startServer();
|