Portals

The biggest pitfall of all code-generation tools is that they completely disregard existing codebase. They always override the entire files or even entire directories, regardless of what was there previously. This means that any manual change, tweak, or enhancement is lost the second code generation runs again.

And that, in reality, makes any code-generation tool worthless beyond anything other than experiments, and certainly not usable in the production environment.

The most powerful feature of the entire blueprint/exporter engine is called Portals, a technology designed to solve this problem which enables developers to co-exist with virtually any generated code or structure.

Portals in theory

Portals define areas in the generated content which are "off-limits" for the engine and preserved/merged when generation is complete. This is supported by a complex and robust logic for extracting and preserving the existing code, which has one single driving directive:

Original code, if defined off limits, must be preserved at all costs

When touching the production code, everything has to be handled with absolute care and the portal engine is aware of that. As a result, merging will always preserve the original code defined off-limits, even if it means breaking the generated code.

Note from developers: When designing this immensely complex system, everything was focused on code security. No one would be thrilled to lose their hard work just because some script run in some unpredictable way, and so everything we've built makes sure this never happens.

As long as it is defined as your code is defined as off-limits, it will be kept intact.

In theory, the portal engine works like this:

Portal engine takes care of extraction and merging the code

Step by step, the following is executed:

  1. The portal engine takes the export destination directory provided by the user

  2. The content of the directory is analyzed and anything important is extracted

  3. Exporter generates what it is supposed to, using portal data to enhance the output

  4. Exported content is validated with the original source

  5. When (and only when) a validation passes, exporter overrides the original files with the newly generated content

If there was no content in the destination directory previously or the directory doesn't exist at all, merging is completely skipped. This usually means that the generator runs for the first time, creating the base files.

Portals in practice

To illustrate, imagine having a CSS stylesheet with styles generated using an exporter. The generated output might look like this:

.style1 {
color: #fff;
font-size: 12px;
}
.style2 {
color: #000;
font-size: 15px;
}

Without Portals, every time there is a change to styles, the generator would throw away the original and replace it with the new version. This means that if add one of the styles manually:

.style1 {
color: #fff;
font-size: 12px;
}
.style2 {
color: #000;
font-size: 15px;
}
.style-manual { <-- This style was added manually
color: #040404;
font-size: 20px;
}

It will be discarded when there was a change to the defined styles:

.style1 {
color: #fafafa; <-- Color changed so the file was regenerated
font-size: 12px;
}
.style2 {
color: #000;
font-size: 15px;
}
<-- style-manual is now missing, as the file was regenerated
but there is no style3 in the data model

That's really unfortunate because it means we can't customize any behavior to our liking. Now, let's see how the blueprint was defined and how we can solve our issue.

The original blueprint takes all styles in Supernova Universal Data Model and represents them as style definitions. The blueprint can look like this:

{* Retrieve style data *}
{[ let styles @project.allStyles() /]}
{* Iterate over each style and use style blueprint to represent it *}
{[ for style in styles ]}
{[ inject "conversion.style" context style /]}
{[/]}

We've used injection to forward data into another blueprint that properly formats and retrieves CSS definition of a specific style. When this blueprint runs, it generates all styles, but it completely disregards any manual additions to the file.

Now, let's look at how we can solve it using the portals. We take the same blueprint but enhance it with code merging capability:

{* Retrieve style data *}
{[ let styles @project.allStyles() /]}
{* Iterate over each style and use style blueprint to represent it *}
{[ for style in styles ]}
{[ inject "conversion.style" context style /]}
{[/]}
{* Add area that can be used for manual input *}
{[ portal /]}

And that's it. Yes, it is really that easy. Now let's look at how the output will change when portal flow tag is used:

.style1 {
color: #fff;
font-size: 12px;
}
.style2 {
color: #000;
font-size: 15px;
}
// <
// >

An area was created using comment tags. Because comments don't participate in compiling the source code, the portal on its own doesn't change the code in any way.

When an opening tag is created, everything that follows is considered manually written and will be used in the merged output. You are now free to write anything inside the portal area, and that part will be preserved:

.style1 {
color: #fff;
font-size: 12px;
}
.style2 {
color: #000;
font-size: 15px;
}
// < Manually written definitions
.style-manual {
color: #123456;
font-size: 12px;
}
// >

Even a description of the portal comment will be preserved. It is a recommended practice to comment on the portal area so others know to write the code there.

Now, any time the code generation runs, the manually written content will be preserved:

.style1 {
color: #fff;
font-size: 12px;
}
.style2 {
color: #000;
font-size: 15px;
}
.style3 {
color: #242424;
font-size: 11px;
}
// < Manually written definitions
.style-manual {
color: #123456;
font-size: 12px;
}
// >

Using multiple Portals

It is completely possible to use several portals within one blueprint. The only rule is that both the merged content and blueprint that defined it has the same number of merging tags. For example, let's take the example above and extend it to merge multiple areas:

{* Retrieve style data *}
{[ let styles @project.allStyles() /]}
{* Iterate over each style and use style blueprint to represent it *}
{[ for style in styles ]}
{[ inject "conversion.style" context style /]}
{[/]}
{* Add area that can be used for manual input *}
{[ portal /]}
{* Retrieve all colors *}
{[ let colors @project.allColors() /]}
{* Iterate over each color and create property for each *}
:root {
{[ for color in colors ]}
{[ inject "conversion.colorprop" context color /]}
{[/]}
{* Add another area that can be used for manual input *}
{[ portal /]}
}

Which will produce the following output when run:

.style1 {
color: #fff;
font-size: 12px;
}
.style2 {
color: #000;
font-size: 15px;
}
// <
<-- Any other custom style can come here, and will be merged
// >
:root {
--main-bg: #fafafa;
--main-header: #fbfbfb;
--main-footer: #fcfcfc;
// <
<-- Any other custom color can come here, and will be merged
// >
}

As you can see, two pairs of portal tags were created, and both can be used to further extend the definition with custom properties, styles, or any other definition.

Portals only work when there is a matching number of portal flow tags defined inside blueprint and opening/closing tags inside the merged content.

For example, if you'd remove one merging pair in the last example (so there would be two portal flows but only one merging pair), the exporter throws an error and will not continue until the code is manually fixed by adding the missing pair or removing one portal flow tag.

This is to prevent unexpected behavior, an approach that aligns with the core principle that the production code should be preserved at all costs.