@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vnejs/desktop",
3
- "version": "0.0.35",
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/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({ autoHideMenuBar: options.isProd, fullscreen: true, frame: true });
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.loadFile(options.file, { search: options.search });
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(() => createWindow(options));
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 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 });
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
- 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
- );
105
+ if (!stateFiles.length) return reply.send({ status: "ok", state: {} });
19
106
 
20
- const onAchievementLock = (req, res) => {
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);
134
+ const createApp = async () => {
135
+ const app = fastify({ bodyLimit: 50 * 1024 * 1024 });
38
136
 
39
- const app =
40
- oldApp ||
41
- express()
42
- .use((req, res, next) => {
43
- res.header("Access-Control-Allow-Origin", "*");
137
+ await app.register(fastifyCors, {
138
+ origin: "*",
139
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
140
+ allowedHeaders: ["Content-Type", "Authorization"],
141
+ });
44
142
 
45
- next();
46
- })
47
- .use(bodyParser.json({ limit: "50mb" }))
48
- .use(express.static(path.join(rootDir, "dist")))
49
- .use("/state/save", onSaveState)
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
- readdirSync(gamePath).forEach((modDir) => {
56
- const mediaDir = path.join(gamePath, modDir, "media");
57
- const publicDir = path.join(gamePath, modDir, "public");
149
+ const publicDirs = [path.join(rootDir, "dist")];
150
+ const mediaDirs = [];
58
151
 
59
- existsSync(mediaDir) && app.use("/media", express.static(mediaDir));
60
- existsSync(publicDir) && app.use(express.static(publicDir));
61
- });
152
+ let modDirs = [];
62
153
 
63
- return new Promise(async (resolve) => {
64
- const server = app.listen(port);
154
+ try {
155
+ modDirs = await readdir(gamePath);
156
+ } catch {}
65
157
 
66
- server.on("listening", () => {
67
- console.log("server started on port: ", port);
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
- resolve(port);
70
- });
71
- server.on("error", async (error) => {
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
- await runServer({ rootDir, oldApp: app, achievementLock, achievementUnlock });
166
+ await app.register(fastifyStatic, { root: publicDirs });
76
167
 
77
- resolve(port);
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 = () => 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
+ };