Brainstorm: What API would make it easy to statically analyze my components?


#1

I’m using a mixin from an in-repo addon called ember-cli-ui-components in my app. It’s used like this

import Component from '@ember/component';
import { Styled, group } from 'ember-cli-ui-components';

export default Component.extend(Styled, {

  styles: {
    base: 'leading-tight',

    defaultStyle: 'regular margined',

    sizes: group({
      headline: {
        tagName: 'h1',
        style: 'text-2 sm:text-1 font-semibold mt-3'
      },
      regular: {
        tagName: 'h2',
        style: 'text-3 font-semibold'
      },
      small: {
        tagName: 'h3',
        style: 'text-4 sm:text-3 font-medium'
      },
      xs: {
        tagName: 'h4',
        style: 'text-6 sm:text-4 font-semibold'
      }
    }),

    uppercase: 'uppercase',
    center: 'text-center mx-auto',
    transparent: 'opacity-70',
    underlined: 'border-solid border-b-2',
    skeleton: 'bg-light-gray text-light-gray w-256'
  }

});

and components are invoked like this

{{#ui-title style='headline center'}}

The mixin works at runtime to compute the classNames list for the element, and applies them in the template.

Question

My question is, I want to start playing with some compile-time optimizations for these UI components, and I’m wondering what would be the best way to mark/annotate these components, so that they can be easily identified in node-land.

For example, my first experiment would be:

  1. Look through all components/ files
  2. Search for use of the Styled mixin (eg. by looking for Component.extend(Styled, {}))
  3. Extract the component names (e.g. "ui-title")
  4. Do some cool stuff (e.g. make it a build-time component)

So my question is around step 2 of the algorithm. Are there established practices/patterns for APIs that would make it easy to identify these things? Comments, decorators, other annotations? Is it easy to find use of the mixin? Should I use a special/magic property defined on the component?

Would love to hear anyone’s thoughts!


#2

If you’re going to look for stuff in JS files, you will definitely want to do a parse but not an eval. (Meaning it may be tempting to try to just require a component file in node and check for properties on the exported class, but that way lies madness because now every component file needs to run in two environments.)

When you parse, you will want to do it with the same options that the app’s babel parser is using. (For example, if the app has enabled some experimental stage 2 features, you don’t want your parser to choke on those.) I have examples of doing this, see here and here and here. Baby 7 has a much nicer API for doing this, but we’re mostly all still on Babel 6.

Depending on your API you can also consider compile-time optimization in templates rather than JS. It’s somewhat easier to do, since you can register an AST transform that takes advantage of the existing AST, rather than adding another whole parser stage like you would need to do in JS.


#3

Awesome – I will definitely check out your examples!

I do wish we could parse (load up) the same file in node & in browserland… I really just want to check for the existence of my Styled mixin and that would be pretty easy if I could execute the code.

Ultimately I am doing compile-time stuff in template-land, but I need to look at the JS to identify which components are using Styled, and the templates don’t contain that info.

(Perhaps I could think of another sigil to identify these components, for example if they start with ui-. But I’m anticipating needing a bit more configurability.)


#4

My point is to not confuse “parse” with “load up”. You can definitely parse in both places.


#5

Ok, I think I am confusing them :stuck_out_tongue:

So, to clarify: I will have to use Babel to parse the file – say, a component – into an AST, and then do some traversals to identify my Styled mixin…

Versus being able to load/require the component, and just check for something like an instance property.

Is that accurate?


#6

Also, I know you said

it may be tempting to try to just require a component file in node and check for properties on the exported class, but that way lies madness because now every component file needs to run in two environments

The further I get the more I think I’m going to want this. For example, my Node-time stuff needs access to some config objects that live on Ember components (for example, the styles key on the Ember component in the OP).

Are you sure there’s no reliable way to do this?

Another way of asking the question is, what’s the best way to share data between Ember Components and build-time Node-land stuff?


#7

Your original thread title is “statically analyze”, and I’m advocating that you do statically analyze. Static analysis is not eval(). If it needs to run the code it’s not static, it’s dynamic, and at that point it’s easy for people to try things that look like they should work but don’t. For example:

import Component from '@ember/component';
import { Styled } from 'ember-cli-ui-components';

export default Component.extend(Styled, {
  styles: {
    base: window.innerWidth > 800 ? 'leading-wide' : 'leading-narrow'
  }
});

But that doesn’t mean you can’t have the API you want, relying only on static analysis. The most familiar example of this idea in ember is:

import hbs from 'htmlbars-inline-precompile';
let template = hbs`<div></div>`;

This looks like regular javascript, and as long as the template is valid it’s indistinguishable (from the user’s perspective) from regular javascript. But it has some very weird properties! For one thing, if your template isn’t valid you get a build-time error instead of a runtime error. For another, this works:

import compileHandlers from 'htmlbars-inline-precompile';
let template = compilerHandlers`<div></div>`;

But this doesn’t:

import hbs from 'htmlbars-inline-precompile';
let compileHandlebars = hbs;
let template = compilerHandlers`<div></div>`;

Because the static analysis is good but not flawless. In this case, hbs is pretty good because tagged template strings are already a pretty statically limited syntax, so the gap is not too uncanny.

You could have something like:

import { buildStyles } from 'ember-cli-ui-components';
export default Component.extend({
  styles: buildStyles({
    base: 'leading-tight'
  })
});

Where buildStyles gets tracked and discovered statically, just like hbs does. The input you will get from that process is not the value of the actual POJO that people are passing you, it’s the AST representing that POJO. You can look at that AST and know for certain that they’re only doing legal things (like using string and number literals) and no illegal things (like trying to evaluate an expression you can’t reliably evaluate the same in both environments). If somebody tries a trick like the window.innerWidth thing, you can give them a clear build-time error explaining why it won’t work.

Tangentially related to your question, but native classes and decorators are easier to statically analyze. Plus they are rapidly reaching the tipping point where we’re going to tell everybody to use them all the time for their components. Maybe consider designing your API with that syntax in mind:

import { style } from 'ember-ui-components';

export default class extends Component {
  @style
  base = 'leading-light';

  @style
  defaultStyle = 'regular margined'
}