@vnejs/desktop 0.0.35 → 0.0.37
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/package.json +9 -7
- package/src/electron.js +27 -4
- package/src/server.js +166 -51
- package/src/web.js +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vnejs/desktop",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.37",
|
|
4
4
|
"description": "Tools to run desktop app of @vnejs",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
"author": "",
|
|
16
16
|
"license": "ISC",
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
18
|
+
"@fastify/cors": "~11.2.0",
|
|
19
|
+
"@fastify/static": "~9.1.3",
|
|
20
|
+
"commander": "~15.0.0",
|
|
21
|
+
"fastify": "~5.8.5",
|
|
22
|
+
"get-port": "~7.2.0",
|
|
23
|
+
"open": "~11.0.0"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|
|
24
|
-
"@electron/packager": "
|
|
25
|
-
"electron": "~
|
|
26
|
+
"@electron/packager": "~20.0.1",
|
|
27
|
+
"electron": "~42.3.3"
|
|
26
28
|
}
|
|
27
29
|
}
|
package/src/electron.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
const { app, BrowserWindow } = require("electron");
|
|
1
|
+
const { app, protocol, BrowserWindow, net } = require("electron");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { pathToFileURL } = require("node:url");
|
|
4
|
+
|
|
2
5
|
let mainWindow;
|
|
3
6
|
|
|
7
|
+
protocol.registerSchemesAsPrivileged([{ scheme: "vnejs", privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true } }]);
|
|
8
|
+
|
|
4
9
|
const createWindow = (options) => {
|
|
5
|
-
mainWindow = new BrowserWindow({
|
|
10
|
+
mainWindow = new BrowserWindow({
|
|
11
|
+
autoHideMenuBar: options.isProd,
|
|
12
|
+
fullscreen: true,
|
|
13
|
+
frame: true,
|
|
14
|
+
webPreferences: { contextIsolation: true, nodeIntegration: false },
|
|
15
|
+
});
|
|
6
16
|
|
|
7
17
|
if (options.port) mainWindow.loadURL(`http://localhost:${options.port}/`);
|
|
8
|
-
if (options.file) mainWindow.
|
|
18
|
+
if (options.file) mainWindow.loadURL(`vnejs://${options.file}`);
|
|
9
19
|
|
|
10
20
|
mainWindow.on("closed", () => (mainWindow = null));
|
|
11
21
|
options.isProd ? mainWindow.removeMenu() : mainWindow.webContents.openDevTools();
|
|
@@ -17,7 +27,20 @@ const createWindow = (options) => {
|
|
|
17
27
|
};
|
|
18
28
|
|
|
19
29
|
const runElectron = (options) => {
|
|
20
|
-
app.whenReady().then(() =>
|
|
30
|
+
app.whenReady().then(() => {
|
|
31
|
+
protocol.handle("vnejs", (request) => {
|
|
32
|
+
const url = new URL(request.url);
|
|
33
|
+
|
|
34
|
+
let pathname = decodeURIComponent(url.pathname);
|
|
35
|
+
|
|
36
|
+
if (pathname === "/") pathname = "/index.html";
|
|
37
|
+
|
|
38
|
+
const filePath = path.join(options.rootDir, "dist", pathname);
|
|
39
|
+
|
|
40
|
+
return net.fetch(pathToFileURL(filePath).toString());
|
|
41
|
+
});
|
|
42
|
+
createWindow(options);
|
|
43
|
+
});
|
|
21
44
|
app.on("window-all-closed", () => process.platform !== "darwin" && app.quit());
|
|
22
45
|
app.on("activate", () => mainWindow === null && createWindow(options));
|
|
23
46
|
};
|
package/src/server.js
CHANGED
|
@@ -1,83 +1,198 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
|
-
const
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const fastify = require("fastify");
|
|
3
4
|
const getPort = require("get-port").default;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
5
|
+
const fastifyCors = require("@fastify/cors");
|
|
6
|
+
const fastifyStatic = require("@fastify/static");
|
|
7
|
+
const { readFile, readdir, writeFile, appendFile, mkdir, unlink, access } = require("fs/promises");
|
|
6
8
|
|
|
7
|
-
const
|
|
9
|
+
const getPrefix = (key) => {
|
|
10
|
+
const dot = key.indexOf(".");
|
|
11
|
+
|
|
12
|
+
return dot === -1 ? key : key.slice(0, dot);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const splitStateByPrefix = (state) =>
|
|
16
|
+
Object.entries(state).reduce((groups, [key, value]) => {
|
|
17
|
+
const prefix = getPrefix(key);
|
|
18
|
+
|
|
19
|
+
groups[prefix] ??= {};
|
|
20
|
+
groups[prefix][key] = value;
|
|
21
|
+
|
|
22
|
+
return groups;
|
|
23
|
+
}, {});
|
|
24
|
+
|
|
25
|
+
const isIPv4 = (family) => family === "IPv4" || family === 4;
|
|
26
|
+
|
|
27
|
+
const getServerUrls = (port) => {
|
|
28
|
+
const local = [`http://localhost:${port}/`, `http://127.0.0.1:${port}/`];
|
|
29
|
+
const network = new Set();
|
|
30
|
+
|
|
31
|
+
for (const interfaces of Object.values(os.networkInterfaces())) {
|
|
32
|
+
for (const iface of interfaces) {
|
|
33
|
+
if (isIPv4(iface.family) && !iface.internal) network.add(`http://${iface.address}:${port}/`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { local, network: [...network] };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const logServerAddresses = (port) => {
|
|
41
|
+
const { local, network } = getServerUrls(port);
|
|
42
|
+
|
|
43
|
+
console.log("server started:");
|
|
44
|
+
console.log(" locally:");
|
|
45
|
+
local.forEach((url) => console.log(` ${url}`));
|
|
46
|
+
|
|
47
|
+
if (network.length) {
|
|
48
|
+
console.log(" on local network (wifi):");
|
|
49
|
+
network.forEach((url) => console.log(` ${url}`));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(" on local network (wifi): not available");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const pathExists = async (target) => {
|
|
56
|
+
try {
|
|
57
|
+
await access(target);
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const runServer = async ({ rootDir, stateDir, oldApp, achievementLock, achievementUnlock } = {}) => {
|
|
8
66
|
const port = await getPort();
|
|
9
67
|
|
|
10
|
-
const
|
|
68
|
+
const resolvedStateDir = stateDir || path.join(rootDir, "state");
|
|
11
69
|
const gamePath = path.join(rootDir, "game");
|
|
12
70
|
|
|
13
|
-
const
|
|
71
|
+
const ensureStateDir = () => mkdir(resolvedStateDir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
const getStateFiles = async () => {
|
|
74
|
+
try {
|
|
75
|
+
const files = await readdir(resolvedStateDir);
|
|
76
|
+
|
|
77
|
+
return files.filter((file) => file.endsWith(".json"));
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const onSaveState = async (req, reply) => {
|
|
84
|
+
try {
|
|
85
|
+
await ensureStateDir();
|
|
86
|
+
|
|
87
|
+
const groups = splitStateByPrefix(req.body);
|
|
88
|
+
const activePrefixes = new Set(Object.keys(groups));
|
|
89
|
+
const stalePrefixes = (await getStateFiles()).map((file) => file.slice(0, -".json".length)).filter((prefix) => !activePrefixes.has(prefix));
|
|
90
|
+
|
|
91
|
+
await Promise.all([
|
|
92
|
+
...Object.entries(groups).map(([prefix, state]) => writeFile(path.join(resolvedStateDir, `${prefix}.json`), JSON.stringify(state))),
|
|
93
|
+
...stalePrefixes.map((prefix) => unlink(path.join(resolvedStateDir, `${prefix}.json`))),
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
return reply.send({ status: "ok" });
|
|
97
|
+
} catch {
|
|
98
|
+
return reply.send({ status: "error" });
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const onGetState = async (req, reply) => {
|
|
103
|
+
const stateFiles = await getStateFiles();
|
|
14
104
|
|
|
15
|
-
|
|
16
|
-
readFile(stateJson, (err, data) =>
|
|
17
|
-
err ? writeFile(stateJson, "{}", () => res.json({ status: "ok", state: {} })) : res.json({ status: "ok", state: JSON.parse(data.toString()) }),
|
|
18
|
-
);
|
|
105
|
+
if (!stateFiles.length) return reply.send({ status: "ok", state: {} });
|
|
19
106
|
|
|
20
|
-
|
|
107
|
+
try {
|
|
108
|
+
const chunks = await Promise.all(stateFiles.map((file) => readFile(path.join(resolvedStateDir, file), "utf8").then(JSON.parse)));
|
|
109
|
+
|
|
110
|
+
return reply.send({ status: "ok", state: Object.assign({}, ...chunks) });
|
|
111
|
+
} catch {
|
|
112
|
+
return reply.send({ status: "error" });
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const onAchievementLock = (req, reply) => {
|
|
21
117
|
typeof achievementLock === "function" && achievementLock(req.body?.id, req.body?.data);
|
|
22
118
|
|
|
23
|
-
return
|
|
119
|
+
return reply.send({ status: "ok" });
|
|
24
120
|
};
|
|
25
|
-
|
|
121
|
+
|
|
122
|
+
const onAchievementUnlock = (req, reply) => {
|
|
26
123
|
typeof achievementUnlock === "function" && achievementUnlock(req.body?.id, req.body?.data);
|
|
27
124
|
|
|
28
|
-
return
|
|
125
|
+
return reply.send({ status: "ok" });
|
|
29
126
|
};
|
|
30
127
|
|
|
31
|
-
const onLog = (req,
|
|
32
|
-
|
|
128
|
+
const onLog = (req, reply) => {
|
|
129
|
+
reply.send("ok");
|
|
33
130
|
|
|
34
131
|
appendFile(path.join(rootDir, "log.txt"), `${req.body.log}\n`);
|
|
35
132
|
};
|
|
36
133
|
|
|
37
|
-
|
|
134
|
+
const createApp = async () => {
|
|
135
|
+
const app = fastify({ bodyLimit: 50 * 1024 * 1024 });
|
|
38
136
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
137
|
+
await app.register(fastifyCors, {
|
|
138
|
+
origin: "*",
|
|
139
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
140
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
141
|
+
});
|
|
44
142
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.use("/state/get", onGetState)
|
|
51
|
-
.use("/achievement/lock", onAchievementLock)
|
|
52
|
-
.use("/achievement/unlock", onAchievementUnlock)
|
|
53
|
-
.use("/log", onLog);
|
|
143
|
+
app.post("/state/save", onSaveState);
|
|
144
|
+
app.get("/state/get", onGetState);
|
|
145
|
+
app.post("/achievement/lock", onAchievementLock);
|
|
146
|
+
app.post("/achievement/unlock", onAchievementUnlock);
|
|
147
|
+
app.post("/log", onLog);
|
|
54
148
|
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const publicDir = path.join(gamePath, modDir, "public");
|
|
149
|
+
const publicDirs = [path.join(rootDir, "dist")];
|
|
150
|
+
const mediaDirs = [];
|
|
58
151
|
|
|
59
|
-
|
|
60
|
-
existsSync(publicDir) && app.use(express.static(publicDir));
|
|
61
|
-
});
|
|
152
|
+
let modDirs = [];
|
|
62
153
|
|
|
63
|
-
|
|
64
|
-
|
|
154
|
+
try {
|
|
155
|
+
modDirs = await readdir(gamePath);
|
|
156
|
+
} catch {}
|
|
65
157
|
|
|
66
|
-
|
|
67
|
-
|
|
158
|
+
for (const modDir of modDirs) {
|
|
159
|
+
const mediaDir = path.join(gamePath, modDir, "media");
|
|
160
|
+
const publicDir = path.join(gamePath, modDir, "public");
|
|
68
161
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (error.code === "EADDRINUSE") {
|
|
73
|
-
console.log("server not started on port: ", port);
|
|
162
|
+
if (await pathExists(mediaDir)) mediaDirs.push(mediaDir);
|
|
163
|
+
if (await pathExists(publicDir)) publicDirs.push(publicDir);
|
|
164
|
+
}
|
|
74
165
|
|
|
75
|
-
|
|
166
|
+
await app.register(fastifyStatic, { root: publicDirs });
|
|
76
167
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
168
|
+
if (mediaDirs.length) {
|
|
169
|
+
await app.register(fastifyStatic, {
|
|
170
|
+
root: mediaDirs,
|
|
171
|
+
prefix: "/media/",
|
|
172
|
+
decorateReply: false,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return app;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const app = oldApp || (await createApp());
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await app.listen({ port, host: "0.0.0.0" });
|
|
183
|
+
|
|
184
|
+
logServerAddresses(port);
|
|
185
|
+
|
|
186
|
+
return port;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error.code === "EADDRINUSE") {
|
|
189
|
+
console.log("server not started on port: ", port);
|
|
190
|
+
|
|
191
|
+
return runServer({ rootDir, stateDir, oldApp: app, achievementLock, achievementUnlock });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
81
196
|
};
|
|
82
197
|
|
|
83
198
|
module.exports = { runServer };
|
package/src/web.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
const open = require("open");
|
|
1
|
+
const open = require("open").default;
|
|
2
2
|
const { runServer } = require("./server");
|
|
3
3
|
|
|
4
4
|
const getRootDir = () => process.env.PWD || process.env.INIT_CWD;
|
|
5
5
|
|
|
6
|
-
module.exports =
|
|
6
|
+
module.exports = async () => {
|
|
7
|
+
const port = await runServer({ rootDir: getRootDir() });
|
|
8
|
+
|
|
9
|
+
return open(`http://localhost:${port}`);
|
|
10
|
+
};
|