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