This is a follow on from the article on Pull libraries inline to your Apps Script project (which you should probably read fiest) to explain a little about how it works.
The Script API
It uses the Script API to get and update Apps Script project content, but the key to making it work is to be able to detect which variables and functions are exposed in the global space of the library being pulled in. They can be similarly exposed in the refactored version pulled into the main project.
Why do we need to know what’s in the library’s global space
A project that uses a library will access variables and functions in that library via libraryusersymbol.variablename and libraryusersymbol.function name. These are exposed via some magic in the Apps Script library system.
When the library is pulled in to the project, we need to still be able to access exactly the same variables and functions in the same way, so we need a away of identifiying them.
The library namespace constructor
Each imported library is wrapped in a namespace. Here’s how a library is wrapped in a namespace when it’s pulled into a project. For the moment it doesn’t really matter what the library is doing. I’ve pulled out all the code except the global space declarations.
First, all the scripts in the library are concatenated, then wrapped in a namespace as above – named after the library name and its version.
Discovering what’s in the library global space via an AST
This creates an Abstract Syntax Tree (Ast), which is a tree representation of all the tokens in the source code.
Getting the Ast body
Next we’ll need to extract the Program body from the AST
Make the exports
Since we are only interested in the global space declarations, we can pick them up from the top branch of the AST body without bothering to traverse the entire tree. Functions and variables are defined slightly differently so we can use the same function to pick up the names in the global space, but with an accessor passed as an argument
We can now create the list of exports like this
Note that inlining will expose a little more than libraries do – libraries only expose variables defined as var and functions. Inlining exposes const,let and var. I have a TODO to add an option to suppress const and let exports to make it completely backwards compatible.
Now the wrapped namespace constructor can return this list as its properties, so the final constructor function looks like this
You’ll notice that I haven’t exposed the classes defined in the library by default. This is because libraries don’t, and I want to replicate (rather than enhance) the library experience. However, with full access to the ast there’s quite a few nice enhancements you could add. If you’ve inlined your libraries, and you want to be able to access the classes in them, just add the exportClasses: true option like this:
When traversing the tree of dependent libraries, it’s possible that some of them reference the same library. There’s no point in creating the same namespace constructors multiple times, so the script ‘_bmlimport/__bmlimporter_gets’ in the amended project contains a function to distributes instances of each namespace. Remember that in the original library version, each library would be its own instance, so these must remain discrete. Here’s a more complex version that will create a bunch of imported library namespaces
Accessing each instance
Because each library namespace is isolated from each other, we can assign the imported library instance to the same user symbol that was used in the original library reference.
Here’s an example of an instance of the library being included in a namespace
It may be that you access libraries that themselves access other libraries and so on recursively. If you are respecting or updating deployments (the versions specified by the libraries, or using the latest deployed versions), then you might end up with multiple versions of the same library.
This information is in itself handy as it will probably prompt you to go and upgrade the library references, but let’s assume that we want to keep the versions exactly as referenced. We have to handle multiple versions of the same code. In this example, we have 2 different versions of the bmLibraryReporter library
In this case, each library namespace references its own instance of a different version of the library, allowing multiple versions to coexist in the same project
Main project code
The main project files are replicated untouched (other than the manifest file which is merged with each of the libraries referenced). The final thing that needs to be handled is the global level reference to the imported libraries referenced by the main project (as opposed to the recursively resolved libraries).
These declarations are found in _bmlimport/__globals
The project now looks like this
The import process can upgrade and harmonize the versions of libraries it pulls in using the versionTreatment parameter, which has these options
This describes which version of the referenced libraries to pull, and can take these values
respect – Respect the version mentioned in the manifest referencing the library. This is the default.
upgrade – Upgrade to the latest deployed version. Note that the deployment record for the new IDE is checked here. It doesn’t have access to libraries deployed using the old IDE, so if it can’t find a new IDE deployment it’ll fail with an error like this. If it does, then you could consider doing a sloppyupgrade (see later) as a workaround if you can’t deploy using the new IDE.
Error: Couldn’t find any deployments for 1v_l4xN3ICa0lAW315NQEzAHPSoNiFdWHsMEwj2qA5t9cgZ5VWci2Qxv2:(cGoa) (upgrade:upgrade to the latest deployed versions) – maybe they were deployed with old IDE?
head – Use the latest code of the library whether or not it’s been deployed. This is equivalent to development mode. These will generate console messages like this.
Modified reference to 1U6j9t_3ONTbhTCvhjwANMcEXeHXr4shgzTG0ZrRnDYLcFl3_IH2b2eAY:(cCacheHandler) from version 18 to head ((head:use the latest version whether or not deployed))
sloppyupgrade – The same as upgrade, but if it can find no deployed versions or the deployed version is lower than the one in the manifest it will do a head. This could generate a console message like this
Sloppy upgrade of reference to library 1dajqLysdKo8IoqddtEaGhtUUlSbtSQ1Agi2K5cXSUm0DxXfLYouSO9yD:(bmRottler) from version 10 to head (use the latest deployed version if there is one – otherwise use the head)
By bringing in all these libraries, it’s possible that they have dependencies (other than libraries) such as oauth scope requirements. The importing process merges all the library manifests (and upgrades the run time to v8 by default), so the final manifest will contain a melange of all the library manifest requirements.
It may be necessary to revert to the library version of a project. During import, it creates a file called __bmlimport/manifest.json. This is just a copy of the original manifest. Therefore it’s very straightforward to revert to the library version, simply by copying the contents into your appsscript.json, then deleting everything in __bmlimport/*. The library also provides a .revert() method that does all that for you.
The key here is the argument ‘global’, which receives ‘this’ as the argument value. When it is in a library ‘this’ refers to the global space of the library, but when it’s inlined, then ‘this’ refers to the global space of the project, which means it won’t be detected in the correct scope by the inline parser.
This won’t affect the library at all, but it will make that entry point visible when it’s inlined
Example converter and reverter
All are also on github in their respective repos at github.com/brucemcpherson
Also worth reading Import, export and mix container bound and standalone Apps Script projects.