positron.js 1.0.4 → 1.0.6
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/bin/positron.js +6 -1
- package/builder.js +25 -2
- package/core/mac/main.swift +104 -21
- package/core/mac/tray.swift +95 -0
- package/core/win/PositronRuntime.csproj +1 -1
- package/core/win/main.cs +176 -22
- package/core/win/tray.cs +142 -0
- package/index.js +165 -42
- package/package.json +9 -2
- package/packager.js +47 -12
- package/tray.js +64 -0
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const { info, error, warn, success } = require("./logs");
|
|
|
13
13
|
|
|
14
14
|
let currMenu = []
|
|
15
15
|
let contextMenu = [];
|
|
16
|
+
let trayMenu = [];
|
|
16
17
|
|
|
17
18
|
const randomPort = () => {
|
|
18
19
|
const min = 1024;
|
|
@@ -180,7 +181,7 @@ ws.on("message", raw => {
|
|
|
180
181
|
win.destroy();
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
|
-
} else if(msg.event == "menu-action" || msg.event == "context-menu-action") {
|
|
184
|
+
} else if(msg.event == "menu-action" || msg.event == "context-menu-action" || msg.event == "tray-menu-action") {
|
|
184
185
|
|
|
185
186
|
const findMenuAction = (items, label, channel) => {
|
|
186
187
|
if (!items || items.length === 0) return null;
|
|
@@ -199,7 +200,12 @@ ws.on("message", raw => {
|
|
|
199
200
|
return null;
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
|
|
203
|
+
let searchMenu = msg.event === "menu-action" ? currMenu : (msg.event === "context-menu-action" ? contextMenu : trayMenu);
|
|
204
|
+
let menuAction = findMenuAction(searchMenu, msg.data.label, msg.data.channel);
|
|
205
|
+
|
|
206
|
+
if (!menuAction && msg.event === "context-menu-action") {
|
|
207
|
+
menuAction = findMenuAction(trayMenu, msg.data.label, msg.data.channel);
|
|
208
|
+
}
|
|
203
209
|
|
|
204
210
|
if (menuAction) {
|
|
205
211
|
menuAction.click();
|
|
@@ -317,7 +323,7 @@ class Window extends Events.EventEmitter {
|
|
|
317
323
|
* @param {string} path The path to the file to load.
|
|
318
324
|
*/
|
|
319
325
|
async loadFile(path) {
|
|
320
|
-
const res = await this.request("loadFile", `loadFile-reply-${this.id}
|
|
326
|
+
const res = await this.request("loadFile", { replyChannel: `loadFile-reply-${this.id}` }, path);
|
|
321
327
|
this.emit("file-loaded", path);
|
|
322
328
|
this.emit("navigated", path);
|
|
323
329
|
return res;
|
|
@@ -328,7 +334,7 @@ class Window extends Events.EventEmitter {
|
|
|
328
334
|
* @param {string} url The URL to load.
|
|
329
335
|
*/
|
|
330
336
|
async loadURL(url) {
|
|
331
|
-
const res = await this.request("loadURL", `loadURL-reply-${this.id}
|
|
337
|
+
const res = await this.request("loadURL", { replyChannel: `loadURL-reply-${this.id}` }, url);
|
|
332
338
|
this.emit("url-loaded", url);
|
|
333
339
|
this.emit("navigated", url);
|
|
334
340
|
return res;
|
|
@@ -349,7 +355,7 @@ emitToRenderer(channel, args = []) {
|
|
|
349
355
|
activeSocket.send(payload);
|
|
350
356
|
this.emit("ipc-sent", { channel, args });
|
|
351
357
|
} else {
|
|
352
|
-
warn(`Cannot send IPC message, socket not ready
|
|
358
|
+
warn(`Cannot send IPC message on ${channel}, socket not ready.`);
|
|
353
359
|
}
|
|
354
360
|
}
|
|
355
361
|
|
|
@@ -575,7 +581,7 @@ focus() {
|
|
|
575
581
|
* @returns {Promise<boolean>} True if the window is visible, false otherwise.
|
|
576
582
|
*/
|
|
577
583
|
async isVisible() {
|
|
578
|
-
const res = await this.request("isVisible"
|
|
584
|
+
const res = await this.request("isVisible");
|
|
579
585
|
return res?.isVisible === "true";
|
|
580
586
|
}
|
|
581
587
|
|
|
@@ -584,7 +590,7 @@ async isVisible() {
|
|
|
584
590
|
* @returns {Promise<boolean>} True if the window is fullscreen, false otherwise.
|
|
585
591
|
*/
|
|
586
592
|
async isFullscreen() {
|
|
587
|
-
const res = await this.request("isFullscreen"
|
|
593
|
+
const res = await this.request("isFullscreen");
|
|
588
594
|
return res?.isFullscreen === "true";
|
|
589
595
|
}
|
|
590
596
|
|
|
@@ -603,7 +609,7 @@ reload() {
|
|
|
603
609
|
* @returns {Promise<Buffer|null>} The captured screenshot as a Buffer, or null if the capture failed.
|
|
604
610
|
*/
|
|
605
611
|
async capturePage() {
|
|
606
|
-
const response = await this.request("capturePage", `capture-page-result-${this.id}`);
|
|
612
|
+
const response = await this.request("capturePage", { replyChannel: `capture-page-result-${this.id}` });
|
|
607
613
|
return response.image ? Buffer.from(response.image, "base64") : null;
|
|
608
614
|
}
|
|
609
615
|
|
|
@@ -612,52 +618,94 @@ async capturePage() {
|
|
|
612
618
|
* @returns {Promise<boolean>} True if the window can navigate back, false otherwise.
|
|
613
619
|
*/
|
|
614
620
|
async canGoBack() {
|
|
615
|
-
const response = await this.request("canGoBack"
|
|
621
|
+
const response = await this.request("canGoBack");
|
|
616
622
|
return response === "true";
|
|
617
623
|
}
|
|
618
624
|
|
|
625
|
+
// @ts-check
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* @typedef {Object} RequestOptions
|
|
629
|
+
* @property {number} [timeout]
|
|
630
|
+
* @property {boolean} [noTimeout]
|
|
631
|
+
* @property {string} [replyChannel]
|
|
632
|
+
*/
|
|
633
|
+
|
|
619
634
|
/**
|
|
620
|
-
*
|
|
621
|
-
* @param {string} command
|
|
622
|
-
* @param {
|
|
623
|
-
* @returns {Promise
|
|
635
|
+
* @overload
|
|
636
|
+
* @param {string} command
|
|
637
|
+
* @param {...any} args
|
|
638
|
+
* @returns {Promise<any>}
|
|
624
639
|
*/
|
|
625
|
-
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* @overload
|
|
643
|
+
* @param {string} command
|
|
644
|
+
* @param {RequestOptions} options
|
|
645
|
+
* @param {...any} args
|
|
646
|
+
* @returns {Promise<any>}
|
|
647
|
+
*/
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Send an IPC request.
|
|
651
|
+
* @param {string} command
|
|
652
|
+
* @param {...any} args
|
|
653
|
+
*/
|
|
654
|
+
async request(command, ...args) {
|
|
626
655
|
return new Promise((resolve, reject) => {
|
|
627
656
|
let settled = false;
|
|
628
657
|
|
|
658
|
+
let options = {};
|
|
659
|
+
|
|
660
|
+
if(typeof args[0] === "object" && args[0].constructor === Object) {
|
|
661
|
+
options = args[0];
|
|
662
|
+
args = args.slice(1);
|
|
663
|
+
}
|
|
664
|
+
|
|
629
665
|
if(!command) {
|
|
630
666
|
reject(new Error("Command is required for request"));
|
|
631
667
|
return;
|
|
632
668
|
}
|
|
633
669
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
670
|
+
const reqId = crypto.randomUUID();
|
|
671
|
+
|
|
672
|
+
let replyChannel = `${command}-reply-${reqId}`;
|
|
673
|
+
|
|
674
|
+
if (options.replyChannel) {
|
|
675
|
+
replyChannel = options.replyChannel;
|
|
676
|
+
} else if(args[0] && (args[0].includes("-reply-") || args[0].includes("-result-"))) {
|
|
677
|
+
// TEMP TRANSITIONAL LOGIC TO SUPPORT LEGACY REQUESTS THAT PASS REPLY CHANNEL AS FIRST ARGUMENT. WILL BE REMOVED IN A FUTURE RELEASE.
|
|
678
|
+
replyChannel = args[0];
|
|
679
|
+
}
|
|
637
680
|
|
|
638
681
|
const unsubscribe = ipc.handle(replyChannel, (data) => {
|
|
639
682
|
if (!settled) {
|
|
640
683
|
settled = true;
|
|
641
684
|
clearTimeout(timeout);
|
|
642
685
|
unsubscribe();
|
|
686
|
+
|
|
687
|
+
for (const key in data) {
|
|
688
|
+
if (data[key] === "true") {
|
|
689
|
+
data[key] = true;
|
|
690
|
+
} else if (data[key] === "false") {
|
|
691
|
+
data[key] = false;
|
|
692
|
+
} else if (!isNaN(data[key])) {
|
|
693
|
+
data[key] = Number(data[key]);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
643
697
|
resolve(data);
|
|
644
698
|
}
|
|
645
699
|
});
|
|
646
700
|
|
|
647
701
|
let timeout;
|
|
648
702
|
|
|
649
|
-
if(!
|
|
703
|
+
if(!options.noTimeout) {
|
|
650
704
|
let timeoutDuration = 7000;
|
|
651
|
-
|
|
652
|
-
const timeoutArg = args.find(
|
|
653
|
-
arg => typeof arg === "string" && arg.startsWith("TIMEOUT=")
|
|
654
|
-
);
|
|
655
|
-
|
|
656
|
-
if (timeoutArg) {
|
|
657
|
-
|
|
658
|
-
args = args.filter(arg => arg !== timeoutArg);
|
|
705
|
+
|
|
659
706
|
|
|
660
|
-
|
|
707
|
+
if (options.timeout) {
|
|
708
|
+
timeoutDuration = options.timeout;
|
|
661
709
|
}
|
|
662
710
|
|
|
663
711
|
timeout = setTimeout(() => {
|
|
@@ -680,7 +728,7 @@ if (timeoutArg) {
|
|
|
680
728
|
* @returns {Promise<boolean>} True if the window can navigate forward, false otherwise.
|
|
681
729
|
*/
|
|
682
730
|
async canGoForward() {
|
|
683
|
-
const response = await this.request("canGoForward"
|
|
731
|
+
const response = await this.request("canGoForward");
|
|
684
732
|
return response === "true";
|
|
685
733
|
}
|
|
686
734
|
|
|
@@ -745,7 +793,7 @@ setBounds(x, y, width, height) {
|
|
|
745
793
|
* @returns {Promise<string|null>} The user's input as a string, or null if the user cancelled the prompt.
|
|
746
794
|
*/
|
|
747
795
|
async prompt(message, defaultValue = "") {
|
|
748
|
-
const res = await this.request("prompt",
|
|
796
|
+
const res = await this.request("prompt", message, defaultValue);
|
|
749
797
|
this.emit("prompt", { message, defaultValue });
|
|
750
798
|
return res?.input;
|
|
751
799
|
}
|
|
@@ -782,7 +830,7 @@ setContextMenu(menuTemplate) {
|
|
|
782
830
|
* @returns {Promise<boolean>} True if the window is focused, false otherwise.
|
|
783
831
|
*/
|
|
784
832
|
async isFocused() {
|
|
785
|
-
const res = await this.request("isFocused"
|
|
833
|
+
const res = await this.request("isFocused");
|
|
786
834
|
return res?.isFocused === "true";
|
|
787
835
|
}
|
|
788
836
|
|
|
@@ -791,7 +839,7 @@ async isFocused() {
|
|
|
791
839
|
* @returns {Promise<{x: number, y: number, width: number, height: number}>} An object containing the window's bounds.
|
|
792
840
|
*/
|
|
793
841
|
async getBounds() {
|
|
794
|
-
return await this.request("getBounds"
|
|
842
|
+
return await this.request("getBounds");
|
|
795
843
|
}
|
|
796
844
|
|
|
797
845
|
/**
|
|
@@ -799,7 +847,7 @@ async getBounds() {
|
|
|
799
847
|
* @returns {Promise<string>} The current URL loaded in the window.
|
|
800
848
|
*/
|
|
801
849
|
async getURL() {
|
|
802
|
-
return (await this.request("getURL"
|
|
850
|
+
return (await this.request("getURL"))?.url || "";
|
|
803
851
|
}
|
|
804
852
|
|
|
805
853
|
/**
|
|
@@ -807,7 +855,7 @@ async getURL() {
|
|
|
807
855
|
* @returns {Promise<string>} The current title of the window.
|
|
808
856
|
*/
|
|
809
857
|
async getTitle() {
|
|
810
|
-
return (await this.request("getTitle"
|
|
858
|
+
return (await this.request("getTitle"))?.title || "";
|
|
811
859
|
}
|
|
812
860
|
|
|
813
861
|
/**
|
|
@@ -834,7 +882,7 @@ setTitlebarTransparent(isTransparent) {
|
|
|
834
882
|
* @returns {Promise<*>} A Promise that resolves to the result of the evaluation.
|
|
835
883
|
*/
|
|
836
884
|
async evaluateJavaScript(script) {
|
|
837
|
-
const res = await this.request("evaluateJS",
|
|
885
|
+
const res = await this.request("evaluateJS", script);
|
|
838
886
|
return res.result;
|
|
839
887
|
}
|
|
840
888
|
|
|
@@ -984,7 +1032,7 @@ async removeOnClick(selector) {
|
|
|
984
1032
|
* @returns {Promise<boolean>} True if the user confirmed, false if the user cancelled.
|
|
985
1033
|
*/
|
|
986
1034
|
async confirm(message) {
|
|
987
|
-
const res = await this.request("confirm",
|
|
1035
|
+
const res = await this.request("confirm", message);
|
|
988
1036
|
this.emit("confirm", message);
|
|
989
1037
|
return res?.confirmed === "true";
|
|
990
1038
|
}
|
|
@@ -995,7 +1043,7 @@ async confirm(message) {
|
|
|
995
1043
|
* @returns {Promise<void>} A Promise that resolves when the swipe navigation setting has been updated.
|
|
996
1044
|
*/
|
|
997
1045
|
async setSwipeNavigation(enabled) {
|
|
998
|
-
const res = await this.request("setSwipeNav",
|
|
1046
|
+
const res = await this.request("setSwipeNav", String(enabled));
|
|
999
1047
|
this.emit("swipe-navigation-updated", enabled);
|
|
1000
1048
|
}
|
|
1001
1049
|
|
|
@@ -1004,7 +1052,7 @@ this.emit("swipe-navigation-updated", enabled);
|
|
|
1004
1052
|
* @returns {Promise<boolean>} True if swipe navigation is enabled, false otherwise.
|
|
1005
1053
|
*/
|
|
1006
1054
|
async isSwipeNavigationEnabled() {
|
|
1007
|
-
const res = await this.request("isSwipeNavEnabled"
|
|
1055
|
+
const res = await this.request("isSwipeNavEnabled");
|
|
1008
1056
|
return res?.enabled === "true";
|
|
1009
1057
|
}
|
|
1010
1058
|
|
|
@@ -1044,10 +1092,19 @@ async addToContentBlocker(config={ json:[], url:"", file:"", reload:true, clearE
|
|
|
1044
1092
|
}
|
|
1045
1093
|
|
|
1046
1094
|
|
|
1047
|
-
const res = await this.request("addToContentBlocker",
|
|
1095
|
+
const res = await this.request("addToContentBlocker", json, config.reload, config.clearExisting);
|
|
1048
1096
|
this.emit("content-blocker-updated", json);
|
|
1049
1097
|
}
|
|
1050
1098
|
|
|
1099
|
+
/**
|
|
1100
|
+
* Displays a file open dialog and returns a Promise that resolves to an array of selected file paths, or null if the user cancelled the dialog.
|
|
1101
|
+
* @param {*} options The options for the file open dialog.
|
|
1102
|
+
* @returns
|
|
1103
|
+
*/
|
|
1104
|
+
async showFileOpenDialog(options = {}) {
|
|
1105
|
+
this.emit("show-file-open-dialog", options);
|
|
1106
|
+
return this.request("showFileOpenDialog", options);
|
|
1107
|
+
}
|
|
1051
1108
|
|
|
1052
1109
|
|
|
1053
1110
|
}
|
|
@@ -1055,6 +1112,10 @@ async addToContentBlocker(config={ json:[], url:"", file:"", reload:true, clearE
|
|
|
1055
1112
|
const app = {
|
|
1056
1113
|
|
|
1057
1114
|
name:"PositronApp",
|
|
1115
|
+
|
|
1116
|
+
setTrayMenu(menuTemplate) {
|
|
1117
|
+
trayMenu = menuTemplate;
|
|
1118
|
+
},
|
|
1058
1119
|
|
|
1059
1120
|
/**
|
|
1060
1121
|
* Quits the application by sending a terminate command to the native layer and then exiting the process. Emits a "before-quit" event before sending the command, and a "quit" event after initiating the quit sequence.
|
|
@@ -1111,8 +1172,10 @@ const app = {
|
|
|
1111
1172
|
* @returns {Promise<Window|null>} The currently focused window, or null if no windows are focused.
|
|
1112
1173
|
*/
|
|
1113
1174
|
async getFocusedWindow() {
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1175
|
+
const getFocused = await this.requestFromNative("getFocusedWindowId");
|
|
1176
|
+
const winId = getFocused?.focusedWindowId ? parseInt(getFocused.focusedWindowId, 10) : -1;
|
|
1177
|
+
const focusedWin = [...activeWindows].find(w => w.id === winId);
|
|
1178
|
+
return focusedWin || { error: "No focused window found" };
|
|
1116
1179
|
},
|
|
1117
1180
|
|
|
1118
1181
|
/**
|
|
@@ -1194,11 +1257,71 @@ userData: {
|
|
|
1194
1257
|
/**
|
|
1195
1258
|
* Full access to the underlying event emitter for application-level events, allowing for advanced event handling patterns if needed.
|
|
1196
1259
|
*/
|
|
1197
|
-
events: appEvents
|
|
1260
|
+
events: appEvents,
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Sends a command to the native layer. This is a low-level method that can be used to send arbitrary commands, but for most use cases you will want to use the higher-level methods provided by the Window class instead. Emits an "ipc-sent" event with the command and arguments as data when done.
|
|
1264
|
+
* @param {string} command The command to send to the native layer.
|
|
1265
|
+
* @param {any[]} args The arguments to send with the command.
|
|
1266
|
+
*/
|
|
1267
|
+
sendToNative(command, args) {
|
|
1268
|
+
const firstWin = activeWindows.values().next().value;
|
|
1269
|
+
if (firstWin) {
|
|
1270
|
+
firstWin.sendCommand(command, args);
|
|
1271
|
+
} else {
|
|
1272
|
+
error("No active windows to send command to native layer");
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1198
1275
|
|
|
1276
|
+
/**
|
|
1277
|
+
* Sends a request to the native layer and returns a Promise that resolves to the response. This is a low-level method that can be used to send arbitrary requests, but for most use cases you will want to use the higher-level methods provided by the Window class instead. Emits an "ipc-request-sent" event with the command and arguments as data when done.
|
|
1278
|
+
* @param {string} command The command to send to the native layer.
|
|
1279
|
+
* @param {any[]} args The arguments to send with the command.
|
|
1280
|
+
* @returns {Promise<any>} A Promise that resolves to the response from the native layer.
|
|
1281
|
+
*/
|
|
1282
|
+
async requestFromNative(command, ...args) {
|
|
1283
|
+
const firstWin = activeWindows.values().next().value;
|
|
1284
|
+
if (firstWin) {
|
|
1285
|
+
return await firstWin.request(command, ...args);
|
|
1286
|
+
} else {
|
|
1287
|
+
error("No active windows to send request to native layer");
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
|
|
1292
|
+
async isDarkMode() {
|
|
1293
|
+
const res = await this.requestFromNative("isDarkMode");
|
|
1294
|
+
return res?.isDarkMode;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const clipboard = {
|
|
1300
|
+
|
|
1301
|
+
async writeText(text) {
|
|
1302
|
+
app.sendToNative("writeToClipboard", [text]);
|
|
1303
|
+
},
|
|
1304
|
+
|
|
1305
|
+
async readText() {
|
|
1306
|
+
const res = await app.requestFromNative("readFromClipboard");
|
|
1307
|
+
return res.text || "";
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const blockPowerSave = {
|
|
1313
|
+
|
|
1314
|
+
start() {
|
|
1315
|
+
app.sendToNative("blockPowerSave");
|
|
1316
|
+
},
|
|
1317
|
+
|
|
1318
|
+
stop() {
|
|
1319
|
+
app.sendToNative("unblockPowerSave");
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1199
1322
|
}
|
|
1200
1323
|
|
|
1201
|
-
module.exports = { Window, ipc, isPackaged, app, PORT };
|
|
1324
|
+
module.exports = { Window, ipc, isPackaged, app, PORT, clipboard, blockPowerSave };
|
|
1202
1325
|
|
|
1203
1326
|
const findNearestPackageJson = require("./findpackage");
|
|
1204
1327
|
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "https://github.com/systemsoftware/positron.js"
|
|
6
6
|
},
|
|
7
7
|
"homepage": "https://positronjs.gitbook.io",
|
|
8
|
-
"version": "1.0.
|
|
8
|
+
"version": "1.0.6",
|
|
9
9
|
"main": "index.js",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node --test"
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
"keywords": [
|
|
14
14
|
"app",
|
|
15
15
|
"framework",
|
|
16
|
-
"desktop"
|
|
16
|
+
"desktop",
|
|
17
|
+
"cross-platform",
|
|
18
|
+
"native",
|
|
19
|
+
"javascript",
|
|
20
|
+
"node",
|
|
21
|
+
"macos",
|
|
22
|
+
"windows"
|
|
17
23
|
],
|
|
18
24
|
"author": "Bryce",
|
|
19
25
|
"license": "MIT",
|
|
@@ -21,6 +27,7 @@
|
|
|
21
27
|
"dependencies": {
|
|
22
28
|
"@yao-pkg/pkg": "^6.20.0",
|
|
23
29
|
"esbuild": "^0.28.0",
|
|
30
|
+
"semver": "^7.8.1",
|
|
24
31
|
"ws": "^8.20.1"
|
|
25
32
|
},
|
|
26
33
|
"bin": {
|
package/packager.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const { execSync } = require("child_process");
|
|
3
|
+
const { execSync, execFileSync } = require("child_process");
|
|
4
4
|
const { info, error, success } = require("./logs");
|
|
5
5
|
const https = require("https");
|
|
6
6
|
const esbuild = require("esbuild");
|
|
@@ -33,24 +33,27 @@ function performPackager() {
|
|
|
33
33
|
|
|
34
34
|
function handleJavaScriptPipeline(appRoot, resourcesPath) {
|
|
35
35
|
const targetOutputFile = path.join(resourcesPath, "index.js");
|
|
36
|
+
let bundledFiles = [];
|
|
36
37
|
|
|
37
38
|
info(`[Packager] Bundling JavaScript code with esbuild...`);
|
|
38
39
|
try {
|
|
39
|
-
esbuild.buildSync({
|
|
40
|
+
const result = esbuild.buildSync({
|
|
40
41
|
entryPoints: [path.join(appRoot, "index.js")],
|
|
41
42
|
bundle: true,
|
|
42
43
|
platform: "node",
|
|
43
44
|
target: `node${MAJOR_NODE_V}`,
|
|
44
45
|
outfile: targetOutputFile,
|
|
45
|
-
minify:
|
|
46
|
+
minify: true,
|
|
46
47
|
sourcemap: false,
|
|
48
|
+
metafile: true,
|
|
47
49
|
});
|
|
50
|
+
bundledFiles = Object.keys(result.metafile.inputs).map(f => path.resolve(appRoot, f));
|
|
48
51
|
} catch (err) {
|
|
49
52
|
error("Fatal: esbuild bundling failed.");
|
|
50
53
|
process.exit(1);
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
copyAppAssets(appRoot, resourcesPath);
|
|
56
|
+
copyAppAssets(appRoot, resourcesPath, bundledFiles);
|
|
54
57
|
|
|
55
58
|
}
|
|
56
59
|
|
|
@@ -70,9 +73,12 @@ async function packageMacOS(appRoot, distDir, appName) {
|
|
|
70
73
|
error("Fatal: Native compiled binary missing from bin/. Run build first.");
|
|
71
74
|
process.exit(1);
|
|
72
75
|
}
|
|
73
|
-
|
|
74
|
-
fs.
|
|
75
|
-
|
|
76
|
+
const binaryPath = path.join(macosPath, appName);
|
|
77
|
+
fs.copyFileSync(compiledBinary, binaryPath);
|
|
78
|
+
fs.chmodSync(binaryPath, "755");
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
76
82
|
const packageJsonPath = path.join(appRoot, "package.json");
|
|
77
83
|
const package = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
78
84
|
|
|
@@ -105,14 +111,29 @@ async function packageMacOS(appRoot, distDir, appName) {
|
|
|
105
111
|
handleJavaScriptPipeline(appRoot, resourcesPath);
|
|
106
112
|
|
|
107
113
|
const bundledJs = path.join(resourcesPath, "index.js");
|
|
114
|
+
const backendName = appName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[\s_]+/g, '-').toLowerCase() + '-backend';
|
|
108
115
|
if (fs.existsSync(bundledJs)) {
|
|
109
|
-
await compileWithPkg(bundledJs, "darwin", resourcesPath,
|
|
116
|
+
await compileWithPkg(bundledJs, "darwin", resourcesPath, backendName);
|
|
117
|
+
const binPathEscaped = path.join(resourcesPath, backendName).replace(/"/g, '\\"');
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
let swiftScript = "";
|
|
121
|
+
if(fs.existsSync(path.join(__dirname, ['positronicon', 'png'].join('.')))) {
|
|
122
|
+
const iconPathEscaped = path.join(__dirname, ['positronicon', 'png'].join('.')).replace(/"/g, '\\"');
|
|
123
|
+
swiftScript = `import Cocoa; NSWorkspace.shared.setIcon(NSImage(contentsOfFile: "${iconPathEscaped}"), forFile: "${binPathEscaped}", options: []);`;
|
|
124
|
+
}
|
|
125
|
+
execFileSync("swift", ["-e", swiftScript], { stdio: "ignore" });
|
|
126
|
+
} catch (err) {
|
|
127
|
+
error("Failed to set custom icon on native binary:", err);
|
|
128
|
+
}
|
|
110
129
|
|
|
111
|
-
// fs.renameSync(targetNodePath, path.join(resourcesPath, "positron-backend"));
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
fs.rmSync(path.join(resourcesPath, "icon.ico"), { force: true });
|
|
115
133
|
|
|
134
|
+
if(!process.argv.includes('--keep-package-json') || !process.argv.includes('--kpj')) {
|
|
135
|
+
fs.rmSync(path.join(resourcesPath, "package.json"), { force: true });
|
|
136
|
+
}
|
|
116
137
|
|
|
117
138
|
success(`Successfully packaged macOS app at: ${appBundlePath}`);
|
|
118
139
|
}
|
|
@@ -162,9 +183,10 @@ async function packageWindows(appRoot, distDir, appName) {
|
|
|
162
183
|
}
|
|
163
184
|
|
|
164
185
|
const bundledJs = path.join(resourcesPath, "index.js");
|
|
186
|
+
const backendName = appName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[\s_]+/g, '-').toLowerCase() + '-backend';
|
|
165
187
|
|
|
166
188
|
if (fs.existsSync(bundledJs)) {
|
|
167
|
-
await compileWithPkg(bundledJs, "win32", resourcesPath,
|
|
189
|
+
await compileWithPkg(bundledJs, "win32", resourcesPath, backendName);
|
|
168
190
|
} else {
|
|
169
191
|
error(`[Packager] Fatal: Bundled JavaScript entry point missing at ${bundledJs}`);
|
|
170
192
|
process.exit(1);
|
|
@@ -173,6 +195,10 @@ async function packageWindows(appRoot, distDir, appName) {
|
|
|
173
195
|
fs.rmSync(path.join(resourcesPath, "icon.icns"), { force: true });
|
|
174
196
|
fs.rmSync(path.join(resourcesPath, "icon.ico"), { force: true });
|
|
175
197
|
|
|
198
|
+
if(!process.argv.includes('--keep-package-json') || !process.argv.includes('--kpj')) {
|
|
199
|
+
fs.rmSync(path.join(resourcesPath, "package.json"), { force: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
176
202
|
const macBinaryPath = path.join(outputFolder, "positron-runtime");
|
|
177
203
|
if (fs.existsSync(macBinaryPath)) {
|
|
178
204
|
fs.rmSync(macBinaryPath);
|
|
@@ -189,7 +215,7 @@ async function packageWindows(appRoot, distDir, appName) {
|
|
|
189
215
|
success(`Successfully packaged Windows app directory at: ${outputFolder}`);
|
|
190
216
|
}
|
|
191
217
|
|
|
192
|
-
function copyAppAssets(src, dest) {
|
|
218
|
+
function copyAppAssets(src, dest, ignoredFiles = []) {
|
|
193
219
|
const ignoreList = ["node_modules", "dist", "bin", ".git"];
|
|
194
220
|
|
|
195
221
|
function copyRecursive(currentSrc, currentDest) {
|
|
@@ -198,6 +224,8 @@ function copyAppAssets(src, dest) {
|
|
|
198
224
|
if (ignoreList.includes(item)) continue;
|
|
199
225
|
|
|
200
226
|
const srcPath = path.join(currentSrc, item);
|
|
227
|
+
if (ignoredFiles.includes(srcPath)) continue;
|
|
228
|
+
|
|
201
229
|
const destPath = path.join(currentDest, item);
|
|
202
230
|
const stat = fs.statSync(srcPath);
|
|
203
231
|
|
|
@@ -209,7 +237,6 @@ function copyAppAssets(src, dest) {
|
|
|
209
237
|
fs.mkdirSync(destPath, { recursive: true });
|
|
210
238
|
copyRecursive(srcPath, destPath);
|
|
211
239
|
} else {
|
|
212
|
-
if (item.endsWith(".js")) continue;
|
|
213
240
|
fs.copyFileSync(srcPath, destPath);
|
|
214
241
|
}
|
|
215
242
|
}
|
|
@@ -221,6 +248,14 @@ function copyAppAssets(src, dest) {
|
|
|
221
248
|
const { exec } = require("@yao-pkg/pkg");
|
|
222
249
|
|
|
223
250
|
async function compileWithPkg(bundledJsPath, targetPlatform, outputFolder, appName) {
|
|
251
|
+
|
|
252
|
+
if(process.argv.includes("--no-pkg")) {
|
|
253
|
+
const finalPath = path.join(outputFolder, "index.js");
|
|
254
|
+
fs.copyFileSync(bundledJsPath, finalPath);
|
|
255
|
+
success(`[Packager] Skipped pkg compilation. Copied bundled JavaScript to: ${finalPath}`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
224
259
|
info(`[Packager] Packaging application into a standalone binary...`);
|
|
225
260
|
|
|
226
261
|
let pkgTarget = "";
|
package/tray.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { app } = require("./index");
|
|
2
|
+
const { Menu } = require("./menu");
|
|
3
|
+
|
|
4
|
+
let createdTray = false;
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
|
|
8
|
+
create(menu, title = "", icon = "") {
|
|
9
|
+
|
|
10
|
+
if(createdTray) {
|
|
11
|
+
console.warn("Tray already created. Use setMenu, setTitle, or setIcon to update the existing tray.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createdTray = true;
|
|
16
|
+
|
|
17
|
+
if(menu instanceof Menu) {
|
|
18
|
+
menu = menu.template
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const stripClick = (items) => {
|
|
22
|
+
if (!items) return null;
|
|
23
|
+
return items.map(i => {
|
|
24
|
+
const newItem = { ...i, click: undefined };
|
|
25
|
+
if (newItem.items) {
|
|
26
|
+
newItem.items = stripClick(newItem.items);
|
|
27
|
+
}
|
|
28
|
+
return newItem;
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
app.setTrayMenu(menu);
|
|
33
|
+
app.sendToNative("createTray", [JSON.stringify(stripClick(menu)), title, icon]);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
setMenu(menu) {
|
|
37
|
+
if(menu instanceof Menu) {
|
|
38
|
+
menu = menu.template
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const stripClick = (items) => {
|
|
42
|
+
if (!items) return null;
|
|
43
|
+
return items.map(i => {
|
|
44
|
+
const newItem = { ...i, click: undefined };
|
|
45
|
+
if (newItem.items) {
|
|
46
|
+
newItem.items = stripClick(newItem.items);
|
|
47
|
+
}
|
|
48
|
+
return newItem;
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
app.setTrayMenu(menu);
|
|
53
|
+
app.sendToNative("createTray", [JSON.stringify(stripClick(menu)), "setMenu"]);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
setTitle(title) {
|
|
57
|
+
app.sendToNative("createTray", [title, "setTitle"]);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
setIcon(iconPath) {
|
|
61
|
+
app.sendToNative("createTray", [iconPath, "setIcon"]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
}
|