Import one script from another?

@sylumer Wow! :open_mouth: That’s a really clever workaround! Thanks for sharing this.

By the way, if you store your library scripts in a nested directory of your iCloud directory, they won’t show up in Scriptable.

This is very cool.

Could a similar technique be used to implement something like Node.js require with namespace support?

As long as appropriate name spacing patterns are applied to what you are importing and you don’t explicitly clash with the name space then I don’t see any reason it wouldn’t.

Just think of the approach as copy and pasting your file content in at that point.

Would it be possible to wrap that up in an const import = import() or #import?

1 Like

Just to note, that with the latest beta, the line


should be changed to explicitly be


This is due to the API changes in build 20. Specifically this one.

I’m pretty sure @simonbs meant to say readString() there and not loadString()copy and paste error from the loadString() function for the Request object perhaps?

1 Like

Oops, yes. That is a copy/paste error :flushed: Thanks for the heads up and sorry for the inconvenience.

1 Like

This might be overkill, but since Scriptable saves all your scripts on iCloud Drive, a much cleaner way to do this is to write your scripts on a mac in multiple files and use a tool like Browserify (or Webpack or Parcel) to combine multiple files into one before syncing them down using iCloud drive.

Of course this does force you to use a mac.

1 Like

I can’t get the app to exclude files in a lib/ subdirectory. I updated to the latest v1.0.1 this morning, but it still traverses the directories and includes my library files. Is there something else I’m supposed to do @simonbs?

The latest beta version is 1.0.2 (not v1.0.1) and it is working for me.

@BoxOfSnoo The note about that bug fix had accidentally snuck into the release notes for 1.0.1 while it’s actually part of 1.0.2. The good news is that Apple already approved 1.0.2 and it’s currently propagating to the App Stores.

That got it! Thanks.

Thanks for sharing this awesome workaround @sylumer! Implementing it in my project now.

Worth noting that only variables defined with var in the import file will be available after import, not const or let.

Very much a library of functions not data :wink:

If you wanted data, I’d wrap them in a function call and maybe even use them to read from a ‘settings’ file depending upon different use cases.

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')                // logs 'foo'
console.log(     // 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).

Thanks for this. May I suggest to modify it a bit to support calling require without the .js extension to make it a bit closer to the CommonJS/NodeJS convention.

@kopischke Ver nice! Thanks for sharing this :raised_hands:

Yeah, I thought about that and my first implementation actually had a very naive stab at it, but I eventually opted out of it. I‘ll explain my thinking:

  1. The semantics of the module path in my hack are fundamentally different from those used by CommonJS / NodeJS. For technical reasons (both iOS sandbox and Scriptable API limitations), the module path is always anchored at Scriptable’s document directory, while the big players anchor it at the call point’s location in the file system. require(‘../lib/module.js’) will not work the way you’d expect it to coming from Node. In such cases, I find it best to not paper over differences by trying to match syntax.
  2. Now you might object that even then, the convenience of not having to type three characters (“.js”) could trump semantics, but consider this:
    • you can’t simply slap on the extension unconditionally because that would just shift the annoyance from always having to type the extension to always having to omit the extension, while also precluding people from using any extension but “.js”;
    • you can’t say “OK, just slap it on if there is no extension already” because there is no easy way to say what actually constitutes an extension if the file name contains a dot anywhere (it’s 2018, even Windows allows extensions of arbitrary length by now, and Apple systems have for ages), which, by the way, they do if you use the Reverse Domain Notation naming scheme common to Apple’s OS. In short, you would end up either dictating a module naming scheme orthogonal to platform conventions (notice anything about the Node module naming conventions?) or having to maintain a list of “allowed alternate extensions” you’d have to update inline in all scripts whenever it changes.

All of this is solvable, but not within six lines of inlined code in a way that scales safely. I’m not opposed to “magic” functionality as such, but in this case, I feel the convenience of not adding the extension to the call is not worth the hassle. Feel free to try to change my mind, of course!

1 Like

I agree, there’s really a lot more to it.
My line of thought is to actually to only replace path with (path.replace(/\.js$/,'') + '.js'). This handles both having/not having the extension and reverse domain name notation.
But yes, this would be limited to only using .js as the extension.

That actually was exactly my first naive implementation … however, as I stated, all this achieves is creating a restriction on file naming, all at the cost of one extra line of code in a snippet that needs to be inlined everywhere. I also happen to think that using the extension more clearly communicates intent, i.e. that you require a file, not some abstract module.

Anyway, lots of words to say I won’t be adding this into my reference snippet. You are of course free to modify it for your own usage!