Using JavaScript for Automation from AppleScript (and vice versa)

JavaScript has various standard libraries which AppleScript lacks (URL encode-decode, and trigonometic functions come to mind for example).

A common AppleScript solution is to use do shell script to get these kinds of missing functionality, and shelling out could even include a call to "osascript -l JavaScript -e strCode"

Here, however, is an AppleScript function

evalOSA() 

which allows you to evaluate JavaScript code (from an AppleScript) a little more directly.

( A JavaScript version of evalOSA() (for evaluating AS or JS) follows further below )

Turns out be significantly faster than calling to osascript through the shell, both from AS and from JS.

Examples: call JS from AS and vice versa.kmmacros (21.2 KB)

AppleScript source:

use framework "OSAKit"

on run
    set strEncoded to ¬
        evalOSA("JavaScript", "encodeURIComponent('Read Me First.txt')")
    
end run

-- evalOSA :: ("JavaScript" | "AppleScript") -> String -> String
on evalOSA(strLang, strCode)
    
    set ca to current application
    set oScript to ca's OSAScript's alloc's initWithSource:strCode ¬
        |language|:(ca's OSALanguage's languageForName:(strLang))
    
    set {blnCompiled, oError} to oScript's compileAndReturnError:(reference)
    
    if blnCompiled then
        set {oDesc, oError} to oScript's executeAndReturnError:(reference)
        if (oError is missing value) then return oDesc's stringValue as text
    end if
    
    return oError's NSLocalizedDescription as text
end evalOSA

JavaScript Source:

function run() {
    ObjC.import('OSAKit');
    
    return ['JS', 'AS']
        .map(function (strLang) {
            return evalOSA(strLang, '2 + 2');
        });
}

function evalOSA(strLang, strCode) {

    var strIdiom = strLang.toLowerCase()
        .indexOf('j') !== -1 ? (
            'JavaScript'
        ) : 'AppleScript',
        error = $(),
        oScript = (
            $.OSALanguage.setDefaultLanguage(
                $.OSALanguage.languageForName(strIdiom)
            ),
            $.OSAScript.alloc.initWithSource(strCode)
        ),
        blnCompiled = oScript.compileAndReturnError(error),

        oDesc = blnCompiled ? (
            oScript.executeAndReturnError(error)
        ) : undefined;

    return oDesc ? (

        oDesc.stringValue.js

    ) : error.js.NSLocalizedDescription.js;

}
3 Likes

OK, I don’t have a lot of experience in the Mac world. But I know that AS does a lot of run-time stuff, and I could easily imagine that your AS code to run JS could run slower than shelling out.

Now that I say this, don’t you think so? I mean, AS does a lot of interpreting when it runs…

I guess it depends on whether we expect the higher overhead to come from invoking a shell to run osascript, or using an ObjC bridge to do the same.

The latter is a generous order of magnitude faster in the JS case …

In the AS version, one could drop the initial compile test, though that is useful for the error messages it can yield …

Wish there was some way to to do profiling on something like this. Along with being able to see exactly what the bridge does during these steps. But I suspect that’s not possible,

It’s the same with that Swift script of mine. Including one additional library makes a HUGE difference - orders of magnitudes difference. WTH is it doing? Argh! I want to know!!

…OK, I’m better now…

1 Like

Wish there was some way to to do profiling on something like this

Well, with a slightly simpler check I realise now that in AppleScript the OSAKit call is, in fact, already significantly faster than the 'do shell script' version.

(Both are a fair bit faster from JavaScript, but of course there's a history of competitive 'browser-war' optimisation there)

Well then. So glad I helped. :stuck_out_tongue_winking_eye:

1 Like

Very, very nice, Rob. :thumbsup: Thanks for sharing.

I've already added your evalOSA() function to my script library.

1 Like

To access an AppleScript script from JXA, I prefer to just use an AppleScript Script Library directly:

JXA Script

var app = Application.currentApplication()
app.includeStandardAdditions = true

//--- SET REF TO MY APPLESCRIPT LIB ---
var jmtxASLib = Library("JMTX TEST Lib AS")

//--- CALL FUNCTION IN MY APPLESCRIPT LIB ---
jmtxASLib.getLibInfo("Hello from JXA")

Results:

Libraries are fine.

Main overhead is installation burden in the context of sharing.

(Less significantly, I notice that there can be a slight lag when the library file is first loaded)

In this use case, they are preferred.

I don't see any need to call AppleScript from JXA just to access native AppleScript functions. The far more common use case is when you already have a complex function/script written in AppleScript that you don't want to convert to JXA. This is almost always functions in my established AppleScript Library.

Trivial. Just save the Script to the ~/Library/Script Libraries folder.

Very insignificant. I never notice the lag, and I use script libraries all the time.

are preferred

The passive voice elides the actor. Not quite sure who is meant to be doing the preferring there.

( Certainly not me :slight_smile: )

I don't see any need to call AppleScript from JXA just to access native AppleScript functions

No, that's not the use case I had in mind. Calling JavaScript functions from AppleScript may make some sense if you need trigonometric etc functions.

I have actually called AS from JS once, in the context of a graphics app where it was useful to be able to set a value across a collection with one AE event – a blind patch in the current JS Automation library.

Trivial. Just save

It's an overhead - how significant will depend on use cases, users, and numbers of library files needed, but dependency-tracking is a classic source of unexpected error.

Mileage varies, but I personally try to avoid dependencies if I can.

Or regular expressions, right?

1 Like

I think you just made my point: There is little need to call AppleScript from JXA, UNLESS you want to take advantage of an existing AppleScript library.

I think you are making a mountain out of a molehill. LOL

Saving a script file to a specific folder really is trivial.

IMO, the users likely to be using/needing AppleScript from JXA are most likely the more experienced users, who will not have a problem with this.

Just to be clear, I do like your AppleScript evalOSA() function, to call JXA from AppleScript. Just don't see the need to call from JXA.

the need to call from JXA.

I think I explained the general case of batch-setting properties across collections, which doesn't yet work in the JavaScript Automation library.

In AS, for example, you might write:

tell application "OmniGraffle"
    tell canvas of front window
        tell front layer
            set (stroke color of lines) to {0, 0, 1}
        end tell
    end tell
end tell

but in these early builds of the JSA libraries, .getProperty() works across collections of elements, but .setProperty() only works in individual elements, so we could call AS from JS to set the color of all lines to blue in a single Apple Event.

Another general context is that subtle elements of non-compliance in the automation interfaces of pre-JSA apps which didn't generate obvious bugs under AS, can sometimes be exposed as bugs by using them from JS, mainly because of unnoticed dependencies on inexplicit datatype coercion.

So for example, a few months back, I found myself writing something which I would now do with evalOSA, in lieu of the slower shell call:

// FALL BACK TO APPLESCRIPT KLUDGE EMBEDDED IN JS TO CREATE PARENT-CHILD LINKS BETWEEN SHAPES
// (The JavaScript interface to doing this appears to be broken at the moment)
sa.doShellScript(
    "osascript <<AS_END 2>/dev/null\ntell application \"OmniGraffle\" to tell front canvas of front document\n" +
    "set recP to {line type: straight}\n" +
    lstLinks.map(function (pair) {
        return 'connect shape id ' + pair[0] +
            ' to shape id ' + pair[1] //+
            //' with properties recP';
    })
    .join('\n') + "\nend tell\nAS_END\n"
);

On libraries, you feel a need to campaign or proselytise ?

I think you can probably afford to relax about the fact that needs and approaches vary.

I am only making technical points. I have no idea what you are doing. Perhaps 1% will benefit from your esoteric comments.

I am only making technical points.

Excellent.

So no more capital-shouting,
and no more representation of opinions as facts ?

@JMichaelTX, @ComplexPoint - [In my best Dad voice] Enough already! Or I’ll send both of you to your rooms without supper. Shame on you two - you should know better. And I don’t care who started it!

4 Likes

Hey Dad, can I borrow your car keys for tonight?

I have a really hot date, and I’d love to take her out in your hot Mustang Cobra.

LOL
<gdr,vvf>

No, this was my sports car, a few years ago - well not this exact one, but one that looked just like it:

1 Like