@vnejs/desktop 0.0.36 → 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.
Files changed (3) hide show
  1. package/package.json +9 -7
  2. package/src/server.js +172 -59
  3. 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.36",
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
- "commander": "14.0.3",
19
- "express": "4.21.2",
20
- "get-port": "7.1.0",
21
- "open": "8.2.1"
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": "19.0.5",
25
- "electron": "~40.8.0"
26
+ "@electron/packager": "~20.0.1",
27
+ "electron": "~42.3.3"
26
28
  }
27
29
  }
package/src/server.js CHANGED
@@ -1,85 +1,198 @@
1
1
  const path = require("path");
2
- const express = require("express");
2
+ const os = require("os");
3
+ const fastify = require("fastify");
3
4
  const getPort = require("get-port").default;
4
- const bodyParser = require("body-parser");
5
- const { writeFile, appendFile, readdirSync, existsSync, readFile } = require("fs");
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 runServer = async ({ rootDir, oldApp, achievementLock, achievementUnlock } = {}) => {
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 stateJson = path.join(rootDir, "state.json");
68
+ const resolvedStateDir = stateDir || path.join(rootDir, "state");
11
69
  const gamePath = path.join(rootDir, "game");
12
70
 
13
- const onSaveState = (req, res) => writeFile(stateJson, JSON.stringify(req.body), (err) => (err ? res.json({ status: "error" }) : res.json({ status: "ok" })));
71
+ const ensureStateDir = () => mkdir(resolvedStateDir, { recursive: true });
14
72
 
15
- const onGetState = (req, res) =>
16
- readFile(stateJson, (err, data) =>
17
- err ? writeFile(stateJson, "{}", () => res.json({ status: "ok", state: {} })) : res.json({ status: "ok", state: JSON.parse(data.toString()) }),
18
- );
73
+ const getStateFiles = async () => {
74
+ try {
75
+ const files = await readdir(resolvedStateDir);
19
76
 
20
- const onAchievementLock = (req, res) => {
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();
104
+
105
+ if (!stateFiles.length) return reply.send({ status: "ok", state: {} });
106
+
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 res.json({ status: "ok" });
119
+ return reply.send({ status: "ok" });
24
120
  };
25
- const onAchievementUnlock = (req, res) => {
121
+
122
+ const onAchievementUnlock = (req, reply) => {
26
123
  typeof achievementUnlock === "function" && achievementUnlock(req.body?.id, req.body?.data);
27
124
 
28
- return res.json({ status: "ok" });
125
+ return reply.send({ status: "ok" });
29
126
  };
30
127
 
31
- const onLog = (req, res) => {
32
- res.end("ok");
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
- console.log("server will try to start on localhost: ", port);
38
-
39
- const app =
40
- oldApp ||
41
- express()
42
- .use((req, res, next) => {
43
- res.header("Access-Control-Allow-Origin", "*");
44
- res.header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
45
- res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
46
-
47
- next();
48
- })
49
- .use(bodyParser.json({ limit: "50mb" }))
50
- .use(express.static(path.join(rootDir, "dist")))
51
- .use("/state/save", onSaveState)
52
- .use("/state/get", onGetState)
53
- .use("/achievement/lock", onAchievementLock)
54
- .use("/achievement/unlock", onAchievementUnlock)
55
- .use("/log", onLog);
56
-
57
- readdirSync(gamePath).forEach((modDir) => {
58
- const mediaDir = path.join(gamePath, modDir, "media");
59
- const publicDir = path.join(gamePath, modDir, "public");
60
-
61
- existsSync(mediaDir) && app.use("/media", express.static(mediaDir));
62
- existsSync(publicDir) && app.use(express.static(publicDir));
63
- });
64
-
65
- return new Promise(async (resolve) => {
66
- const server = app.listen(port);
67
-
68
- server.on("listening", () => {
69
- console.log("server started on port: ", port);
70
-
71
- resolve(port);
134
+ const createApp = async () => {
135
+ const app = fastify({ bodyLimit: 50 * 1024 * 1024 });
136
+
137
+ await app.register(fastifyCors, {
138
+ origin: "*",
139
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
140
+ allowedHeaders: ["Content-Type", "Authorization"],
72
141
  });
73
- server.on("error", async (error) => {
74
- if (error.code === "EADDRINUSE") {
75
- console.log("server not started on port: ", port);
76
142
 
77
- await runServer({ rootDir, oldApp: app, achievementLock, achievementUnlock });
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);
78
148
 
79
- resolve(port);
80
- }
81
- });
82
- });
149
+ const publicDirs = [path.join(rootDir, "dist")];
150
+ const mediaDirs = [];
151
+
152
+ let modDirs = [];
153
+
154
+ try {
155
+ modDirs = await readdir(gamePath);
156
+ } catch {}
157
+
158
+ for (const modDir of modDirs) {
159
+ const mediaDir = path.join(gamePath, modDir, "media");
160
+ const publicDir = path.join(gamePath, modDir, "public");
161
+
162
+ if (await pathExists(mediaDir)) mediaDirs.push(mediaDir);
163
+ if (await pathExists(publicDir)) publicDirs.push(publicDir);
164
+ }
165
+
166
+ await app.register(fastifyStatic, { root: publicDirs });
167
+
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
+ }
83
196
  };
84
197
 
85
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 = () => runServer({ rootDir: getRootDir() }).then((port) => open(`http://localhost:${port}`));
6
+ module.exports = async () => {
7
+ const port = await runServer({ rootDir: getRootDir() });
8
+
9
+ return open(`http://localhost:${port}`);
10
+ };