Import one script from another?

Hi folks, having delved into the rabbit hole that is home rolled import mechanisms before (in JavaScript for Automation, aka JXA), I’d thought I would share an improved version of @sylumer’s solution. This is basically a poor man’s version of the CommonJS module system (which the NodeJS module logic built and expanded on).

You will need this snippet of code in your script:

function require (path) {
  try { var fm = FileManager.iCloud() } catch (e) { var fm = FileManager.local() }
  let code = fm.readString(fm.joinPath(fm.documentsDirectory(), path))
  if (code == null) throw new Error(`Module '${path}' not found.`)
  return Function(`${code}; return exports`)()
}

Now, given a module script test-module that declares a top level variable exports like this:

const exports = {
  foo: function () { console.log('foo') },
  bar: 123,
  Baz: class Baz { 
    constructor (qux) {
      this.out = () => { console.log(qux) }
    }
  }
}

you can do:

const mod = require('test-module.js')
mod.foo()                // logs 'foo'
console.log(mod.bar)     // logs 123
new mod.Baz('qux').out() // logs 'qux'

Even better, you can use JavaScript’s object destructuring syntax to only import selected parts of the module into your script scope. For instance, if you only need the Baz class, do:

const {Baz} = require('test-module.js')
new Baz('qux').out()     // logs 'qux'

What are the advantages of this over the original solution? Well:

  1. Function is faster and safer than eval() – in fact, it is the recommended alternative to eval() on MDN;
  2. the defined module structure allows for exporting constant literals and classes defined via the class keyword, which is not possible with eval(), and it allows for partial imports (see above);
  3. error handling and proper fallback to local storage if iCloud is not active (assuming that Scriptable does the same; I haven’t checked).
8 Likes