@vnejs/desktop 0.0.36 → 0.0.38

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vnejs/desktop",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
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/cli.js CHANGED
@@ -10,6 +10,9 @@ const program = new Command();
10
10
 
11
11
  program.name(packageJson.name).description(packageJson.description).version(packageJson.version);
12
12
 
13
- program.command("web").action(webAction);
13
+ program
14
+ .command("web")
15
+ .option("--port-preferred <port>", "preferred port number (overrides package.json vnejs.portPreferred)")
16
+ .action(webAction);
14
17
 
15
18
  program.parse();
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const { runServer } = require("./server");
2
2
  const { runElectron } = require("./electron");
3
+ const runWeb = require("./web");
3
4
 
4
- module.exports = { runElectron, runServer };
5
+ module.exports = { runElectron, runServer, runWeb };
package/src/server.js CHANGED
@@ -1,85 +1,199 @@
1
1
  const path = require("path");
2
- const express = require("express");
3
- const getPort = require("get-port").default;
4
- const bodyParser = require("body-parser");
5
- const { writeFile, appendFile, readdirSync, existsSync, readFile } = require("fs");
2
+ const os = require("os");
3
+ const fastify = require("fastify");
4
+ const fastifyCors = require("@fastify/cors");
5
+ const fastifyStatic = require("@fastify/static");
6
+ const { readFile, readdir, writeFile, appendFile, mkdir, unlink, access } = require("fs/promises");
7
+ const { default: getPort } = require("get-port");
6
8
 
7
- const runServer = async ({ rootDir, oldApp, achievementLock, achievementUnlock } = {}) => {
8
- const port = await getPort();
9
+ const getPrefix = (key) => {
10
+ const dot = key.indexOf(".");
9
11
 
10
- const stateJson = path.join(rootDir, "state.json");
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 readPortPreferred = async (rootDir) => {
66
+ const pkg = JSON.parse(await readFile(path.join(rootDir, "package.json"), "utf8"));
67
+ const port = Number(pkg.vnejs?.portPreferred);
68
+
69
+ return Number.isInteger(port) && port > 0 ? port : undefined;
70
+ };
71
+
72
+ const runServer = async ({ rootDir, stateDir, oldApp, onAchievementLock, onAchievementUnlock } = {}) => {
73
+ const port = await getPort({ port: await readPortPreferred(rootDir) });
74
+
75
+ const resolvedStateDir = stateDir || path.join(rootDir, "state");
11
76
  const gamePath = path.join(rootDir, "game");
12
77
 
13
- const onSaveState = (req, res) => writeFile(stateJson, JSON.stringify(req.body), (err) => (err ? res.json({ status: "error" }) : res.json({ status: "ok" })));
78
+ const ensureStateDir = () => mkdir(resolvedStateDir, { recursive: true });
79
+
80
+ const getStateFiles = async () => {
81
+ try {
82
+ const files = await readdir(resolvedStateDir);
83
+
84
+ return files.filter((file) => file.endsWith(".json"));
85
+ } catch {
86
+ return [];
87
+ }
88
+ };
89
+
90
+ const onSaveState = async (req, reply) => {
91
+ try {
92
+ await ensureStateDir();
14
93
 
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
- );
94
+ const groups = splitStateByPrefix(req.body);
95
+ const activePrefixes = new Set(Object.keys(groups));
96
+ const stalePrefixes = (await getStateFiles()).map((file) => file.slice(0, -".json".length)).filter((prefix) => !activePrefixes.has(prefix));
19
97
 
20
- const onAchievementLock = (req, res) => {
21
- typeof achievementLock === "function" && achievementLock(req.body?.id, req.body?.data);
98
+ await Promise.all([
99
+ ...Object.entries(groups).map(([prefix, state]) => writeFile(path.join(resolvedStateDir, `${prefix}.json`), JSON.stringify(state))),
100
+ ...stalePrefixes.map((prefix) => unlink(path.join(resolvedStateDir, `${prefix}.json`))),
101
+ ]);
22
102
 
23
- return res.json({ status: "ok" });
103
+ return reply.send({ status: "ok" });
104
+ } catch {
105
+ return reply.send({ status: "error" });
106
+ }
24
107
  };
25
- const onAchievementUnlock = (req, res) => {
26
- typeof achievementUnlock === "function" && achievementUnlock(req.body?.id, req.body?.data);
27
108
 
28
- return res.json({ status: "ok" });
109
+ const onGetState = async (req, reply) => {
110
+ const stateFiles = await getStateFiles();
111
+
112
+ if (!stateFiles.length) return reply.send({ status: "ok", state: {} });
113
+
114
+ try {
115
+ const chunks = await Promise.all(stateFiles.map((file) => readFile(path.join(resolvedStateDir, file), "utf8").then(JSON.parse)));
116
+
117
+ return reply.send({ status: "ok", state: Object.assign({}, ...chunks) });
118
+ } catch {
119
+ return reply.send({ status: "error" });
120
+ }
29
121
  };
30
122
 
31
- const onLog = (req, res) => {
32
- res.end("ok");
123
+ const onAchievementLockMiddleware = (req, reply) => {
124
+ typeof onAchievementLock === "function" && onAchievementLock(req.body?.id, req.body?.data);
125
+
126
+ return reply.send({ status: "ok" });
127
+ };
128
+
129
+ const onAchievementUnlockMiddleware = (req, reply) => {
130
+ typeof onAchievementUnlock === "function" && onAchievementUnlock(req.body?.id, req.body?.data);
131
+
132
+ return reply.send({ status: "ok" });
133
+ };
134
+
135
+ const onLog = (req, reply) => {
136
+ reply.send("ok");
33
137
 
34
138
  appendFile(path.join(rootDir, "log.txt"), `${req.body.log}\n`);
35
139
  };
36
140
 
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);
141
+ const createApp = async () => {
142
+ const app = fastify({ bodyLimit: 50 * 1024 * 1024 });
143
+
144
+ await app.register(fastifyCors, {
145
+ origin: "*",
146
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
147
+ allowedHeaders: ["Content-Type", "Authorization"],
72
148
  });
73
- server.on("error", async (error) => {
74
- if (error.code === "EADDRINUSE") {
75
- console.log("server not started on port: ", port);
76
149
 
77
- await runServer({ rootDir, oldApp: app, achievementLock, achievementUnlock });
150
+ app.post("/state/save", onSaveState);
151
+ app.get("/state/get", onGetState);
152
+ app.post("/achievement/lock", onAchievementLockMiddleware);
153
+ app.post("/achievement/unlock", onAchievementUnlockMiddleware);
154
+ app.post("/log", onLog);
78
155
 
79
- resolve(port);
80
- }
81
- });
82
- });
156
+ const publicDirs = [path.join(rootDir, "dist")];
157
+ const mediaDirs = [];
158
+
159
+ let modDirs = [];
160
+
161
+ try {
162
+ modDirs = await readdir(gamePath);
163
+ } catch {}
164
+
165
+ for (const modDir of modDirs) {
166
+ const mediaDir = path.join(gamePath, modDir, "media");
167
+ const publicDir = path.join(gamePath, modDir, "public");
168
+
169
+ if (await pathExists(mediaDir)) mediaDirs.push(mediaDir);
170
+ if (await pathExists(publicDir)) publicDirs.push(publicDir);
171
+ }
172
+
173
+ await app.register(fastifyStatic, { root: publicDirs });
174
+
175
+ if (mediaDirs.length) await app.register(fastifyStatic, { root: mediaDirs, prefix: "/media/", decorateReply: false });
176
+
177
+ return app;
178
+ };
179
+
180
+ const app = oldApp || (await createApp());
181
+
182
+ try {
183
+ await app.listen({ port, host: "0.0.0.0" });
184
+
185
+ logServerAddresses(port);
186
+
187
+ return port;
188
+ } catch (error) {
189
+ if (error.code === "EADDRINUSE") {
190
+ console.log("server not started on port: ", port);
191
+
192
+ return runServer({ rootDir, stateDir, oldApp: app, onAchievementLock, onAchievementUnlock });
193
+ }
194
+
195
+ throw error;
196
+ }
83
197
  };
84
198
 
85
199
  module.exports = { runServer };
package/src/web.js CHANGED
@@ -1,6 +1,12 @@
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
+ const runWeb = async ({ rootDir = getRootDir() } = {}) => {
7
+ const port = await runServer({ rootDir });
8
+
9
+ return open(`http://localhost:${port}/?port=${port}`);
10
+ };
11
+
12
+ module.exports = runWeb;