@next2d/builder 0.2.0 → 0.3.1
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/README.md +2 -0
- package/dist/electron-config.d.ts +10 -0
- package/dist/electron-config.js +22 -4
- package/dist/electron-host.d.ts +3 -1
- package/dist/electron-host.js +15 -3
- package/dist/electron-native.d.ts +15 -0
- package/dist/electron-native.js +73 -0
- package/dist/electron.js +5 -4
- package/docs/native-bridge.md +147 -0
- package/docs/steam.md +176 -506
- package/package.json +6 -6
- package/templates/electron/index.js +27 -3
- package/templates/electron/native-bridge.cjs +98 -0
- package/templates/electron/preload.cjs +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@next2d/builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Multi-platform builder for Next2d Framework, supporting export to various platforms such as macOS, Windows, iOS, Android and Web(HTML)",
|
|
5
5
|
"author": "Toshiyuki Ienaga <ienaga@next2d.app>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@eslint/eslintrc": "^3.3.7",
|
|
44
44
|
"@eslint/js": "^10.0.1",
|
|
45
|
-
"@types/node": "^26.6.
|
|
46
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
47
|
-
"@typescript-eslint/parser": "^8.
|
|
45
|
+
"@types/node": "^26.6.4",
|
|
46
|
+
"@typescript-eslint/eslint-plugin": "^8.71.0",
|
|
47
|
+
"@typescript-eslint/parser": "^8.71.0",
|
|
48
48
|
"eslint": "^10.11.0",
|
|
49
49
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
50
|
-
"globals": "^17.
|
|
50
|
+
"globals": "^17.13.0",
|
|
51
51
|
"typescript": "^6.0.3",
|
|
52
|
-
"vite": "^8.3.
|
|
52
|
+
"vite": "^8.3.2"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
const { app, BrowserWindow, Menu, net, protocol, screen, session } = require("electron");
|
|
1
|
+
const { app, BrowserWindow, Menu, net, protocol, screen, session, ipcMain } = require("electron");
|
|
2
2
|
const path = require("node:path");
|
|
3
3
|
const fs = require("node:fs");
|
|
4
4
|
const { pathToFileURL } = require("node:url");
|
|
5
5
|
const { resolveAssetPath, CSP } = require("./local-assets.cjs");
|
|
6
6
|
const config = require("./runtime-config.json");
|
|
7
|
+
const { NativeBridge, isTrustedSender } = require("./native-bridge.cjs");
|
|
8
|
+
let nativeBridge;
|
|
9
|
+
const closeNativeBridge = () => nativeBridge?.close();
|
|
10
|
+
process.on("exit", closeNativeBridge);
|
|
11
|
+
app.on("before-quit", closeNativeBridge);
|
|
7
12
|
// Keep saves in a stable user directory even if the visible game name changes.
|
|
8
13
|
app.setName(config.appName);
|
|
9
|
-
app.
|
|
14
|
+
const userDataOverride = app.commandLine.getSwitchValue("user-data-dir");
|
|
15
|
+
app.setPath("userData", userDataOverride ? path.resolve(userDataOverride) : path.join(app.getPath("appData"), config.appId));
|
|
10
16
|
const assetRoot = app.isPackaged
|
|
11
17
|
? path.join(process.resourcesPath, "resources")
|
|
12
18
|
: path.join(__dirname, "resources");
|
|
@@ -31,7 +37,11 @@ const createWindow = async () => {
|
|
|
31
37
|
backgroundColor: "#000000",
|
|
32
38
|
autoHideMenuBar: true,
|
|
33
39
|
show: false,
|
|
34
|
-
webPreferences: {
|
|
40
|
+
webPreferences: {
|
|
41
|
+
nodeIntegration: false, contextIsolation: true, sandbox: true,
|
|
42
|
+
...(config.nativeBridge ? { preload: path.join(__dirname, "preload.cjs"),
|
|
43
|
+
backgroundThrottling: config.nativeBridge.backgroundThrottling ?? false } : {})
|
|
44
|
+
}
|
|
35
45
|
});
|
|
36
46
|
mainWindow.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
37
47
|
mainWindow.webContents.on("will-navigate", (event, url) => {
|
|
@@ -53,6 +63,7 @@ const createWindow = async () => {
|
|
|
53
63
|
});
|
|
54
64
|
mainWindow.webContents.on("render-process-gone", (_event, details) => {
|
|
55
65
|
console.error("Renderer exited:", details.reason);
|
|
66
|
+
closeNativeBridge();
|
|
56
67
|
app.exit(1);
|
|
57
68
|
});
|
|
58
69
|
mainWindow.once("ready-to-show", () => mainWindow.show());
|
|
@@ -85,6 +96,19 @@ app.whenReady().then(async () => {
|
|
|
85
96
|
return new Response("Not found", { status: 404 });
|
|
86
97
|
}
|
|
87
98
|
});
|
|
99
|
+
if (config.nativeBridge) {
|
|
100
|
+
ipcMain.handle("next2d:native:request", (event, method, params) => {
|
|
101
|
+
if (!isTrustedSender(event, mainWindow)) throw new Error("Native bridge access denied");
|
|
102
|
+
if (!config.nativeBridge.methods.includes(method)) throw new Error("Native method not allowed");
|
|
103
|
+
if (!nativeBridge) {
|
|
104
|
+
const nativeRoot = path.join(app.isPackaged ? process.resourcesPath : __dirname, "native");
|
|
105
|
+
nativeBridge = new NativeBridge(path.join(nativeRoot, config.nativeBridge.executable), config.nativeBridge.methods, (message) => {
|
|
106
|
+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send("next2d:native:event", message);
|
|
107
|
+
}, { cwd: nativeRoot });
|
|
108
|
+
}
|
|
109
|
+
return nativeBridge.request(method, params);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
88
112
|
await createWindow();
|
|
89
113
|
}).catch((error) => {
|
|
90
114
|
console.error(error);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const { spawn } = require("node:child_process");
|
|
2
|
+
const { StringDecoder } = require("node:string_decoder");
|
|
3
|
+
const MAX_BYTES = 256 * 1024;
|
|
4
|
+
|
|
5
|
+
class NativeBridge {
|
|
6
|
+
constructor(executable, methods, onEvent, options = {}) {
|
|
7
|
+
this.methods = new Set(methods);
|
|
8
|
+
this.pending = new Map();
|
|
9
|
+
this.sequence = 0;
|
|
10
|
+
this.failure = null;
|
|
11
|
+
this.timeout = options.timeout ?? 15000;
|
|
12
|
+
this.onEvent = onEvent;
|
|
13
|
+
this.child = spawn(executable, options.args ?? [], {
|
|
14
|
+
cwd: options.cwd, shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"]
|
|
15
|
+
});
|
|
16
|
+
const decoder = new StringDecoder("utf8");
|
|
17
|
+
let buffer = "";
|
|
18
|
+
this.child.stdout.on("data", (chunk) => {
|
|
19
|
+
if (this.failure) return;
|
|
20
|
+
buffer += decoder.write(chunk);
|
|
21
|
+
let end;
|
|
22
|
+
while ((end = buffer.indexOf("\n")) >= 0) {
|
|
23
|
+
const line = buffer.slice(0, end);
|
|
24
|
+
buffer = buffer.slice(end + 1);
|
|
25
|
+
if (Buffer.byteLength(line) > MAX_BYTES) return this.fail("Native message too large");
|
|
26
|
+
try {
|
|
27
|
+
const message = JSON.parse(line);
|
|
28
|
+
if (!message || typeof message !== "object") throw new Error("Invalid message");
|
|
29
|
+
if (typeof message.event === "string" && message.event.length <= 64 && message.event !== "bridge.closed") {
|
|
30
|
+
this.onEvent({ event: message.event, data: message.data ?? null });
|
|
31
|
+
} else if (Number.isSafeInteger(message.id)) {
|
|
32
|
+
const pending = this.pending.get(message.id);
|
|
33
|
+
if (!pending) continue;
|
|
34
|
+
clearTimeout(pending.timer);
|
|
35
|
+
this.pending.delete(message.id);
|
|
36
|
+
if (typeof message.error === "string") pending.reject(new Error(message.error));
|
|
37
|
+
else pending.resolve(message.result ?? null);
|
|
38
|
+
} else throw new Error("Invalid message");
|
|
39
|
+
} catch {
|
|
40
|
+
return this.fail("Invalid native protocol");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (Buffer.byteLength(buffer) > MAX_BYTES) this.fail("Native message too large");
|
|
44
|
+
});
|
|
45
|
+
// Drain diagnostics without sending credentials or SDK logs to the renderer.
|
|
46
|
+
this.child.stderr.resume();
|
|
47
|
+
this.child.stdin.on("error", () => this.fail("Native input closed"));
|
|
48
|
+
this.child.once("error", () => this.fail("Native process could not start"));
|
|
49
|
+
this.child.once("exit", () => this.fail("Native process exited"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async request(method, params) {
|
|
53
|
+
if (this.failure) throw new Error(this.failure);
|
|
54
|
+
if (!this.methods.has(method)) throw new Error("Native method not allowed");
|
|
55
|
+
if (this.pending.size >= 128 || this.child.stdin.writableLength > MAX_BYTES) throw new Error("Native bridge busy");
|
|
56
|
+
const id = ++this.sequence;
|
|
57
|
+
const line = JSON.stringify({ id, method, params: params ?? null }) + "\n";
|
|
58
|
+
if (Buffer.byteLength(line) > MAX_BYTES) throw new Error("Native request too large");
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
this.pending.delete(id);
|
|
62
|
+
reject(new Error("Native request timed out"));
|
|
63
|
+
}, this.timeout);
|
|
64
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
65
|
+
this.child.stdin.write(line);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fail(reason, graceful = false) {
|
|
70
|
+
if (this.failure) return;
|
|
71
|
+
this.failure = reason;
|
|
72
|
+
for (const pending of this.pending.values()) {
|
|
73
|
+
clearTimeout(pending.timer);
|
|
74
|
+
pending.reject(new Error(reason));
|
|
75
|
+
}
|
|
76
|
+
this.pending.clear();
|
|
77
|
+
if (graceful) {
|
|
78
|
+
this.child.stdin.end();
|
|
79
|
+
const timer = setTimeout(() => this.child.kill("SIGKILL"), 4000);
|
|
80
|
+
timer.unref();
|
|
81
|
+
this.child.once("exit", () => clearTimeout(timer));
|
|
82
|
+
} else this.child.kill("SIGKILL");
|
|
83
|
+
this.onEvent({ event: "bridge.closed", data: { reason } });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
close() { this.fail("Native bridge closed", true); }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const isTrustedSender = (event, window) => {
|
|
90
|
+
if (!window || window.isDestroyed() || event.sender !== window.webContents
|
|
91
|
+
|| event.senderFrame !== window.webContents.mainFrame) return false;
|
|
92
|
+
try {
|
|
93
|
+
const url = new URL(event.senderFrame.url);
|
|
94
|
+
return url.protocol === "next2d:" && url.host === "game" && !url.username && !url.password;
|
|
95
|
+
} catch { return false; }
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
module.exports = { NativeBridge, isTrustedSender };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require("electron");
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld("next2dNative", {
|
|
4
|
+
request: (method, params) => ipcRenderer.invoke("next2d:native:request", method, params),
|
|
5
|
+
onEvent: (listener) => {
|
|
6
|
+
if (typeof listener !== "function") throw new TypeError("Expected event listener");
|
|
7
|
+
const handler = (_event, message) => listener(message);
|
|
8
|
+
ipcRenderer.on("next2d:native:event", handler);
|
|
9
|
+
return () => ipcRenderer.removeListener("next2d:native:event", handler);
|
|
10
|
+
}
|
|
11
|
+
});
|