embroider-build / embroider

Compiling Ember apps into spec-compliant, modern Javascript.
MIT License
339 stars 137 forks source link

[Strawman] Target Macros #185

Open chadhietala opened 5 years ago

chadhietala commented 5 years ago

Motivation

Since Embroider has a macro system for things like package versions and config, I'm wondering if we should add macros for including/excluding platform API polyfills. It is not uncommon that you want to utilize a platform API that happens to be implemented in a subset of browsers. For instance the below is an example of what typically happens when you try to use something like IntersectionObserver which is in all browsers except IE11.

import IntersectionObserverPolyfill from 'intersection-observer';

const options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
};

const observerCallBack = (entries) => {
  entries.forEach(function(entry) {
    let box = entry.target;
    let visiblePct = (Math.floor(entry.intersectionRatio * 100)) + "%";

    box.querySelector(".topLeft").innerHTML = visiblePct;
    box.querySelector(".topRight").innerHTML = visiblePct;
    box.querySelector(".bottomLeft").innerHTML = visiblePct;
    box.querySelector(".bottomRight").innerHTML = visiblePct;
  });
}

export class MyComponent extends Component {
  init() {
    super.init();

    if ('IntersectionObserver' in window) {
      this.observer = new IntersectionObserver(observerCallBack, options);
    } else {
      this.observer = new IntersectionObserverPolyfill(observerCallBack, options);
    }
  }

  didInsertElement() {
    this.observer.observe(this.element);
  }

  willDestroy() {
    this.observer.unobserve();
  }
}

The idea here would to add

or

Design

During the expansion time I believe we could utilize browserList and the caniuse-api to expand into booleans that would then either be inlined or removed from the minifier. I believe the system already cleans unused imports. So the above example would be:

import { macroIf, forTarget } from '@embroider/macros'
import IntersectionObserverPolyfill from 'intersection-observer';

// ... SNIP for brevity sake

export class MyComponent extends Component {
  init() {
    super.init();
    macroIf(forTarget('ie11'),
     () => {
       this.observer = new IntersectionObserverPolyfill(observerCallBack, options) 
     },
     () => {
       this.observer = new IntersectionObserver(observerCallBack, options);
     }
    );
  }

  didInsertElement() {
    this.observer.observe(this.element);
  }

  willDestroy() {
    this.observer.unobserve();
  }
}

For ie11 build the output would be:

import IntersectionObserverPolyfill from 'intersection-observer';

// ... SNIP for brevity sake

export class MyComponent extends Component {
  init() {
    super.init();
    this.observer = new IntersectionObserverPolyfill(observerCallBack, options);
  }

  didInsertElement() {
    this.observer.observe(this.element);
  }

  willDestroy() {
    this.observer.unobserve();
  }
}

But for all other builds you would get:

export class MyComponent extends Component {
  init() {
    super.init();
    this.observer = new IntersectionObserver(observerCallBack, options);
  }

  didInsertElement() {
    this.observer.observe(this.element);
  }

  willDestroy() {
    this.observer.unobserve();
  }
}

Alternative

Typically webpack projects just use the define plugin for feature flagging specific APIs. While this works, it means we would have to expose someway for applications to define this config and get it to webpack. It is also unclear to me if this couples us to webpack in a way that is not desired.

ef4 commented 5 years ago

The embroider equivalent to the webpack define plugin is the getConfig and getOwnConfig macros. So the thing you're describing would be possible using those. Addons do get to run arbitrary node code in the app build, it's just that it's only allowed to emit JSON-serializable state.

But I agree it would be helpful to have standardized macros that consume the shared browser target configuration. It may be possible to implement this entirely in an addon. The addon would compile its own macro to getConfig. The benefit of that pattern is that the core @embroider/macros have a pretty high stability requirement, because published code on NPM is going to use them, and any versioning of them is entirely on us (addons don't get to bring their own macro plugin).

In general, I'm realizing we can probably move this kind of "predicate" macro into "userspace" by compiling it down to getConfig, and that would help reduce the API surface of the core embroider macros package.