jxa-comparison.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. #!/usr/bin/env npx ts-node
  2. /**
  3. * JXA vs AppleScript Comparison Script
  4. *
  5. * This script runs identical operations using both AppleScript and JXA
  6. * to compare behavior, performance, and output format.
  7. *
  8. * Run with: npx tsx scripts/jxa-comparison.ts
  9. */
  10. import { execSync } from "child_process";
  11. interface TestResult {
  12. name: string;
  13. applescript: { success: boolean; output: string; time: number; error?: string };
  14. jxa: { success: boolean; output: string; time: number; error?: string };
  15. comparison: string;
  16. }
  17. function runAppleScript(script: string): {
  18. success: boolean;
  19. output: string;
  20. time: number;
  21. error?: string;
  22. } {
  23. const escaped = script.trim().replace(/'/g, "'\\''");
  24. const start = Date.now();
  25. try {
  26. const output = execSync(`osascript -e '${escaped}'`, { encoding: "utf8", timeout: 30000 });
  27. return { success: true, output: output.trim(), time: Date.now() - start };
  28. } catch (e) {
  29. return { success: false, output: "", time: Date.now() - start, error: (e as Error).message };
  30. }
  31. }
  32. function runJXA(script: string): {
  33. success: boolean;
  34. output: string;
  35. time: number;
  36. error?: string;
  37. } {
  38. const escaped = script.trim().replace(/'/g, "'\\''");
  39. const start = Date.now();
  40. try {
  41. const output = execSync(`osascript -l JavaScript -e '${escaped}'`, {
  42. encoding: "utf8",
  43. timeout: 30000,
  44. });
  45. return { success: true, output: output.trim(), time: Date.now() - start };
  46. } catch (e) {
  47. return { success: false, output: "", time: Date.now() - start, error: (e as Error).message };
  48. }
  49. }
  50. const tests: Array<{ name: string; applescript: string; jxa: string }> = [
  51. {
  52. name: "List Accounts",
  53. applescript: `tell application "Notes" to get name of every account`,
  54. jxa: `
  55. const Notes = Application("Notes");
  56. Notes.accounts().map(a => a.name()).join(", ");
  57. `,
  58. },
  59. {
  60. name: "List Folders (iCloud)",
  61. applescript: `tell application "Notes" to tell account "iCloud" to get name of every folder`,
  62. jxa: `
  63. const Notes = Application("Notes");
  64. const iCloud = Notes.accounts.byName("iCloud");
  65. iCloud.folders().map(f => f.name()).join(", ");
  66. `,
  67. },
  68. {
  69. name: "Count Notes",
  70. applescript: `tell application "Notes" to count notes`,
  71. jxa: `
  72. const Notes = Application("Notes");
  73. Notes.notes().length;
  74. `,
  75. },
  76. {
  77. name: "Get First Note Title",
  78. applescript: `tell application "Notes" to get name of first note`,
  79. jxa: `
  80. const Notes = Application("Notes");
  81. Notes.notes()[0].name();
  82. `,
  83. },
  84. {
  85. name: "Get First Note Creation Date",
  86. applescript: `tell application "Notes" to get creation date of first note`,
  87. jxa: `
  88. const Notes = Application("Notes");
  89. Notes.notes()[0].creationDate().toISOString();
  90. `,
  91. },
  92. {
  93. name: "Unicode Test - Get Note with Emoji (if exists)",
  94. applescript: `
  95. tell application "Notes"
  96. set noteList to notes whose name contains "🎉"
  97. if (count of noteList) > 0 then
  98. return name of first item of noteList
  99. else
  100. return "No emoji notes found"
  101. end if
  102. end tell
  103. `,
  104. jxa: `
  105. const Notes = Application("Notes");
  106. const emojiNotes = Notes.notes().filter(n => n.name().includes("🎉"));
  107. emojiNotes.length > 0 ? emojiNotes[0].name() : "No emoji notes found";
  108. `,
  109. },
  110. {
  111. name: "Search Notes",
  112. applescript: `
  113. tell application "Notes"
  114. set matchingNotes to notes whose name contains "test"
  115. return count of matchingNotes
  116. end tell
  117. `,
  118. jxa: `
  119. const Notes = Application("Notes");
  120. Notes.notes().filter(n => n.name().toLowerCase().includes("test")).length;
  121. `,
  122. },
  123. {
  124. name: "Error Handling - Non-existent Note",
  125. applescript: `tell application "Notes" to get note "ThisNoteShouldNotExist12345"`,
  126. jxa: `
  127. const Notes = Application("Notes");
  128. Notes.notes.byName("ThisNoteShouldNotExist12345").name();
  129. `,
  130. },
  131. ];
  132. console.log("=".repeat(80));
  133. console.log("JXA vs AppleScript Comparison");
  134. console.log("=".repeat(80));
  135. console.log("");
  136. const results: TestResult[] = [];
  137. for (const test of tests) {
  138. console.log(`\n📋 ${test.name}`);
  139. console.log("-".repeat(40));
  140. const asResult = runAppleScript(test.applescript);
  141. const jxaResult = runJXA(test.jxa);
  142. let comparison: string;
  143. if (asResult.success === jxaResult.success) {
  144. if (asResult.output === jxaResult.output) {
  145. comparison = "✅ Identical output";
  146. } else {
  147. comparison = "⚠️ Different format (both succeeded)";
  148. }
  149. } else {
  150. comparison = "❌ Different success status";
  151. }
  152. console.log(`AppleScript: ${asResult.success ? "✓" : "✗"} (${asResult.time}ms)`);
  153. if (asResult.success) {
  154. console.log(
  155. ` Output: ${asResult.output.substring(0, 100)}${asResult.output.length > 100 ? "..." : ""}`
  156. );
  157. } else {
  158. console.log(` Error: ${asResult.error?.substring(0, 100)}`);
  159. }
  160. console.log(`JXA: ${jxaResult.success ? "✓" : "✗"} (${jxaResult.time}ms)`);
  161. if (jxaResult.success) {
  162. console.log(
  163. ` Output: ${jxaResult.output.substring(0, 100)}${jxaResult.output.length > 100 ? "..." : ""}`
  164. );
  165. } else {
  166. console.log(` Error: ${jxaResult.error?.substring(0, 100)}`);
  167. }
  168. console.log(`Comparison: ${comparison}`);
  169. results.push({ name: test.name, applescript: asResult, jxa: jxaResult, comparison });
  170. }
  171. // Performance summary
  172. console.log("\n" + "=".repeat(80));
  173. console.log("Performance Summary");
  174. console.log("=".repeat(80));
  175. const successfulTests = results.filter((r) => r.applescript.success && r.jxa.success);
  176. if (successfulTests.length > 0) {
  177. const asTotal = successfulTests.reduce((sum, r) => sum + r.applescript.time, 0);
  178. const jxaTotal = successfulTests.reduce((sum, r) => sum + r.jxa.time, 0);
  179. console.log(`\nSuccessful tests: ${successfulTests.length}`);
  180. console.log(
  181. `AppleScript total time: ${asTotal}ms (avg: ${Math.round(asTotal / successfulTests.length)}ms)`
  182. );
  183. console.log(
  184. `JXA total time: ${jxaTotal}ms (avg: ${Math.round(jxaTotal / successfulTests.length)}ms)`
  185. );
  186. console.log(
  187. `\nPerformance ratio: JXA is ${(asTotal / jxaTotal).toFixed(2)}x ${jxaTotal < asTotal ? "faster" : "slower"} than AppleScript`
  188. );
  189. }
  190. console.log("\n" + "=".repeat(80));
  191. console.log("Key Findings");
  192. console.log("=".repeat(80));
  193. console.log(`
  194. 1. Date Format:
  195. - AppleScript: "date Saturday, December 27, 2025 at 3:44:02 PM" (locale-dependent)
  196. - JXA: ISO format when using .toISOString() (consistent, parseable)
  197. 2. Array Output:
  198. - AppleScript: Comma-separated string "item1, item2, item3"
  199. - JXA: Native JavaScript arrays, can be formatted as needed
  200. 3. Error Messages:
  201. - Both provide similar error messages from OSA framework
  202. 4. Unicode:
  203. - Both should handle Unicode equally (same underlying framework)
  204. 5. String Escaping:
  205. - AppleScript: Complex escaping for shell + AppleScript + HTML
  206. - JXA: Standard JavaScript escaping (simpler)
  207. `);