Import one script from another?

Just wondering if there’s a way to import one script from another?

I’m likely to want to write some functions that I re-use in several scripts.

Thanks

4 Likes

Hi,

There’s not a way to import a script into another script yet. It’s something I’d like to do but haven’t gotten around to yet.

4 Likes

Here’s an example of how you can set-up a library of Javascript functions for use in Scriptable

Add it to a script entry and run it. It will create a new script file in Scriptable. This new file is the library file and is the same as any other file you might create in Scriptable. It is created with a couple of functions in it. The creation of this file is purely for the purposes of the example. You would typically create this library file yourself in Scriptable.

Once created, the contents of the library file are read in and evaluated. This effectively makes the functions accessible to the current script.

The script then finally outputs some examples to the Scriptable console using those functions from the library.

//This constant is the name of your library file
//It will appear as an entry in Scriptable
const libraryName = "test-lib";

//These constants define where to place your library file
const scriptableFilesPath = "/var/mobile/Library/Mobile Documents/iCloud~dk~simonbs~Scriptable/Documents/";
const libraryPath = `${scriptableFilesPath}${libraryName}.js`;


//Set-up a local file manager object instance
let fmLocal = FileManager.local();

//---
//The content in this section writes a library file
//Typically you would create & maintain the library in Scriptable
//We're only doing it here for the sake of example.

//This next constant defines two functions - square and cube
//This is the content that will be written to the file
const libraryContent = "var square = x => x * x;\nvar cube = x => x * x * x;";
//Create the library file with the content we defined above
let result = fmLocal.writeString(libraryPath, libraryContent);
//---

//Read the content of the library file and interpret it for execution
eval(fmLocal.read(libraryPath));

//---
//Again this section is just for the puposes of the example
//It uses the square and cube functions we wrote out to the library

//Output some values to Scrciptable's console using library functions
console.log(square(6));
console.log(cube(6));
console.log(square(cube(6)));

//---

Here’s some screenshots…

Initial script list

Script and output when run

Resulting script list

Content of additional (library) script

Hope that helps.

10 Likes

@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

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).
6 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: