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/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
- const menuAction = findMenuAction((msg.event === "menu-action" ? currMenu : contextMenu), msg.data.label, msg.data.channel);
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}`, path);
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}`, url);
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. Channel: ${channel}`);
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", `isVisible-reply-${this.id}`);
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", `isFullscreen-reply-${this.id}`);
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", `canGoBack-reply-${this.id}`);
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
- * Sends a request/response command to the native layer. The command will be sent, and the method will wait for a response on the specified reply channel. Once a response is received, the promise will resolve with the reply data.
621
- * @param {string} command The command to send.
622
- * @param {string} replyChannel The channel to listen for the reply on.
623
- * @returns {Promise<*>} A promise that resolves to the reply data.
635
+ * @overload
636
+ * @param {string} command
637
+ * @param {...any} args
638
+ * @returns {Promise<any>}
624
639
  */
625
- async request(command, replyChannel, ...args) {
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
- if(!replyChannel) {
635
- replyChannel = `${command}-reply-${this.id}`;
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(!args.includes("NO_TIMEOUT")) {
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
- timeoutDuration = parseInt(timeoutArg.split("=")[1]);
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", `canGoForward-reply-${this.id}`);
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", `prompt-reply-${this.id}`, message, defaultValue);
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", `isFocused-reply-${this.id}`);
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", `getBounds-reply-${this.id}`);
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", `getURL-reply-${this.id}`))?.url || "";
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", `getTitle-reply-${this.id}`)).title || "";
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", `evaluateJS-reply-${this.id}`, script);
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", `confirm-reply-${this.id}`, message);
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", `setSwipeNav-reply-${this.id}`, String(enabled));
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", `isSwipeNavEnabled-reply-${this.id}`);
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", `addToContentBlocker-reply-${this.id}`, json, config.reload, config.clearExisting);
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 results = await Promise.all([...activeWindows].map(win => win.isFocused().then(isFocused => ({ win, isFocused }))));
1115
- return results.find(({ isFocused }) => isFocused)?.win || null;
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.4",
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: ob,
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
- fs.copyFileSync(compiledBinary, path.join(macosPath, appName));
74
- fs.chmodSync(path.join(macosPath, appName), "755");
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, "positron-backend");
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, "positron-backend");
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
+ }