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:

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.

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:

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

Behind the scenes: This right here is the main reason why we have decided to build our own programming language from scratch.

The amount of stuff you have to code to properly translate design system data to code is staggering - and encapsulates everything from obtaining the data (such as Figma API), parsing, auth, writing the conversion scripts, adding automation servers, possibly containers, making it all robust.. It is so much and yet it will still break the second your tech stack, target platform, or the tooling changes.

Traditional templating languages are in no way capable of anything like this. With Pulsar, however, most of the data retrieval is a one-liner and you don't have to worry about anything mentioned above.

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:

{[ 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

Transformers must always have at least one attribute (value), which is the value of the property they are transforming. Value is always sent the first. Transformers without values are not allowed (as it doesn't make sense), and you should use functions instead.

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
     {{ token.name.lowercased() }}
  {[ else ]}
     {{ token.name }}
  {[/]}
{[/]}

Note: Payload is available as if you would declare it through a variable. You can think about it as global variable in javascript, although it is immutable and can not be changed, so trying to override it will throw an error:

{[ 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.

No javascript code has access to any functionality connected to the Supernova backend (your data) unless you specifically pass the data from the blueprint itself.

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

  • Date.now 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.

Behind the scenes: Exporters have the ability to touch your sensitive data, such as design system tokens, components, documentation, and so on.

While we fully expect that before you use any community-built exporter, you will thoroughly inspect its code to be safe and happy (this is why no functionality of exporter is a black-box), we went to great extra lengths that potential data breach is not possible in the first place.

The execution of the exporter javascript code is done through a secure, fully contained sandbox, similarly to what Figma does with their plugin system. If you are interested in more details about the exporter security for enterprise purposes, we will be happy to answer any of your questions.

Last updated