Issues with importing complex modules

Is there any undocumented complexities that anyone else has run into when using the importModule command with complex modules?

I have noticed that if there is any code that would be executed when a script is ran, aka. any code that is not in a function, that importModule will fail with the cryptic error of TypeError: undefined is not an object

I additionally had bugs with a function that would cascade through its promise chain without properly awaiting previous statements, but then would completely error out with the same type error if a catch statement was added to the end of the promise chain in an attempt to see what was happening.

I am currently attempting to make a very complex tool for interacting with the api for a ticketing tool that I use, and would love to be able to break my code into 5 or six different source files, both for readability as well as for reusability, as many of the api calls have highly redundant parts.

Im very tempted to categorize this as a bug, as the error that appears is opaque and difficult to understand.

Any assistance with this would be greatly appreciated.

The module documentation only describes examples around exports. If you have non-function based code to be executed, it isn’t explicitly described in the documentation as supporting that. Further, and somewhat more difficult to track down, Simon also discussed the general intent for the module import functionality in this thread, where the discussion indicates that the functionality is currently only intended for use with Scriptable specific libraries that you construct yourself.

It sounds like you might be attempting to utilise the functionality beyond what’s described in the above and that may be the source of your issues.

I’m not a trained JavaScript programmer (self-taught many moons ago against version 1.1 or 1.2 I think), so I may be missing something here, but from a more general programming point of view, I wouldn’t normally expect anything other than functions (and structures like classes, etc.) to be included in a library. I would expect initialisation calls to be made to execute set-up code, only as and when the programmer requires, nor immediately on loading the library as timing, ordering and inter-dependencies can be critical considerations.

Perhaps in this regard, you just need to look at refactoring some of the library/module functionality to “play better with Scriptable”?

With regards to promises, well they can be tricky (I dread to think how many times I’ve hat to debug function calls with promises in them), but overall, the chains of promises should be respected and should work. I would suggest stripping things down to their simplest case and building it up to the point where you either have a promise-based structuring that works, or you have a clear example that @simonbs could use to reproduce, and then investigate the issue.

I think that my issue was actually a combination of a few different issues that were not clear at first.

Your suggestion to refactor really did help me with realizing how stuck in my was I was. Having most of my experience in python I was treating importModule as equivalent to import and that was tripping me up a whole bunch.

I seem to be suffering from the same issue, or lack of JavaScript knowledge in my case…

I have a file that defines a class RTM (an implementation of the Remember The Milk API) that I want to export, so that I can import it in other scripts:

class RTM {
    constructor() {
        this.token = ...
        ...
    }

    async call(method, params, options) {
        ...
    }

    addTask(task) {
        return this.call(...)
    }
}

(How) Can this be done? What modules.exports syntax should I use?

Whatever I try I end up with TypeError: undefined is not an object…

If the class is everything you want to export from that file, then

module.exports = RTM;

should do it. Import it with

const RTM = importModule("rtm.js");
2 Likes

I really thought I tried that yesterday, but apparently I did not…

In the script that uses my class I next need to do this? (as I did before)

const rtm = new RTM()

And then I can use it like this?

rtm.addTask(...)
1 Like

Yes, that’s correct!

While I’m learning about this:

What should I do if I want to export the method addTask, but not the (“internal”) method call?

(Is that possible?)

Then you’d have to either create an instance of the class and export its method addTask, but bound to the instance like this:

let rtm = new RTM();
module.exports = rtm.addTask.bind(rtm);

You have to bind the function to the class instance, because javascript forgets, where the method came from:

class Foo {
  constructor() {}
  bar() {
    return this;
  }
}

let foo = new Foo();
foo.bar() === foo // true

let bar = foo.bar;
bar() === foo // false
typeof bar() == "undefined" // true

bar = foo.bar.bind(foo);
bar() === foo // true

// this is equivalent to what .bind(foo) returns:
bar = function() {
  return bar.call(foo);
};
bar() === foo // true

Or declare the method addTask as static:

class Foo {
  constructor() {}
  static bar() {
    return "Hello world!";
  }
}

module.exports = Foo.bar;

Note that you don’t need to bind the method this time, as a static method should never expect this to be something.

If you want to export more than one object/class/function/primitive (= string, number,…), then you will need to use

module.exports = {
  foo: foo,
  bar: bar,
 ...
};
1 Like

Thank you!

I’m afraid there’s (at least) one part left that I don’t understand yet.

I can’t use the static approach. How woud I export/expose two methods of a class?

Suppose in your example Foo has three methods; bar0, bar1 and bar2.
If I want to expose (only) bar1 and bar2, how would I do that?

EDIT: Maybe I’m confused by foo: bar in the last example, if that was bar: bar I’d probably understand:

module.exports = {
  bar1: bar1,
  bar2: bar2,
}

(can’t experiment right now)

Yes that’s right. It is a completely new example and has nothing to do with the others. I’ve changed it.

Well then it would be something like this:

class Foo {
  constructor() {}
  bar0() {
    return this;
  }
  bar1() {
    return this;
  }
  bar2() {
    return this;
  }
}

let foo = new Foo();
module.exports = {
  bar1: foo.bar1.bind(foo),
  bar2: foo.bar2.bind(foo)
};
2 Likes

Got it working! Thank you for your help.

2 Likes