Import one script from another?

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

eval(fmLocal.read(libraryPath));

should be changed to explicitly be

eval(fmLocal.readString(libraryPath));

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')
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

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!

CAVEAT: the following is just a technical tidbit without practical usage. For a module importing system that actually works, see my require() code above.

For those among you interested in the wider context, the whole thing may end up a non-issue in the foreseeable future: the dynamic import() spec is currently in stage 3 of the TC39 process, meaning it’s pretty much slated for inclusion in the JS language spec. In fact, import() has been in Chrome since build 64 and, more to the point, in WebKit and hence JavaScript Core, since January of, hold onto your hats, 2017.

However, as of today, the functionality is only active inside the JSC Shell and the WebKit inspector. You can do

let module = await import('./foo')

today in Scriptable, but all that nets you is a failed Promise. Let’s hope that changes soon-ish.

2 Likes

@kopischke Thanks for sharing your thoughts about this. I expect to add support for importing other scripts in a future update and I think your implementation and the thoughts surrounding it is a great inspiration.

2 Likes