Using Javascript

Javascript is an all-around great scripting language well suited for code-generation tasks, especially if it is combined with a powerful templating engine such as Pulsar.

Pulsar allows you to extend the core language capabilities with javascript directly, allowing you to bring any functionality it doesn't provide out of the box.

Enabling javascript in the exporter package

To enable javascript extension of Pulsar blueprints in the exporter, add config >js key into the exporter.json configuration pointing to the main javascript file you want to use, for example:

Javascript-enabled exporter configuration

Javascript-enabled exporter configuration

Adding "js": "support.js" means that the exporter package will look for a javascript helper file in the root directory, under the name support.js.

Your newly created support.js file is your main entry-point to extend the language functionality of Pulsar. There are currently three main groups of functionality you can extend, each with distinct usage:

  • Adding new synchronous and asynchronous functions
  • Adding transformers, methods you can run on top of specific data types
  • Default data payload

Combined together, you have almost unlimited control over how the data inside blueprints is transformed and computed.

Introducing new language-level functions

Pulsar allows you to call functions (with or without arguments) such as @ds.allTokens(). This will fetch the data from the selected design system and give all of it as the result. Let's take the following example:

                                                            {[ const allTokens = @ds.allTokens() /]}  // Fetch tokens
                                                        {{ allTokens.count() }}                    // Write token count to output

You can define a set of new non-native language functions as well, using the javascript. For example, what if we wanted to skip every N-th token @ds.allTokens() function returns? To do that, we open our support.js file and pass the following code:

                                                            // This will expand Pulsar functionality with new function
                                                        // In any blueprint: @js.skipNthToken(token[], number)
                                                        Pulsar.registerFunction("skipNthToken", function (tokens, n) {
                                                          if (n < 1) {
                                                            return tokens
                                                          return tokens.filter(function(value, index, Arr) {
                                                            return index % n == 0;

We are using Pulsar. global-scope object to register new functionality into the language. Pulsar is only available inside the file you have declared inside the exporter.json configuration.

Once you've done that, the new function is immediately available in your blueprints! When used, the resulting array will be missing every n-th token:

                                                            {[ const allTokens = @ds.allTokens() /]}  
                                                        {{ allTokens.count() }}                         // 9 written to output
                                                        {{ @js.skipNthToken(allTokens, 3).count() }}    // 6 written to output

Pulsar engine is in fact advanced enough to pick up this change right away even for your inline VSCode autocomplete, which you can validate by going to any blueprint and trying to write your function, starting with @ symbol — which signalizes that you are calling a function.

Note that all your custom functions are additionally automatically prefixed with @js. and you must write them as such in your blueprint.

Javascript extension method showing up immediately after it was declared

You can declare an unlimited number of helpers, with an unconstrained number of attributes. Over time, you can even create a library of custom functionality that you can be sharing between your different exporters.

Asynchronous functions with promises

In many cases, you might need promises instead of just plain functions. Pulsar lets you do that as well - in fact, the functions you see natively, such as @ds.allColorTokens() are all using this advanced option. The great thing about promises in Pulsar is that they are automatically identified, analyzed and auto-awaited, so you can write serial code with a very complex "backend". Take the following example:

                                                            {[ const allTokens = @ds.allTokens() /]}  
                                                        {{ allTokens.count() }}                         // 9 written to output

The dsm.allTokens() is a function that selects an appropriate design system to target based on your selection, authenticates you, downloads data from Supernova servers, parses them, prepares them for easy use, yet all its thousands of lines of code powering it are completely hidden to you — allowing you to focus on what is important and that is the manipulation with that data.

In order to register asynchronous function, use the same approach as before, in fact, there is no difference between registering normal and asynchronous function because all is done automatically:

                                                            Pulsar.registerFunction("getSumOfThree", function (first, second, third) {
                                                          return new Promise((resolve, reject) => {
                                                            resolve(first + second + third)

Also, the usage is the same as well:1

                                                            {[ const sum = @js.getSumOfThree(1, 2, 3) /]}  
                                                        {{ sum }}                                          // 6 written to output

Introducing new language-level transformers

You can think of transformers as type-specific methods, for example, to make lowercase string:

                                                            {[ const name = "Supernova".lowercased() /]}
                                                        {{ name }}                                     // supernova

Some transformers work on all data types (string, number, object..) while some only work on specific ones (such as lowercased that can only be used with strings). There is a whole native library of all base transformers you might ever need — but if you are missing some, you can create it yourself.

To do that, register a new transformer similarly to how you've done it with functions:

                                                            // In blueprint: numericValue.minus10(x)
                                                        Pulsar.registerTransformer("minus10", function (value, multiplier) {
                                                          return value - 10

Then, you can use the transformer inside your blueprint, also available in code autocompletion:1

                                                            {[ const baseValue = 10 /]}
                                                        {{ baseValue.minus10(10) }}   // 0 

Note that defining a transformer like this means it can be used on any data type. However, in this case, running the transformer on top of a string would result in strange results - so you can define transformers that are typed and constrained only to one specific type:

                                                            // In blueprint: numericValue.toXTheValue(x)
                                                        Pulsar.registerTransformer("minus10", function (value, multiplier) {
                                                          return value - 10

This will properly force the transformer to do a type check and throw a proper error when an incorrect data type was provided. Allowed types are string, number, object, array and boolean.

                                                            string, number, object, array, boolean

Providing debug or configuration payload

The last thing you can do is to provide the initial payload to blueprint execution. For example, say you want to have an exporter that can be easily reconfigured to either original or lowercased names of the tokens.

To achieve that, you can provide configuration object through the javascript, where will be available to all your blueprints and can serve as a single point of configuration that can be changed easily:

                                                            // In blueprint: numericValue.toXTheValue(x)
                                                        Pulsar.registerPayload("myConfig", {
                                                          useLowercase: true

To access it, simply use it as you would use your own defined property inside the blueprint. The property is defined on a global scope and available everywhere.

                                                            {[ const tokens = @ds.allTokens  /]} // Get all tokens from design system
                                                        {[ for token in tokens ]}             // Iterate through all of them
                                                          {[ if myConfig.useLowercase ]}      // Lowercased or normal name, based on JS configuration
                                                             {{ }}
                                                          {[ else ]}
                                                             {{ }}

                                                            {[ myConfig.useLowercase = 2 /]}    // Throws immutability error

This is to highlight that purpose of providing the payload is to provide immutable, configuration data.

Javascript security and limitations

In order to prevent unintentional usage of the javascript engine and to make it as secure as possible to pass even the most strict enterprise data-protection requirements, we are running the javascript functionality in a sandboxed environment completely separately from the main execution system.

Additionally, for security reasons:

  • All the networking capabilities and access to the network have been removed
  • All the local file system capabilities have been removed
  • and new Date() have been removed
  • RegExp.prototype.compile have been removed
  • Math.random and any other randomizer has been removed
  • All primordial objects are non-extensible
  • All non-standard context properties have been removed

Additionally, no imports of npm modules are currently allowed. We are, however, working on adding this as soon as possible, as well as the ability to create the helpers with Typescript instead of Javascript.