Polaris Sketchwork

This is a collection of proposals and other sketches that I (@wycats) am working on fleshing out for inclusion in Polaris.

In general, they attempt to flesh out and clarify the existing vision for Polaris, which I outlined in my EmberConf 2022 keynote.

At the moment, much of the work of Polaris is underway, and most of the features slated for Polaris have already been implemented in some form. However, there is no cohesive description of the big-picture vision of various features.

In addition some of Polaris' well-established goals have implications that have not yet been described clearly. In particular, two major goals of Polaris are the unification of template and JavaScript features and the elimination of dependencies on classic Ember features in idiomatic Polaris apps. Taken together, these goals require changes to the Ember router, which is the last remaining piece of critical Ember infrastructure that fundamentally depends upon classic features. It's also glaringly out of sync with the lifetime management story of the rest of modern Ember.

Despite this implication, we don't yet have a single place that fleshes out what that means.

My goal with this repository is to provide my personal understanding of the high-level design vision of Polaris, as well as my best guess at proposals that still require additional work.

I plan to add more proposals over the next few weeks and months, but wanted to get the stuff I already wrote down out there in the meantime.

Feel free to file issues on GitHub if you have questions or thoughts!

Ember Reactivity

Note to readers: For the most part, this design document describes the current state of the Ember.js reactivity system, as of Octane, as a way of laying the groundwork for understanding Polaris features. When this document is talking about Polaris features, it explicitly calls that out.

How Ember Reactivity Works

The Data Universe

Reactive Data Cell

An atomic piece of reactive storage that the app can read from or write to.

In Octane, the only kind of Reactive Cell is a @tracked field. Polaris will also include a cell type to create standalone reactive storage outside of a class.

Composite Reactive Object

An object that uses multiple reactive values internally. A Composite Reactive Object exposes methods to read and write to its underlying reactive values.

Composite Reactive Objects are Constructed when they are first first created. The code that constructs a Reactive Object is called a Reactive Constructor.

The code in a Reactive Constructor may read from the data universe, but it must not write to the data universe. However, if a Reactive Constructor initializes a reactive variable for the first time, it may mutate the reactive variable for the duration of the Reactive Constructor.

The Data Universe

The collection of all Reactive Storage.

Formula

Normal JavaScript functions or getters that compute values based on other reactive values. These functions do not need to be annotated, but they must not mutate the data universe.

The Ember Rendering process reads from Reactive Storage and Formulas in the data universe, but must not write to it. (In Ember, Formulas used in the Rendering process are called "helpers".)

👍 A good rule of thumb: don't mutate anything in your getters or helpers.

Polaris will include a number of built-in Composite Reactive Objects: Map, Set, WeakMap, WeakSet, array and object.

💡 All reactive storage built into Ember follows the Equivalence rule. This means that they are (a) annotations of storage built into JavaScript, (b) have the same behavior as the underlying JavaScript storage, and (c) behave equivalently if the annotation is removed. The annotation causes the rendered output to remain up to date when the storage is mutated, but it does not affect the behavior or timing of the data itself.

The Data Universe is Always Coherent

When Reactive Storage is mutated, the mutation takes effect immediately. Any code that reads from the variable will see the new value.

This means that the Data Universe is always coherent. If you make a change to a reactive variable, and you call a function that depends on the reactive variable, the function will see the new state of the reactive variable, and the value it returns will therefore be up to date.

Example
class Person {
  @tracked name: string;
  @tracked location: string;

  constructor(name: string, location: string) {
    this.name = name;
    this.location = location;
  }

  get card() {
    return `${this.name} (${this.location})`;
  }
}

const wycats = new Person("Yehuda", "New York");

wycats.card; // "Yehuda (New York)"

wycats.name = "Yehuda Katz";
wycats.card; // "Yehuda Katz (New York)"

wycats.location = "San Francisco";
wycats.card; // "Yehuda Katz (San Francisco)"

wycats.location = "Portland";
wycats.card; // "Yehuda Katz (Portland)"

The Output

Rendering

Ember reads from Reactive Variables, as well as functions and getters that depend on reactive variables, in order to create and update the DOM.

Functions and getters called during Rendering are called Formulas. Formulas may read from the data universe, but they must not mutate the Data Universe.

Actions

An Action is any code that runs inside of a browser callback, such as a click handler. Actions may freely read or mutate the Data Universe. By definition, an Action does not happen during Rendering.

The constructor in the Person class above is a Reactive Constructor.

Resources

A Resource is a user-defined Composite Reactive Object with cleanup behavior. You get access to a resource's value by linking it to a parent object. When the parent object is destroyed, the resource is destroyed as well, which means its cleanup behavior is run.

For example, a Resource might be a class that represents the current version of a document delivered over a web socket. When the connection is closed, the web socket should be closed.

Example

import { Resource, cell } from "@glimmer/reactivity";

function RemoteData(url) {
  return Resource((resource) => {
    const value = cell({ type: "loading" });
    const controller = new AbortController();

    resource.on.cleanup(() => controller.abort());

    fetch(url, { signal: controller.signal })
      .then((response) => {
        if (response.ok) {
          return response.json();
        } else {
          value.current = { type: "error", value: response.statusText };
        }
      })
      .then((data) => {
        value.current = { type: "success", value: data };
      });

    return value;
  });
}
In TypeScript
import { Resource, type Linkable, cell } from "@glimmer/reactivity";

function RemoteData<T>(url: string): Linkable<RemoteData<T>> {
  return Resource((resource) => {
    const value = cell({ type: "loading" });
    const controller = new AbortController();

    resource.on.cleanup(() => controller.abort());

    fetch(url, { signal: controller.signal })
      .then((response) => {
        if (response.ok) {
          return response.json();
        } else {
          value.current = { type: "error", value: response.statusText };
        }
      })
      .then((data) => {
        value.current = { type: "success", value: data };
      });
  });
}

type RemoteData<T> =
  | {
      type: "loading";
    }
  | {
      type: "success";
      data: T;
    }
  | {
      type: "error";
      error: Error;
    };

In this example, the RemoteData function takes a URL and returns a linkable resource.

The Resource function is similar to new Promise in JavaScript. It takes a callback that constructs the resource (a reactive constructor).

In this case, the resource's constructor uses a cell to represent the current state of the resource. It uses an AbortController to make the fetch abortable, and then registers a resource cleanup handler that aborts it.

It initiates the fetch, and in response to the fetch succeeding or failing, it sets the value of the cell.

Finally, it returns the cell, which is the value of the resource.

And this is how it's used in a component:

import { use, resource } from "@glimmer/reactivity";

export default class UserComponent extends Component {
  @use user = () =>
    RemoteData(`https://api.example.com/users/${this.args.id}`);

  <template>
    {{#if (eq (user.type) "loading")}}
      Loading...
    {{else if (eq (user.type) "error")}}
      Error: {{user.value}}
    {{else}}
      Hi! {{user.value.name}}
    {{/if}}
  </template>
}
With the #match Proposal
import { use, resource } from "@glimmer/reactivity";
import { RemoteData } from "#lib/remote-data";

export default class UserComponent extends Component {
  @use user = () =>
    RemoteData("https://api.example.com/users/${this.args.id}");

  <template>
    {{#match this.user}}
      {{:when "loading"}}
        Loading...
      {{:when "error" as |error|}}
        Error: {{error}}
      {{:when "success" as |user|}}
        Hi! {{user}}
    {{/match}}
  </template>
}

Details

  1. If you return a cell or formula (a function with no parameters) from a resource constructor, the value of the resource is the value of the cell or return value of the formula. Otherwise, value of the resource is the return value of the resource constructor.
  2. The @use decorator links the resource to the instance of the object that it's used in (in this case, the component).
  3. The function passed to resource() is, itself, a formula. If it uses reactive values when constructing the resource, and they change, the resource will be cleaned up and re-created. (This effectively makes resources restartable by default).

Using Resources in Templates

Resources are used in templates the way helpers are used in Octane.

💡 That's because resources and helpers are the same thing in Polaris.

import { RemoteData } from "#app/lib/remote-data";

<template>
  {{#let (RemoteData (concat "https://api.example.com/users/" @id)) as |data|}}
    {{#if (eq (data.type) "loading")}}
      Loading...
    {{else if (eq (data.type) "error")}}
      Error: {{data.value}}
    {{else}}
      Hi! {{data.value.name}}
    {{/if}}
  {{/let}}
</template>
With #match Proposal
import { RemoteData } from "#app/lib/remote-data";

<template>
  {{#let (RemoteData (concat "https://api.example.com/users/" @id)) as |data|}}
    {{#match data}}
      {{:when "loading"}}
        Loading...
      {{:when "error" as |error|}}
        Error: {{error}}
      {{:when "success" as |user|}}
        Hi! {{user.name}}
    {{/match}}
  {{/let}}
</template>

Two differences between using a resource in a template and using a resource in a class:

  • In a class, you use @use to link the resource's lifetime to the lifetime of the class instance. In a template, that happens automatically.
  • In a class, you construct the resource with resource(() => ...). In a template, you don't need to wrap the call to RemoteData, because the template syntax already does that for you.

Relationship to Octane Features

Relationship to Shipped Primitives

Relationship to Approved but Unshipped Primitives

  • Cell is built on createStorage (approved RFC #669). It could mostly be built on top of @tracked, but we need cell to make the cell the value of the resource (and avoid an unnecessary .current in uses of resources representing a single value).

While createStorage is not currently shipped in Ember, a high-fidelity polyfill exists.

The current implementation of the reactivity system in Ember has a notion of description that can be used in debugging. This is a good idea, and we should align it with the design of composite reactive objects.

The #match feature makes it possible to match an instance of an enumeration (an object with a string type property, and a value property).

Let's say we have a Contact type, which can either be a telephone type or an email type. In our template, we'd like to match the contact, and use a component based on what type of contact it is.

import EmailContact from "./EmailContact";
import TelephoneContact from "./TelephoneContact";
import { Component } from "@glimmer/component";

export default class ContactComponent extends Component {
  <template>
    {{#match @contact}}
      {{:when "telephone" as |number|}}
        <TelephoneContact @number={{number}} />
      {{:when "email" as |email|}}
        <EmailContact @email={{email}} />
    {{/match}}
  </template>
}

In TypeScript:

import EmailContact from "./EmailContact";
import TelephoneContact from "./TelephoneContact";
import { Component } from "@glimmer/component";

type Contact =
  | {
      type: "telephone";
      value: string;
    }
  | {
      type: "email";
      value: string;
    };

interface ContactSignature {
  Args: {
    contact: Contact;
  };
}

export default class ContactComponent extends Component<{
  Args: { contact: Contact };
}> {
  <template>
    {{#match @contact}}
    {{:telephone as |number|}}
      <TelephoneContact @number={{number}} />
    {{:email as |email|}}
      <EmailContact @email={{email}} />
    {{/match}}
  </template>
}

The Default Clause

A #match block can have an else clause. If the #match block is not matched, the else clause is used.

Curly Components Get Named Blocks

The initial design of the named blocks feature added named blocks to angle-bracket components, but not to curly components ("This RFC does not propose an extension to curly syntax, although a future extension to curly syntax is expected.")

The #match syntax is based upon extending named blocks to curly syntax.

Since angle bracket components landed in Ember, curly components that take a block exist philosophically to model control flow. Handlebars already has an else syntax, and a lot of control flow can be shoehorned into the notion of else. However, #match illustrates that general-purpose control flow cannot always be modelled as a pair of "default" and "else" blocks.

The semantics of named blocks in curly components are identical to the semantics of named blocks in angle bracket components.

The syntax of named blocks in curly components aligns with the else syntax, and do not require a closing tag.

Optional else Clause

Named Blocks in curly components can have an optional else clause. This is useful to allow a control-flow construct (like #match) to take an alternative while still allowing the entire space of block names to be used.

{{#match @contact}}
{{:telephone as |number|}}
  <TelephoneContact @number={{number}} />
{{:email as |email|}}
  <EmailContact @email={{email}} />
{{else as |value|}}
  <p>{{value}}</p>
{{/match}}

The Enumeration Format

The #match syntax takes an instance of an "enumeration" as its argument, and yields a block based on the type of the enumeration.

An enumeration is an object with:

  • a type property, whose value is a string
  • an optional value property, with any value

TypeScript Support

This feature is based on TypeScript's discriminated union feature.

This means that the #match syntax can be trivially translated to TypeScript.

Template Syntax:

{{#match @contact}}
  {{:telephone as |number|}}
    <TelephoneContact @number={{number}} />
  {{:email as |email|}}
    <EmailContact @email={{email}} />
{{/match}}

Translated to TypeScript:

switch (this.args.contact.type) {
  case "telephone":
    TelephoneContact({ number: this.args.contact.value });
    break;
  case "email":
    EmailContact({ email: this.args.contact.value });
    break;
}

When translated this way, the values yielded to the blocks get the correct, narrowed type by TypeScript.

Motivation: Asynchronous Data

In the design of Polaris features, we frequently want to model the state of asynchronous data as a reactive data structure.

This allows resources to use asynchronous loading internally, and expose their status as a reactive value.

We want to model this as an enumeration:

type AsyncData<T> =
  | {
      type: "loading";
    }
  | {
      type: "success";
      value: T;
    }
  | {
      type: "error";
      value: unknown;
    };

It is, of course, possible to use AsyncData in a template by using #if:

{{#if (eq @data.type "loading")}}
  <p>Loading...</p>
{{else if (eq @data.type "success")}}
  <p>Hello {{@data.value.name}}</p>
{{else if (eq @data.type "error")}}
  <p>Something went wrong: {{@data.value}}</p>
{{/if}}

💡 This would even narrow correctly in TypeScript, provided that the eq helper is translated to ===.

However, this is not ergonomically ideal, in part because it doesn't make it very obvious that @data.value depends on @data.type.

The #match syntax makes this more ergonomic and clearer:

{{#match @data}}
{{:loading}}
  Loading...
{{:success as |user|}}
  <p>Hello {{user.name}}</p>
{{:error as |error|}}
  <p>Something went wrong: {{error}}</p>
{{/match}}

Debouncing

In the [router] design, we'd like to model the existing model hook in the Route class as a function that returns a Resource<AsyncData>.

You could specify your loading and error states via #match.

A Route Today

import { Route } from "@ember/routing";

export default class extends Route {
  async model({ user_id }: { user_id: string }) {
    const data = await fetch(`/api/users/${user_id}`);
    return data.json();
  }
}

Loading:

<Loading />

Error:

Something went wrong...

Loaded:

<h1>{{@model.name}}</h1>

<div class="user-details">
  <p>{{@model.email}}</p>
  <p>{{@model.phone}}</p>
</div>

A Polaris Route

import { Route } from "@ember/routing";

export default class extends Route {
  @use model = resource(() => RemoteData(`/api/users/${this.params.user_id}`));

  <template>
    {{#match this.model}}
    {{:loading}}
      Loading...
    {{:error as |error|}}
      <p>Something went wrong: {{error}}</p>
    {{:success as |user|}}
      <h1>{{user.name}}</h1>

      <div class="user-details">
        <p>{{user.email}}</p>
        <p>{{user.phone}}</p>
      </div>
    {{/match}}
  </template>
}

In this design:

  • The route is a component that gets params from the router.
  • The route is not long-lived. When the user navigates away from the route, the component is destroyed, like any other route.
  • The "model hook" is no longer special. It's simply a resource.
  • If the user navigates away from the route, and the RemoteData is still active, it is torn down. It was possible to model this with the model hook, but it basically falls out of making the model a resource and having an idiomatic RemoteData.

In this example, we don't necessarily want to show the loading spinner immediately. Instead, we may want to wait 100ms or so before showing it. We would also want to wait the same amount of time if #match gets a new input, and just keep around the old UI if the new data loads quickly.

This example demonstrates that we probably want a version of #match that's tailored for the case of AsyncData.

Roughly speaking, it would keep two copies of its input: the current one and the next one, and if the next input is loading, it would wait 100ms before committing it to current.

💡 If we ship RemoteData, we will certainly want to give it the full suite of fetch features. However, we can define the shape of the AsyncData enumeration as a protocol without shipping a concrete implementation.

This document is 🚧🚧🚧🚧.

At a 20,000 foot view, the idea is to turn the route into a component that gets params from the router, turn the model hook into a resource that returns AsyncData, and turn the substate feature into a #match on that data.

This document will soon be updated to describe the routing proposal in detail.

In the meantime, resources are described in detail in the reactivity section. #match is described in the match proposal, and the end of that proposal has a detailed sketch of the basic idea of AsyncData and how it could be used in routing.

JavaScript With Content Tags

🚧🚧🚧🚧 This document is a work in progress. The precise details are still rapidly evolving, and not everything that we already know is written down yet. 🚧🚧🚧🚧

JavaScript with Content Tags is an extension to ECMAScript that makes it possible to embed other languages in JavaScript. Importantly, embedded content can refer to variables in the surrounding JavaScript environment.

Like template literals, this proposal is designed for modularity: anyone can define a content tag without needing to change the core design. Unlike template literals, content tags have a build-time component, which allows them to translate their specific content tag into JavaScript.

The Syntax

  PrimaryExpression :
+   ContentTag

  Declaration :
+   ContentTag

  FunctionBody :
+   ContentTag

  ClassElement :
+   ContentTag
+
+ ContentTag :
+   "<" ContentTagOpen [TemplateAttributes] ">" ContentTagBody "</" ContentTagClose ">"
+
+ ContentTagOpen :
+   TagName (* lookahead: whitespace or ">")
+
+ ContentTagClose :
+   TagName (* must match most recent ContentTagOpen *)
+ 
+ TagName :
+     Identifier
+   | Identifier { "." Identifier }
+ 
+ ContentTagBody :
+   <unicode character>* (* lookahead: "</template>" *)
+
+ TemplateAttributes :
+   <whitespace> { TemplateAttribute } <whitespace>
+
+ TemplateAttribute :
+   AttributeName (* lookahead: <whitespace> | ">" *)
+   AttributeName "=" AttributeValue
+
+ AttributeName :
+   { <not whitespace or "="> }
+
+ AttributeValue :
+     "{" AttributeExpression "}"
+   | SingleQuotedAttributeValue
+   | DoubleQuotedAttributeValue
+
+ AttributeExpression :
+   <described below>
+
+ SingleQuotedAttributeValue :
+   "'" { <not "'"> } "'"
+ 
+ DoubleQuotedAttributeValue :
+   '"' { <not '"'> } '"'

AttributeExpression

AttributeExpression is lexed using the simplified JavaScript lexer.

Translation

PrimaryExpression

The content tag name is translated to the tag reference of a tagged template literal. The body of the tag is translated to the body of the tagged template literal.

If the content tag contains attributes, the attributes are translated to an object literal. Attributes with values are translated to keys and values in the object literal. Attributes without values are translated the same way, with the value true. The object literal is passed to the tagged template literal.

Basic Example

function Card({ person }) {
  return <jsx>
    <div className="card">
      <div className="card-header">
        <h3>{person.name}</h3>
      </div>
      <div className="card-body">
        <p>{person.bio}</p>
      </div>
    </div>
  </jsx>
}

This would be translated to:

function Card({ person }) {
  return jsx`
    <div className="card">
      <div className="card-header">
        <h3>${person.name}</h3>
      </div>
      <div className="card-body">
        <p>${person.bio}</p>
      </div>
    </div>
  `;
}

Example With Attributes

const card = <template strict>
  <div class="card">
    <div className="card-header">
      <h3>{{@person.name}}</h3>
    </div>
    <div class="card-body">
      <p>{{@person.bio}}</p>
    </div>
  </div>
</template>

This would be translated to:

const card = template({ strict: true })`
  <div class="card">
    <div className="card-header">
      <h3>${person.name}</h3>
    </div>
    <div class="card-body">
      <p>${person.bio}</p>
    </div>
  </div>
`;

Compiler Extension Architecture

FAQ

Why Not Template Literals?

JavaScript with Content Tags makes it possible for languages with their own syntax for variable references to use that syntax when referring to JavaScript variables. It also allows embedded languages to use any characters without worrying about escaping them, and allows the embedded language to restrict how JavaScript variables can be used.

In contrast, template literals must use the JavaScript ${} syntax, and the embedded content in a template literal is an arbitrary JavaScript expression.

Example: Embedding Handlebars

For example, consider embedding Handlebars in JavaScript:

const person = {
  name: "John",
  location: "New Mexico, USA",
};

const contact = <template>
  <p>
    <strong>{{person.name}}</strong> ({{person.location}})
  </p>
</template>

contact(); // <p><strong>John</strong> (New Mexico, USA)</p>

We could parameterize the template using the approach Ember uses today:

const contact =
  <template>
    <p>
      <strong>{{@name}}</strong> ({{@location}})
    </p>
  </template>

contact({ name: "John", location: "New Mexico, USA" });
// <p><strong>John</strong> (New Mexico, USA)</p>

Or by creating a function that takes the person as a parameter:

function Contact(person) {
  <template>
    <p>
      <strong>{{person.name}}</strong> ({{person.location}})
    </p>
  </template>
}

contact({ name: "John", location: "New Mexico, USA" });
// <p><strong>John</strong> (New Mexico, USA)</p>

We could also adopt something like the Ember approach to components:

function Contact(args: { name: string; location: string }) {
  <template>
    <p>
      <strong>{{args.name}}</strong> ({{args.location}})
    </p>
  </template>
}

const Person =
  <template>
    <Contact @name="John" @location="New Mexico, USA" />
  </template>

Example: Embedding JSX

The content tags framework makes it possible to embed JSX into JavaScript without having to use the ${} syntax.

Consider this example from the JSX proposal, using template literals:

// Template Literals
var box = jsx`
  <${Box}>
    ${
      shouldShowAnswer(user) ?
      jsx`<${Answer} value=${false}>no</${Answer}>` :
      jsx`
        <${Box.Comment}>
         Text Content
        </${Box.Comment}>
      `
    }
  </${Box}>
`;

This obviously reads very poorly. The JSX spec also rightly points out that simply wrapping the JSX in a template literal is not sufficient.

var box = jsx`
  <Box>
    {
      shouldShowAnswer(user) ?
      <Answer value={false}>no</Answer> :
      <Box.Comment>
         Text Content
      </Box.Comment>
    }
  </Box>
`;

However, this would lead to further divergence. Tooling that is built around the assumptions imposed by template literals wouldn't work. It would undermine the meaning of template literals. It would be necessary to define how JSX behaves within the rest of the ECMAScript grammar within the template literal anyway.

To address the issue, JSX defines some additional ways to access local variables.

var box =
  <Box>
    {
      shouldShowAnswer(user) ?
      <Answer value={false}>no</Answer> :
      <Box.Comment>
         Text Content
      </Box.Comment>
    }
  </Box>;

Since the content tags framework allows embedded languages to specify how their content should access local variables, you could embed JSX in JavaScript:

var box =
  <jsx>
    <Box>
      {
        shouldShowAnswer(user) ?
        <Answer value={false}>no</Answer> :
        <Box.Comment>
          Text Content
        </Box.Comment>
      }
    </Box>
  </jsx>

Example: Embedding CSS

The general content tags framework allows us to easily embed other languages in JavaScript, and allow those languages to refer to JavaScript variables. Let's define a tiny extension to CSS that allows us to refer to JavaScript variables:

const color = "red";

<style>
  .red {
    color: $color;
  }
</style>

In this case, we allow $-prefixed names to refer to JavaScript variables.

We will also allow paths (starting with an identifier followed by any number of . members) to be used as variables, and we will allow standalone functions to be used as CSS functions.

We could combine a feature like this with the Handlebars example above to make it possible to create static HTML and CSS files inside JavaScript:

const light = "#fff";
const dark = "#000";

function border(color: HexColor): string {
  return `1px solid ${color}`;
}

const styles =
  <style>
    .box {
      border: border($light);
    }
  </style>

<template>
  <styles />

  <div class="box">
    Hello, World!
  </div>
</template>

Conclusion

In general, these examples are meant to illustrate the way that content tags can take advantage of JavaScript's lexical scope in ways that are idiomatic to their embedded languages.

🚧🚧🚧🚧 This section is a work in progress. 🚧🚧🚧🚧

Simplified JavaScript Lexer

🚧🚧🚧🚧 This document will describe a simplified way to tokenize JavaScript code. 🚧🚧🚧🚧

It will have high enough fidelity to reliably identify content tags in the code, as well as the position of the content tag (module top-level, function body, or class element).

Optional Second Phase

The first phase of the content tags specification translates the content tag syntax to JavaScript. This is a very useful input into downstream tools, many of which already exist.

For example, the graphql-tag library is a JavaScript library that can be used to generate a GraphQL query from a tagged template string.

import gql from 'graphql-tag';

const query = gql`
  {
    user(id: 5) {
      firstName
      lastName
    }
  }
`

When using the content tag preprocessor, you could write:

import gql from 'graphql-tag';

const query =
  <gql>
    {
      user(id: 5) {
        firstName
        lastName
      }
    }
  </gql>

Without any additional steps, this would be translated to the original graphql-tag syntax, and everything would work as expected.

However, you may want to use the content tag preprocessor as the first step in a build pipeline to generate JavaScript code.

For example, consider the <template> tag feature in Ember Polaris:

import { template } from '@glimmer/template';

const name = "Godfrey";

<template>
  <div>
    <h1>Hello, {{name}}</h1>
  </div>
</template>

The content tag preprocessor would convert this to:

import { template } from '@glimmer/template';

const name = "Godfrey";

export default template`
  <div>
    <h1>Hello, {{name}}</h1>
  </div>
`;

But the template still references name, which is not available at runtime. To make this work, Glimmer templates need a second phase of compilation, which compiles to:

import { template } from '@glimmer/template';

const name = "Godfrey";

export default template("<div>\\n  <h1>Hello, {{name}}</h1>\\n</div>", () => ({ name }));

It is, of course, possible to write a second phase of compilation that works with the output of the content tag preprocessor using something like Babel. That's the point of the design of the content tag preprocessor: to translate the custom syntax into something that can be further translated using vanilla JavaScript build pipelines.

That said, in order to build the second phase, you would need to:

  • Make sure to carefully realign output source maps
  • Do the same steps for eslint and TypeScript, as well as any other tools that depend on rich static analysis.

To make it easier to build this sort of tool, we intend to provide a second phase compiler utility that helps translate the output of the content tag preprocessor into runtime JavaScript.

The design of the second phase compiler will be based on our implementation of such a tool for the <template> tag feature in Ember Polaris.