Each exporter must follow certain anatomy as described in previous section. However, looking deeper, there is one even more important piece of the exporter, and that is where all the magic of transforming the design system data into usable output happens.
This function is usually located in index.ts file and contains the entire logic of the exporter.
Function signature
The main export function has the following signature:
Pulsar.export(async (sdk: Supernova, context: PulsarContext): Promise<Array<AnyOutputFile>>
The function is declared as async, which allows you to do some really magical things like download data from network, run tasks in parallel and so on. Its main attributes are:
SDK
The SDK object gives you access to all the data within the target design system. For example, in order to access the token and token group data, you can simply write the following:
Pulsar.export(async (sdk: Supernova, context: PulsarContext): Promise<Array<AnyOutputFile>> => {
// Fetch data from design system that is currently being exported (context)
const remoteVersionIdentifier: RemoteVersionIdentifier = {
designSystemId: context.dsId,
versionId: context.versionId,
}
// Fetch the necessary data
let tokens = await sdk.tokens.getTokens(remoteVersionIdentifier)
let tokenGroups = await sdk.tokens.getTokenGroups(remoteVersionIdentifier)
....
}
Compared to standard Supernova SDK object you have to instantiate yourself, the SDK object is already securely pre-configured with things like an API key, so you can simply start using it right away. Since Supernova gives you access to all the data in your design system, you can build exporters that touch tokens, components, assets, versions, changelogs, even documentation in a structured format. Read more about what all is possible to get from the SDK (hint: everything Supernova has to offer).
Context
You don't have to worry about figuring out what data should be used and what design systems to target — the exporters are agnostic to the design system and workspace selection and should run the same no matter which design system they target. That magic happens in the lifecycle of the exporter automatically and when developing the exporter, you only care about using the resulting context object. The context contains the following information:
- Target workspace
- Target design system
- Target design system version
- Target brand (optional)
- Target theme (optional)
To understand why context is important, imagine that you write an exporter and two different teams will use it. Both teams have a different data in their design systems, but you only need to code the exporter once. When the following code runs for each of the teams:
// Fetch data from design system that is currently being exported (context)
const remoteVersionIdentifier: RemoteVersionIdentifier = {
designSystemId: context.dsId,
versionId: context.versionId,
}
let tokens = await sdk.tokens.getTokens(remoteVersionIdentifier)
The data in the resulting tokens will be different because the data is pulled from a different design system, specified in the context. You can also imagine a situation where you've built an exporter that allows you to export differently themed tokens. In such a situation, the best thing is to set up two different pipelines, each with different themes. In the code, you would then do something like this:
// Apply theme, if specified by the VSCode extension or pipeline configuration
if (context.themeId) {
const themes = await sdk.tokens.getTokenThemes(remoteVersionIdentifier)
const theme = themes.find((theme) => theme.id === context.themeId)
if (theme) {
tokens = await sdk.tokens.computeTokensByApplyingThemes(tokens, [theme])
} else {
// Don't allow applying theme which doesn't exist in the system
throw new Error("Unable to apply theme which doesn't exist in the system.")
}
}
What this code does is that if the theme was selected in the pipeline configuration or in VS Code extension, it will be available in the context object and you can use it to resolve the tokens with themes in mind (the list of resulting tokens will be the same, but their values will be different based on their theme overrides).
When the pipelines run, the themeId will be different for each (say, light and dark for two separate pipelines), resulting in vastly different output.
Output
The final part of the export function is its output. The purpose of the export function is to generate the output to write into the export destination (GitHub repository and so on) with each run.
You can return unlimited number of files from the export function, as long as they all point to unique paths. The simplest output without using any data would be declared as follows:
Pulsar.export(async (sdk: Supernova, context: PulsarContext): Promise<Array<AnyOutputFile>> => {
// Create a static file and return it
return [
FileHelper.createTextFile({
relativePath: "./",
fileName: "colors.css",
content: ":root {}",
}),
]
})
This will result in a single file colors.css with a text context (:root {}), which will be written into the root directory of the export destination. If you'd change the code in the following way:
Pulsar.export(async (sdk: Supernova, context: PulsarContext): Promise<Array<AnyOutputFile>> => {
// Create two static files and return it
return [
FileHelper.createTextFile({
relativePath: "./styles/",
fileName: "colors.css",
content: ":root {}",
}),
FileHelper.createTextFile({
relativePath: "./",
fileName: "index.css",
content: "@import url(./styles/colors.css)",
})
]
})
you'd obtain two files on the output, index.css in the root folder and colors.css in the styles folder.
There are multiple file types you can return, each with a different functionality.