Sketch JS API foreignObject setSourceLibraryName throwing an error

Hello,

So I`ve been trying to solve an issue for couple of days since the last sketch update. I have two libraries that share the same components but different styling. What I had was a plugin that run over a sketch file and a library and updates every symbol of the sketch file with the passed library. So what I do is:

var library = the new passed library
 var importedSymbols = document.getSymbols()
importedSymbols.forEach(symbol => {            
              try {
                var symbolLib = symbol.getLibrary();
                if (symbolLib && symbolLib.id && symbolLib.id !== library.id && symbolLib.name) {
                  symbol.sketchObject.foreignObject().setLibraryID(library.id);
                   symbol.sketchObject.foreignObject().setSourceLibraryName(library.name)
                }
              } catch (error) {
                console.log(error);
              }             
              
          });

and then I use

    symbol.syncWithLibrary();

to update all the symbols with the new library.
This was working so far but seems like there is an error thrown now:
TypeError: null is not an object (evaluating ‘symbol.sketchObject.foreignObject().setSourceLibraryName’)

Seems like the API has changed a bit and I cannot figure it out.
WIll appreciate any help

Hi Martin.
I am unfortunately not able to assist you with your specific plugin issue, but from the sounds of what this plugin does, you might be interested in trying out the new Replace Library feature in the latest Sketch update. This might do what you need, and what your plugin did before.

Hello Brett,
Thanks for your response. Is there a API that is exposed so that this feature could be used progamatically by any chance? I use this plugin of mine in a CI with the Sketch Command-line interface

Hey Martin,

Looking at the error message you posted, it’s not really the setSourceLibraryName() method itself that fails, but foreignObject() returning null (which it shouldn’t I guess, since symbol.getLibrary() is non-null).

Anyway, I’d start debugging by logging both symbol.sketchObject and symbol.sketchObject.foreignObject() for every symbol to find the faulty ones – and then see if they look alright in the document/library.

2 Likes

Thank you, as always, for your reply Dmitry! :pray:

1 Like

Hey @rodionovd,
Thank you for your suggestion.
Seems like it is not a problem with faulty symbols because I tried to use that plugin with a newly created sketch design with just one component. It still fails to update.
However, I found out that before using symbol.sketchObject.foreignObject().setLibraryID(library.id); the foreignObject() returns a valid value. After that it becomes null for some reason. Seems like the setLibraryID method detaches it somehow. So I modified the code like this:

var library = the new passed library
 var importedSymbols = document.getSymbols()
importedSymbols.forEach(symbol => {            
              try {
                var symbolLib = symbol.getLibrary();
                if (symbolLib && symbolLib.id && symbolLib.id !== library.id && symbolLib.name) {
                  var foreignObject = symbol.sketchObject.foreignObject();
                  foreignObject.setLibraryID(library.id);
                  foreignObject.setSourceLibraryName(library.name)
                }
              } catch (error) {
                console.log(error);
              }             
              
          });

and this does not throw the error. However, the component is still not updated for some reason. Probably syncWithLibrary is not working correct now because if I open the sketch design and look into the component’s library, it is the new one. However it didnt update the component. Also, found out that if I run the plugin twice which calls syncWithLibrary again with the new library (I do not set libraryID and sourceLibraryName if they are the same with the current library that symbol has, just pure syncWithLibrary call) the component is updated. Seems really weird to me because this was working just fine a week ago and I havent made any changes neither to the plugin nor to the libraries.

Ay!

So it took me a while to find a minimal reproducible scenario that triggers this error, but I think I got it:

  1. Create a “MyLibrary-light.sketch” library with a single symbol named “Item/light”.
  2. Duplicate this library (via File > Duplicate in Sketch) to “MyLibrary-dark.sketch”, rename the symbol to “Item/dark”, make a minor change (color) so it looks different from the light version.
  3. Register both libraries with Sketch.
  4. Create a new “MyDocument.sketch” document, insert an “Item/dark” symbol there.
  5. Close the document and re-open it again.
  6. Run the following script:
const Document = require('sketch/dom').Document
const Library = require('sketch/dom').Library

let document = Document.getSelectedDocument()
let library = Library.getLibraries().find(library => {
  return library.name == "MyLibrary-light"
})

document.getSymbols().forEach(symbol => {
    symbol.sketchObject.foreignObject().setLibraryID(library.id)
    symbol.sketchObject.foreignObject().setSourceLibraryName(library.name)
});

Note that the 5th step with re-opening the document is absolutely necessary here, otherwise you won’t be able to reproduce the problem.

I’ve also uploaded my test libraries and a sample document here for those following along at home:
:point_right: Sketch101_foreignObject_setLibraryID.zip - Google Drive

My theory of what’s happening


(Everything below is a pure speculation as I can only reverse-engineer Sketch so far with my limited skills and time constraints).

What might be happening in this case is an interesting combination of the following Sketch architecture aspects:

  1. For performance and other reasons, Sketch maintains not one but two virtual layer trees per document:

    [
] the double tree structure we use in our model — a mutable tree that lets the UI layer easily make changes, backed by a lockless immutable model that we can safely pass between threads for doing things such as rendering the canvas across multiple threads.
    :writing_hand: Speeding up Symbols: how we improved performance in Sketch 67 · Sketch

    In order to reduce memory footprint and to avoid extra work required to keep these two trees in sync, certain model objects in Sketch exist in a read-only/immutable form by default – while their mutable counterpart is just a lightweight proxy object. Such lightweight objects don’t store any data themselves, but simply proxy certain calls to the read-only model.

    This is until you actually try to modify one of these lightweight objects of course. As soon as you do that, Sketch converts this lightweight model to a full one by copying all of the data from its read-only counterpart and performing all other kinds of heavy object initialization procedures (I guess you can call this approach “copy-on-write”?).

  2. When a library (aka foreign) symbol is inserted into a document for the first time, Sketch creates a hidden copy of this symbol, and starts creating instances of that copy from now on. This, among other things, enables you to decouple your designs from your libraries by having “frozen” versions of your library symbols that are not updated automatically as the libraries evolve.

    This internal frozen symbol copy is hidden from users and only exists deep in Sketch data structures. Sketch is free to update, delete and re-create this copy whenever it pleases: which is not only when e.g. you explicitly apply library changes via UI, but in other cases too – and one of these other cases is converting a lightweight foreign symbol model (the “foreignObject” in your code) to a full-weight one as described above.


Now with all this background theory in place, this is what I believe happens:

The first time you call “setLibraryID()” on a “foreignObject” (which is actually a “foreign symbol”) in your code, Sketch performs some heavy-lifting for it to become a full mutable object instead of a lightweight-one. As part of this initialization it generates a new hidden symbol copy of said library symbol.

As a result, the “symbol” object you’re currently holding a reference to is now obsolete and no longer referenced anywhere in Sketch: it’s been completely replaced by a newly generated copy.

This is why the next time you try to access its “foreignObject” property, it returns “null”, leading to the aforementioned error – it simply doesn’t belong to any library symbol anymore. In other words, this “symbol” object is now stale and should be re-queried from the Sketch API.

Workaround


The only workaround I have is to make Sketch initialize all library symbols and their hidden local copies before doing any thing with those symbols:

// [...]

// WORKAROUND: let Sketch fully initialize all foreign model objects beforehand,
// (re)creating local symbols as needed
document.getSymbols().forEach(symbol => {
  let foreignObject = symbol.sketchObject.foreignObject()
  foreignObject.fireFaultIfNeeded()
  // Alternative: any other no-op assignment like
  // 	foreignObject.setLibraryID(foreignObject.libraryID());
  // will also do. `fireFaultIfNeeded()` is just asking Sketch directly
})

document.getSymbols().forEach(symbol => {
    if (/* need to switch source library */) {
      symbol.sketchObject.foreignObject().setLibraryID(library.id);
      symbol.sketchObject.foreignObject().setSourceLibraryName(library.name);
      symbol.syncWithLibrary();
    }
});

Note that I don’t store the result of document.getSymbols() in a variable because it will become stale after the first forEach loop (since the corresponding Sketch models will be gone) and you’ll have to re-query it.

Hope it helps!

1 Like

Hello @rodionovd again!

Thanks for your research and response again.

So what I read from your reply makes sense to me and seems like that I came upon it like 20 minutes ago. I found another workaround for my scenario because I saw that syncWithLibrary method returns false when trying to sync the newly updated symbol. So I said to myself that probably because the symbols were stored in a variable using the getSymbols method, they might be outdated and somehow detached after setting new library id and name. So what I did is almost the same you proposed in the workaround. Here is the code:

var library = the new passed library
 var importedSymbols = document.getSymbols()
importedSymbols.forEach(symbol => {            
              try {
                var symbolLib = symbol.getLibrary();
                if (symbolLib && symbolLib.id && symbolLib.id !== library.id && symbolLib.name) {
                  var foreignObject = symbol.sketchObject.foreignObject();
                  foreignObject.setLibraryID(library.id);
                  foreignObject.setSourceLibraryName(library.name)
                }
              } catch (error) {
                console.log(error);
              }             
              
        });

// and then sync the new symbols 
var importedSymbols = document.getSymbols();
  importedSymbols.forEach((symbol) => {
    symbol.syncWithLibrary();
  });

And this is working fine for now.
Seems like this is something that changed over the last version of Sketch and I am not entirely sure whether this is an issue or it is supposed to work like that.
So anyway, thanks very much for your time and help.
I am marking your reply as a solution because it is way more comprehensive.

1 Like

Happy to help, Martin!

Yep, your approach of discarding the “symbol” object and working on its “foreignObject” directly also makes sense given that you’re offloading the sync part to a new loop :raised_hands:

2 Likes