From 0854490e251893705c99ddcf37a10f8a99829b91 Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Wed, 13 Feb 2019 09:23:58 -0500 Subject: [PATCH 1/7] Initial draft --- .../000-sfc-and-template-import-primitives.md | 679 ++++++++++++++++++ 1 file changed, 679 insertions(+) create mode 100644 text/000-sfc-and-template-import-primitives.md diff --git a/text/000-sfc-and-template-import-primitives.md b/text/000-sfc-and-template-import-primitives.md new file mode 100644 index 0000000000..1708373fdb --- /dev/null +++ b/text/000-sfc-and-template-import-primitives.md @@ -0,0 +1,679 @@ +- Start Date: (fill me in with today's date, YYYY-MM-DD) +- Relevant Team(s): Ember.js +- RFC PR: (after opening the RFC PR, update this with a link to it and update the file name) +- Tracking: (leave this empty) + +# SFC & Template Import Primitives + +## Summary + +Expose low-level primitives for associating templates with component classes +and customizing a template's ambient scope. + +These primitives unlock experimentation, allowing addons to provide +highly-requested features (such as single-file components) via stable, public +API. + +## Motivation + +This proposal is intended to unlock experimentation around two +highly-requested features: + +1. Single-file components. +2. Module imports in component templates. + +Although exploring these features is the primary motivation, an important +benefit of stabilizing low-level APIs is that they enable the Ember community +to experiment with new, unexpected ideas. + +### Single-File Components + +In Ember components today, JavaScript code lives in a `.js` file and template +code lives in a separate `.hbs` file. Juggling between these two files adds +friction to the developer experience. + +Template and JavaScript code are inherently coupled, and changes to one are +often accompanied by changes to the other. Separating them provides little +value in terms of improving reusability or composability. + +Other component APIs eliminate this friction in different ways. React uses +JSX, which produces JavaScript values: + +```js +// MyComponent.jsx + +export default class extends Component { + state = { name: 'World' }; + + render() { + return
Hello {this.state.name}!
+ } +} +``` + +Vue has an optional [`.vue` single-file component (SFC) +format](https://vuejs.org/v2/guide/single-file-components.html): + +```html + + + + + +``` + +### Module Imports in Templates + +One benefit of JSX is that it leverages JavaScript's existing scope +system. React components are JavaScript values that can be imported and +referenced like any other JavaScript value. If a binding would be in scope +in JavaScript, it's in scope in JSX: + +```js +import { Component } from 'react'; +import OtherComponent from './OtherComponent'; + +export default class extends Component { + render() { + return ( +
+ // We know OtherComponent is in scope because it was imported at the top + // of the file, so we can invoke it here as a component. + +
+ ); + } +} +``` + +This is a bit nicer than Vue's SFCs, where JavaScript code and template code +happen to be in the same file but otherwise interact no differently than if +they were still in separate files. Developers must learn a proprietary Vue +API for explicitly copying values from JavaScript's scope into the template's +scope, and component names can be different between where they are imported +(`OtherComponent`) and where they are invoked (`other-component`): + +```html + + + +``` + +Neither of these approaches is exactly right for Ember. Ideally, we'd find a +way to "bend the curve" of tradeoffs: maintaining the performance benefits of +templates, while gaining productivity and learnability by having those +templates seamlessly participate in JavaScript's scoping rules. + +### Unlocking Experimentation + +Rather than deciding on the best syntax for single-file components and +template imports upfront, this RFC proposes new low-level APIs that addons +can use as compile targets to implement experimental file formats. + +For example, imagine a file format that uses a frontmatter-like syntax to +combine template and JavaScript sections in a single file: + +```hbs +--- +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import titleize from './helpers/titleize'; +import BlogPost from './components/blog-post'; + +export default class MyComponent extends Component { + @service session; +} +--- + +{{#let this.session.currentUser as |user|}} + +{{/let}} +``` + +Or imagine this syntax, where a component's template is located syntactically +within the class body: + +```js +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import titleize from './helpers/titleize'; +import BlogPost from './components/blog-post'; + +export default class MyComponent extends Component { + + + @service session; +} +``` + +Or even a variation of the Vue SFC format: + +```html + + + +``` + +With the API proposed in this RFC, an Ember addon could transform any of the +above file formats, at build time, into a JavaScript file that looks +something like this: + +```js +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import titleize from './helpers/titleize'; +import BlogPost from './components/blog-post'; + +import { createTemplateFactory, setComponentTemplate } from '@ember/template-factory'; + +const templateJSON$1 = { + "id": "ANJ73B7b", + "block": "{\"statements\":[\"...\"]}", + "meta": { "moduleName": "src/ui/components/MyComponent.js" } +}; + +const template$1 = createTemplateFactory(templateJSON$1, { + scope: () => [BlogPost, titleize] +}); + +export default setComponentTemplate(template$1, class MyComponent extends Component { + @service session; +}); +``` + +Note that the file formats shown above are hypothetical and used as +illustrative examples only. This RFC does *not* propose a recommended file +format. Rather, the goal is to expose public JavaScript API for: + +1. Compiling a template into JSON. +3. Associating a compiled template with its backing JavaScript class. +2. Specifying which values are in scope for a given template at runtime. + +By focusing on primitives first, the community can iterate on different SFC +designs via addons, before settling on a default format that we'd incorporate +into the framework. + +## Detailed design + +### Module API + +This RFC proposes the following new modules and exports: + +Name | Description +---------|---------- +`import { precompile } from '@ember/template-compiler'` | Compiles template source code into a JSON wire format. +`import { createTemplateFactory } from '@ember/template-factory'` | Creates a template factory from wire format JSON, provides scope values. +`import { setComponentTemplate } from '@ember/template-factory'` | Associates a template factory with a JavaScript component class. + +Detailed descriptions and rationales for these APIs are provided below. + +### Template Wire Format + +Browsers don't understand Ember templates natively. So how are these +templates compiled and turned into a format that can be run in the browser? + +The Glimmer VM supports two modes for compiling templates: an Ahead-of-Time +(AOT) mode where templates are compiled into binary bytecode, and a +Just-in-Time (JIT) mode where templates are compiled into an intermediate +JSON format, with final bytecode compilation happening on demand in the browser +the first time a template is rendered. + +Ember uses Glimmer's JIT mode, so templates are sent to the browser as an +optimized JSON data structure we call the _wire format_. The +`@glimmer/compiler` package provides a helper function called `precompile` +that turns raw template source code into this "pre-compiled" JSON wire format. + +```js +import { precompile } from '@glimmer/compiler'; +const json = precompile(`

{{this.firstName}}

`); +``` + +There is some additional processing Ember does on top of the Glimmer VM +compiler to allow compiled templates to work. This RFC proposes exposing this +functionality as public API via a `precompile` function exported from the +`@ember/template-compiler` package. + +```js +import { precompile } from '@ember/template-compiler'; +const json = precompile(`

{{this.firstName}}

`); +``` + +The exact structure of the wire format returned from `precompile` is not +specified, is not considered public API, and is likely to change across minor +versions. Other than a guarantee that it is a string that can be safely +parsed via `JSON.stringify`, users should treat the value returned by +`precompile` as completely opaque. + +### Template Factories + +Once we have our template compiled into the wire format, we need a way to +annotate it so that Ember knows that the JSON value represents a compiled +template. The wrapper object around the wire format JSON that does this is +called a _template factory_. + +This RFC proposes exposing a function for creating template factories called +`createTemplateFactory`, exported from the `@ember/template-factory` package. + +Users would use this function to turn the raw, wire format JSON into a template +factory object at runtime: + +```js +import { createTemplateFactory } from '@ember/template-factory'; + +const json = /* wire format JSON here */ +const templateFactory = createTemplateFactory(json); +``` + +### Associating Template Factories and Classes + +In Ember, a component is made up of a template and an optional backing +JavaScript class. But how does Ember know which class and template go +together? + +Today, the answer is "it's complicated." That complexity makes it difficult +for addons to override default behavior and experiment with things like +single-file components. + +By default, templates are associated with a class implicitly by name. For +example, the template at `app/templates/components/user-avatar.hbs` will be +joined with the class exported from `app/components/user-avatar.js`, because +they share the base file name `user-avatar`. + +The benefit of this approach is that files are in predictable locations. If +you are editing a template and need to switch to the class, or are reading a +another template and want to see the implementation of the `{{user-avatar}}` +component, finding the right file to jump to should not be difficult. + +Sometimes, though, Ember developers run into scenarios where they'd like to +re-use the same template across multiple components. Ember supports +overriding a component's template by defining the `layout` property in the +component class: + +```js +import Component from '@ember/component'; +import OtherTemplate from 'app/templates/other-template'; + +export default Component.extend({ + layout: OtherTemplate +}) +``` + +However, this capability comes at a cost. Supporting dynamic layouts like +this requires Ember to put the Glimmer VM into a mode where it checks each +component instance for a `layout` property, preventing these components from +being fully optimized. + +This RFC proposes a more explicit, static system for associating templates +with component classes. A function called `setComponentTemplate` is exported +from the `@ember/template-factory` package. To create the association, this +function is invoked with the template factory and component class as +arguments: + +```js +import Component from '@glimmer/component'; +import { createTemplateFactory, setComponentTemplate } from '@ember/template-factory'; + +const json = /* wire format JSON here */ +const templateFactory = createTemplateFactory(json); + +class MyComponent extends Component { + /* ... */ +} + +setComponentTemplate(template, MyComponent); +``` + +For convenience, `setComponentTemplate` returns the same component class that +was passed, allowing the class to be defined and associated with a template +in a single expression: + +```js +export default setComponentTemplate(template, class MyComponent extends Component { + /* ... */ +}); +``` + +Even though this is a runtime API, template association is intended to be as +static and restricted as possible, allowing us to enable better optimizations +in the future: + +* Templates should be set on component classes as early as possible, ideally + immediately after the class is defined. +* A component's template cannot be set more than once. Attempting to change a + component's template after one has been set will throw an exception. +* Setting a component's template after rendering has begun (even if no previous + template was set) also throws an exception. +* Every instance of a given component has the same template. It is not + possible to dynamically change a component's template at runtime. + +Component subclasses inherit their template from the nearest superclass that +has had a template set via `setComponentTemplate`. If no superclass has an +associated template, Ember will fall back to the current template resolution +rules. + +Templates set via `setComponentTemplate`, including those set on a +superclass, take precedence over the `layout` property, container lookup, or +any other existing mechanism of template resolution. For example, a template +set on a superclass via `setComponentTemplate` would take precedence over a +subclass's `layout` property (if it had one). However, a template set via +`setComponentTemplate` on the subclass would take precedence over the +superclass. + +### Template Scope + +In this context, _scope_ refers to the set of names that can be used to refer +to values, helpers, and other components from within your template. For +example, when you type `{{@firstName}}`, `{{this.count}}`, or ``, how does Ember (or even you, as the programmer) figure out what each of these +refers to? The answer to that is determined by the template's scope. + +Today, the names available inside a particular template are determined by a +few different rules: + +1. `this` always refers to the component instance (if the template has a backing class). +2. Arguments (like `@firstName`) are placed into scope when you invoke the component. +3. Block parameters (like `item` in `{{#each @items as |item|}}`) are in scope, but only inside the block. +4. Anything else must be looked up dynamically through Ember's container. + This lookup process is somewhat complex and not always as fast as we'd like. + +Because the rules of how components, helpers, and other values are looked up +can be confusing for new learners, we'd like to explore alternate APIs that +allow users to explicitly import them and refer to them from templates. + +Today, it's not easy for addons to add additional names to a template's scope +that take precedence over dynamic resolution. + +This RFC proposes that the `precompile` function, described earlier, also +accepts a `scope` option that specifies additional identifiers (as strings) +that are available in the template's scope: + +```js +import { precompile } from '@ember/template-compiler'; + +const json = precompile(`{{t "Hello!"}} `, { + scope: ['User', 't'] +}); +``` + +The `scope` option is an array of zero or more strings. In this example, the +identifiers `User` and `t` are added to the template scope. Identifiers +provided this way are in scope for the entire template (unless shadowed by a +block argument). + +This RFC further proposes that the `createTemplateFactory` function, +described earlier, also accepts a `scope` option that returns the reified +scope values at runtime: + +```js +import { createTemplateFactory } from '@ember/template-factory'; +import User from './OtherComponent'; +import t from '../helpers/i18n/t'; + +const json = /* wire format JSON here */ +const template = createTemplateFactory(json, { + scope: () => [User, t] +}); +``` + +Note that, unlike with `precompile`, the `scope` option here is not an array +but a function that _returns_ an array. This allows the array to be created +after the JavaScript module has finished evaluating, ensuring the bindings +included in the array have had time to be fully initialized. + +The order of the array is unimportant, other than that the same order must be +preserved between the identifiers passed to `precompile` and the +corresponding values passed to `createTemplateFactory`. + +Values provided to the template scope must be constant. Once the `scope` array has been created, +mutation of those values is unsupported and may cause unexpected errors +during render. Future RFCs may introduce support for scope values that change +over time. + +Scope values are also limited to component classes and template helpers (i.e. +subclasses of `Helper` or functions wrapped with `helper()` from +`@ember/component/helper`). However, support for additional kinds of scope +values may be expanded in future RFCs. + +If any of the following conditions are met, a runtime exception will be thrown: + +* The `scope` function returns a value that is not an array. +* The `scope` function returns an array containing a value that is not a + component class, helper, or `undefined`. +* The `scope` function returns an array whose length is different than + the `scope` array provided to `precompile`. +* A `scope` was provided when calling `precompile`, but is not provided when + calling `createTemplateFactory` with the resulting wire format. +* A `scope` was _not_ provided when calling `precompile`, and _is_ provided when + calling `createTemplateFactory` with the resulting wire format. + +## How we teach this + +### Guide-level Documentation + +This is a low-level API, and most Ember developers should never need to learn +or care about it. Only addon authors interested in providing alternate file +formats need to understand these APIs in depth. + +As such, the examples and explanations given in this RFC should be enough for +experienced addon authors to get started. Of course, familiarity with +Broccoli, parsers like Babel, and creating Ember CLI addons is a +pre-requisite. + +### API-Level Documentation + +#### precompile + +```js +import { precompile } from '@ember/template-compiler'; +``` + +Compiles a string of Glimmer template source code into an intermediate "wire +format" and returns it as a string. The exact value returned from +`precompile` is not specified, is not considered public API, and is likely to +change across minor versions. Other than the fact that it is guaranteed to be +a string that may be safely parsed by `JSON.parse()`, you should treat the +value returned by `precompile` as completely opaque. + +Optionally, an array of additional identifiers can be specified that will be +included in the template's ambient scope. Each identifier must have a +corresponding value provided at run time when the template's template factory +is created. + +```ts +function precompile(templateSource: string, options?: PrecompileOptions): string; + +interface PrecompileOptions { + scope?: string[]; +} +``` + +#### createTemplateFactory + +```js +import { createTemplateFactory } from '@ember/template-factory'; +``` + +Creates a wrapper object around the raw wire format data of a compiled +template. If the template had additional identifiers added to its ambient +scope when it was compiled with `precompile`, a `scope` function must be +provided when calling `createTemplateFactory` that returns an array of +reified values corresponding to each identifier. + +```ts +function createTemplateFactory(wf: WireFormatJSON, options?: CreateTemplateFactoryOptions): TemplateFactory; + +interface CreateTemplateFactoryOptions { + scope?: () => ScopeValue[]; +} + +type ScopeValue = ComponentFactory | HelperFactory | HelperFunction | undefined; +``` + +#### setComponentTemplate + +```js +import { setComponentTemplate } from '@ember/template-factory'; +``` + +Associates a template factory with a component class. When a component is +invoked, a new instance of the class is created and the template associated +with it via `setComponentTemplate` is rendered. + +```ts +function setComponentTemplate(template: TemplateFactory, ComponentClass: T): T; +``` + +## Drawbacks + +### Requires Addons + +The most obvious drawback is that the APIs outlined here, once implemented, +stabilized, documented, and landed in a stable release, do not provide any +value to Ember users if no one build addons that take advantage of them. + +On the other end of the spectrum, there's the risk that people build _too many_ addons +using these primitives, causing fragmentation and confusion in the Ember ecosystem. + +There's also a risk that people find single-file component addons extremely +productive and they become popular among "pro users," but new Ember learners +aren't aware of them. Once these ideas are validated and shown to be an +improvement, it's important they are incorporated into the happy path so we +can climb the mountain together. + +### Relies on Runtime Behavior + +While this proposal tries to be strict about reducing the dynamism around how +component templates are specified, it still relies on having objects like +template factories available to imperatively annotate at runtime. + +If and when we add support for Glimmer's AOT bytecode compilation mode, these +objects will no longer exist, and the strategy of having addons simply +compile the template portion of single-file components into an inline wire +format will no longer work. + +While I don't consider this a blocker, it does mean that when the time comes, +existing SFC addons would likely need to be updated to support AOT +compilation, unless we can come up with a clever compatibility hack. + +## Alternatives + +### Providing Scope as POJO + +As currently proposed in this RFC, configuring the ambient scope for a template is a two-step process: + +1. Provide additional identifiers as an array of strings to `precompile` at build time. +2. Provide a corresponding array of values to `createTemplateFactory` at run time. + +The additional coordination required between these two environments adds +complexity for addons wanting to use these APIs. + +An alternative considered was to specify scope as an object at runtime, where +the keys are the identifiers and the values are also the scope values. This +scope object would only need to be provided to `createTemplateFactory`, and +no coordination with `precompile` would be required. + +Ultimately, I opted for the API described in this RFC for two reasons: + +1. Requiring the ambient scope at build time allows the compiler to detect + invalid identifiers and raise an early error, which is a much better + experience for developers than waiting for a runtime error to happen. +2. The array form can minify more aggressively than the object form where keys + must be preserved. + +For an example of the minification impact, consider the following example: + +```js +import ComponentA from './ComponentA'; +import ComponentB from './ComponentB'; +import ComponentC from './ComponentC'; + +// Scope as array +createTemplateFactory(json, { + scope: () => [ComponentA, ComponentB, ComponentC] +}) + +// Scope as hash +createTemplateFactory(json, { + scope: () => ({ ComponentA, ComponentB, ComponentC }) +}) +``` + +With tools like Rollup and Uglify, the array-based API allows for more +aggressive reduction in file size. In my contrived example, minification +reduced byte size by about 72% with the array-based API and only about 57% +with the object-based API: + +```js +// Array +!function(){"use strict";var c={},s={},t={};l({scope:()=>[c,s,t]})}(); +// Object +!function(){"use strict";var n={},o={},t={};json,l({scope:()=>({ComponentA:n,ComponentB:o,ComponentC:t})})}(); +``` + +### Additional Resolver-Based APIs + +One alternative to the API propsed here (where we explicitly attach a +template to a component class) would be to provide better hooks into the +resolver system so that addons can better simulate single-file components via +the existing dynamic resolution system. + +I rejected this approach for a number of reasons. First, the current resolver +already has quite a bit of branching and dynamic behavior, often for +backwards compatibility. Adding additional functionality and logic to this system +did not seem like a great option. + +Second, I believe the API proposed in this RFC is much simpler for addon +authors to understand and implement if they want to experiment with +single-file component file formats. More dynamic systems can also add more +layers of indirection that are difficult to understand and debug. + +Third, and most importantly, the static nature of the API as proposed here +makes it much easier to understand at build time which components and +templates go together, and what the dependencies between components are. The +ability to quickly and accurately determine this dependency graph is +imperative to the success of initiatives like Embroider and is required to +unlock good trees-haking and code-splitting. + +## Unresolved questions + +> Optional, but suggested for first drafts. What parts of the design are still +TBD? From 098f41d1a70a065bc3b1ce9d871d02bda61da128 Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Sat, 23 Feb 2019 00:03:44 -0500 Subject: [PATCH 2/7] Fix typo --- text/000-sfc-and-template-import-primitives.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/000-sfc-and-template-import-primitives.md b/text/000-sfc-and-template-import-primitives.md index 1708373fdb..2473918692 100644 --- a/text/000-sfc-and-template-import-primitives.md +++ b/text/000-sfc-and-template-import-primitives.md @@ -671,7 +671,7 @@ makes it much easier to understand at build time which components and templates go together, and what the dependencies between components are. The ability to quickly and accurately determine this dependency graph is imperative to the success of initiatives like Embroider and is required to -unlock good trees-haking and code-splitting. +unlock good tree-shaking and code-splitting. ## Unresolved questions From 440368ccc5cccfd1673fc454d807b9f6f64779ec Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Mon, 25 Feb 2019 17:28:22 -0500 Subject: [PATCH 3/7] Fill in start date and RFC PR link --- text/000-sfc-and-template-import-primitives.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/000-sfc-and-template-import-primitives.md b/text/000-sfc-and-template-import-primitives.md index 2473918692..6d4eb0053f 100644 --- a/text/000-sfc-and-template-import-primitives.md +++ b/text/000-sfc-and-template-import-primitives.md @@ -1,6 +1,6 @@ -- Start Date: (fill me in with today's date, YYYY-MM-DD) +- Start Date: 2019-02-22 - Relevant Team(s): Ember.js -- RFC PR: (after opening the RFC PR, update this with a link to it and update the file name) +- RFC PR: https://github.com/emberjs/rfcs/pull/454 - Tracking: (leave this empty) # SFC & Template Import Primitives From a095ae59e34c51f2db8b613e714045e1aef7211c Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Mon, 25 Feb 2019 17:29:14 -0500 Subject: [PATCH 4/7] Update default scope behavior, add mode support --- .../000-sfc-and-template-import-primitives.md | 418 +++++++++++------- 1 file changed, 268 insertions(+), 150 deletions(-) diff --git a/text/000-sfc-and-template-import-primitives.md b/text/000-sfc-and-template-import-primitives.md index 6d4eb0053f..20bf7a7626 100644 --- a/text/000-sfc-and-template-import-primitives.md +++ b/text/000-sfc-and-template-import-primitives.md @@ -205,15 +205,14 @@ import BlogPost from './components/blog-post'; import { createTemplateFactory, setComponentTemplate } from '@ember/template-factory'; -const templateJSON$1 = { +const wireFormat$1 = { "id": "ANJ73B7b", "block": "{\"statements\":[\"...\"]}", - "meta": { "moduleName": "src/ui/components/MyComponent.js" } + "meta": { "moduleName": "src/ui/components/MyComponent.js" }, + "scope": () => [BlogPost, titleize] }; -const template$1 = createTemplateFactory(templateJSON$1, { - scope: () => [BlogPost, titleize] -}); +const template$1 = createTemplateFactory(wireFormat$1); export default setComponentTemplate(template$1, class MyComponent extends Component { @service session; @@ -224,7 +223,7 @@ Note that the file formats shown above are hypothetical and used as illustrative examples only. This RFC does *not* propose a recommended file format. Rather, the goal is to expose public JavaScript API for: -1. Compiling a template into JSON. +1. Compiling a template into a JavaScript wire format. 3. Associating a compiled template with its backing JavaScript class. 2. Specifying which values are in scope for a given template at runtime. @@ -240,8 +239,8 @@ This RFC proposes the following new modules and exports: Name | Description ---------|---------- -`import { precompile } from '@ember/template-compiler'` | Compiles template source code into a JSON wire format. -`import { createTemplateFactory } from '@ember/template-factory'` | Creates a template factory from wire format JSON, provides scope values. +`import { precompile } from '@ember/template-compiler'` | Compiles template source code into a JavaScript wire format. +`import { createTemplateFactory } from '@ember/template-factory'` | Creates a template factory wrapper around the wire format. `import { setComponentTemplate } from '@ember/template-factory'` | Associates a template factory with a JavaScript component class. Detailed descriptions and rationales for these APIs are provided below. @@ -260,7 +259,7 @@ the first time a template is rendered. Ember uses Glimmer's JIT mode, so templates are sent to the browser as an optimized JSON data structure we call the _wire format_. The `@glimmer/compiler` package provides a helper function called `precompile` -that turns raw template source code into this "pre-compiled" JSON wire format. +that turns raw template source code into this "pre-compiled" wire format. ```js import { precompile } from '@glimmer/compiler'; @@ -279,28 +278,28 @@ const json = precompile(`

{{this.firstName}}

`); The exact structure of the wire format returned from `precompile` is not specified, is not considered public API, and is likely to change across minor -versions. Other than a guarantee that it is a string that can be safely -parsed via `JSON.stringify`, users should treat the value returned by -`precompile` as completely opaque. +versions. Other than a guarantee that it is a string that can be embedded in +JavaScript and is a valid JavaScript expression, users should treat the value +returned by `precompile` as completely opaque. ### Template Factories Once we have our template compiled into the wire format, we need a way to -annotate it so that Ember knows that the JSON value represents a compiled -template. The wrapper object around the wire format JSON that does this is -called a _template factory_. +annotate it so that Ember knows that the value represents a compiled +template. The wrapper object around the wire format that does this is called +a _template factory_. This RFC proposes exposing a function for creating template factories called `createTemplateFactory`, exported from the `@ember/template-factory` package. -Users would use this function to turn the raw, wire format JSON into a template +Users would use this function to turn the raw wire format into a template factory object at runtime: ```js import { createTemplateFactory } from '@ember/template-factory'; -const json = /* wire format JSON here */ -const templateFactory = createTemplateFactory(json); +const wireFormat = /* embed wire format from precompile here */ +const templateFactory = createTemplateFactory(wireFormat); ``` ### Associating Template Factories and Classes @@ -352,8 +351,8 @@ arguments: import Component from '@glimmer/component'; import { createTemplateFactory, setComponentTemplate } from '@ember/template-factory'; -const json = /* wire format JSON here */ -const templateFactory = createTemplateFactory(json); +const wireFormat = /* wire format here */ +const templateFactory = createTemplateFactory(wireFormat); class MyComponent extends Component { /* ... */ @@ -403,87 +402,149 @@ superclass. In this context, _scope_ refers to the set of names that can be used to refer to values, helpers, and other components from within your template. For example, when you type `{{@firstName}}`, `{{this.count}}`, or ``, how does Ember (or even you, as the programmer) figure out what each of these -refers to? The answer to that is determined by the template's scope. +/>`, how does Ember know what each of these refers to? The answer to that is +determined by the template's scope. Today, the names available inside a particular template are determined by a few different rules: -1. `this` always refers to the component instance (if the template has a backing class). -2. Arguments (like `@firstName`) are placed into scope when you invoke the component. -3. Block parameters (like `item` in `{{#each @items as |item|}}`) are in scope, but only inside the block. -4. Anything else must be looked up dynamically through Ember's container. - This lookup process is somewhat complex and not always as fast as we'd like. +1. `this` always refers to the component instance (if the template has a + backing class). +2. Arguments (like `@firstName`) are placed into scope when you invoke the + component. +3. Block parameters (like `item` in `{{#each @items as |item|}}`) are in + scope, but only inside the block. +4. Anything else, like components and helper names, must be looked up + dynamically through Ember's container. This lookup process is somewhat + complex and not always as fast as we'd like. -Because the rules of how components, helpers, and other values are looked up -can be confusing for new learners, we'd like to explore alternate APIs that -allow users to explicitly import them and refer to them from templates. +Because the rules for how components and helpers are looked up are implicit +and can be confusing for new learners, we'd like to explore alternate APIs +that allow users to explicitly import values and refer to them from +templates. -Today, it's not easy for addons to add additional names to a template's scope -that take precedence over dynamic resolution. +This RFC proposes that templates compiled via the `precompile` function +resolve unknown identifiers from the ambient JavaScript scope, rather than +performing dynamic resolution through the container. -This RFC proposes that the `precompile` function, described earlier, also -accepts a `scope` option that specifies additional identifiers (as strings) -that are available in the template's scope: +To do this, `precompile` emits JavaScript code that contains references to +the same identifiers as those used in the template: ```js import { precompile } from '@ember/template-compiler'; -const json = precompile(`{{t "Hello!"}} `, { - scope: ['User', 't'] -}); +const wireFormat = precompile(`{{t "Hello!"}} `); +/* => `{ + "id": "ANJ73B7b", + "block": "{\"statements\":[\"...\"]}", + "scope": () => [t, User] +}` ^---- template identifiers emitted as JavaScript references +*/ ``` -The `scope` option is an array of zero or more strings. In this example, the -identifiers `User` and `t` are added to the template scope. Identifiers -provided this way are in scope for the entire template (unless shadowed by a -block argument). +If a template references an identifier that is not in lexical +scope, a `ReferenceError` will be produced at runtime: + +```js +import { precompileTemplate } from '@ember/template-compilation'; + +// Compiles successfully but the generated code will throw a ReferenceError when +// evaluated. +const wireFormat = precompile(``); +/* => `{ + "id": "ANJ73B7b", + "block": "{\"statements\":[\"...\"]}", + "scope": () => [MissingComponent] +}` ^------ will produce ReferenceError +*/ +``` -This RFC further proposes that the `createTemplateFactory` function, -described earlier, also accepts a `scope` option that returns the reified -scope values at runtime: +Optionally, you can provide an array of strings specifying the ambient +identifiers in scope. If a `scope` array is provided and the template +contains an identifier that is not present in the list, that reference will +fall back to today's dynamic resolution lookup behavior: ```js -import { createTemplateFactory } from '@ember/template-factory'; -import User from './OtherComponent'; -import t from '../helpers/i18n/t'; +// While `t` comes from ambient JavaScript scope, `User` will be looked up +// through the container. +const wireFormat = precompile(`{{t "Hello!"}} `, { + mode: 2, + scope: ['t'] +}); /* => `{ + "id": "ANJ73B7b", + "block": "{\"statements\":[\"...\"]}", + "scope": () => [t] +}` ^----- only includes identifiers specified in `scope` +*/ +``` -const json = /* wire format JSON here */ -const template = createTemplateFactory(json, { - scope: () => [User, t] -}); +```js +import { precompile } from '@ember/template-compilation'; + +const wireFormat = precompile(``, { + scope: ['BlogPost', 'OtherComponent'] +}) +/* => Throws an exception: "MissingComponent is not defined." */ ``` -Note that, unlike with `precompile`, the `scope` option here is not an array -but a function that _returns_ an array. This allows the array to be created -after the JavaScript module has finished evaluating, ensuring the bindings -included in the array have had time to be fully initialized. - -The order of the array is unimportant, other than that the same order must be -preserved between the identifiers passed to `precompile` and the -corresponding values passed to `createTemplateFactory`. - -Values provided to the template scope must be constant. Once the `scope` array has been created, -mutation of those values is unsupported and may cause unexpected errors -during render. Future RFCs may introduce support for scope values that change -over time. - -Scope values are also limited to component classes and template helpers (i.e. -subclasses of `Helper` or functions wrapped with `helper()` from -`@ember/component/helper`). However, support for additional kinds of scope -values may be expanded in future RFCs. - -If any of the following conditions are met, a runtime exception will be thrown: - -* The `scope` function returns a value that is not an array. -* The `scope` function returns an array containing a value that is not a - component class, helper, or `undefined`. -* The `scope` function returns an array whose length is different than - the `scope` array provided to `precompile`. -* A `scope` was provided when calling `precompile`, but is not provided when - calling `createTemplateFactory` with the resulting wire format. -* A `scope` was _not_ provided when calling `precompile`, and _is_ provided when - calling `createTemplateFactory` with the resulting wire format. +Addons that need to support existing templates that rely on container lookup +to discover components and helpers should provide the list of valid +identifiers via `scope`. Omitting this list causes all lookups to happen via +ambient scope rather than through the container. + +As of this RFC, scope values resolved via ambient scope are limited to +component classes and template helpers (i.e. subclasses of `Helper` or +functions wrapped with `helper()` from `@ember/component/helper`). However, +support for additional kinds of scope values may be expanded in future RFCs. + +### Compilation Modes + +To support future evolution of template syntax and semantics, this RFC +defines a `mode` option that can specify different rules for template +compilation. + +This RFC defines a single mode: + + Mode | ID | Description +---------|--------------|------------ +**Classic** | 1 | All deprecated and non-deprecated syntax supported. Mix of dynamic resolution and JavaScript scope supported. + +Modes are specified as an integer ID to reduce the size of compiled output +and to allow additional modes to be added in the future. To specify the mode, +pass a `mode` option to `precompile`: + +```js +const wireFormat = precompile(`{{t "Hello!"}} `, { + mode: 1, + scope: ['t'] +}); /* => `{ + "id": "ANJ73B7b", + "block": "{\"statements\":[\"...\"]}", + "scope": () => [t] +}` +*/ +``` + +If the mode is not specified, classic mode (mode `1`) is assumed. Future RFCs +may specify additional modes. + +In strict mode, templates that references names that are not present in a supplied +`scope` array produce a compile-time error. In sloppy mode, references to names +that are not specified in the `scope` array will fall back to dynamic +resolution: + +```js +// `t` comes from ambient scope, `User` will be looked up. +const wireFormat = precompile(`{{t "Hello!"}} `, { + mode: 2, + scope: ['t'] +}); /* => `{ + "id": "ANJ73B7b", + "block": "{\"statements\":[\"...\"]}", + "scope": () => [t] +}` ^----- only includes identifiers specified in `scope` +*/ +``` ## How we teach this @@ -507,22 +568,34 @@ import { precompile } from '@ember/template-compiler'; ``` Compiles a string of Glimmer template source code into an intermediate "wire -format" and returns it as a string. The exact value returned from -`precompile` is not specified, is not considered public API, and is likely to -change across minor versions. Other than the fact that it is guaranteed to be -a string that may be safely parsed by `JSON.parse()`, you should treat the -value returned by `precompile` as completely opaque. - -Optionally, an array of additional identifiers can be specified that will be -included in the template's ambient scope. Each identifier must have a -corresponding value provided at run time when the template's template factory -is created. +format" and returns it as a string of JavaScript code. The exact value +returned from `precompile` is not specified, is not considered public API, +and is likely to change across minor versions. The string is guaranteed to be +a valid JavaScript expression. Other than that, you should treat the value +returned by `precompile` as completely opaque. + +By default, components and helpers referenced in the passed template will be +looked up from the JavaScript scope where the wire format string is inserted. +To enable legacy container resolution behavior, a `scope` option can be +provided containing a list of identifiers that should be considered to come +from ambient JavaScript scope. Any identifiers in the template that are not +present in the `scope` list will fall back to dynamic container resolution at +runtime. + +An optional `mode` option may also be provided. Currently, only one mode is +supported, which is the classic mode with ID of `1`. If no mode is specified, +the default mode of `1` is assumed. Future RFCs may specify additional modes. ```ts function precompile(templateSource: string, options?: PrecompileOptions): string; interface PrecompileOptions { scope?: string[]; + mode?: PrecompileMode; +} + +enum PrecompileMode { + Classic = 1; } ``` @@ -539,13 +612,7 @@ provided when calling `createTemplateFactory` that returns an array of reified values corresponding to each identifier. ```ts -function createTemplateFactory(wf: WireFormatJSON, options?: CreateTemplateFactoryOptions): TemplateFactory; - -interface CreateTemplateFactoryOptions { - scope?: () => ScopeValue[]; -} - -type ScopeValue = ComponentFactory | HelperFactory | HelperFunction | undefined; +function createTemplateFactory(wf: WireFormat): TemplateFactory; ``` #### setComponentTemplate @@ -596,58 +663,17 @@ compilation, unless we can come up with a clever compatibility hack. ## Alternatives -### Providing Scope as POJO - -As currently proposed in this RFC, configuring the ambient scope for a template is a two-step process: - -1. Provide additional identifiers as an array of strings to `precompile` at build time. -2. Provide a corresponding array of values to `createTemplateFactory` at run time. - -The additional coordination required between these two environments adds -complexity for addons wanting to use these APIs. - -An alternative considered was to specify scope as an object at runtime, where -the keys are the identifiers and the values are also the scope values. This -scope object would only need to be provided to `createTemplateFactory`, and -no coordination with `precompile` would be required. - -Ultimately, I opted for the API described in this RFC for two reasons: - -1. Requiring the ambient scope at build time allows the compiler to detect - invalid identifiers and raise an early error, which is a much better - experience for developers than waiting for a runtime error to happen. -2. The array form can minify more aggressively than the object form where keys - must be preserved. - -For an example of the minification impact, consider the following example: +### Providing Scope Explicitly -```js -import ComponentA from './ComponentA'; -import ComponentB from './ComponentB'; -import ComponentC from './ComponentC'; +As currently proposed in this RFC, template identifiers are looked up from +ambient scope, with an optional fallback to dynamic resolution behavior. -// Scope as array -createTemplateFactory(json, { - scope: () => [ComponentA, ComponentB, ComponentC] -}) +An alternative considered was to specify scope values explicitly when +creating the template factory. However, this adds boilerplate that is +unneeded in the most common scenarios. -// Scope as hash -createTemplateFactory(json, { - scope: () => ({ ComponentA, ComponentB, ComponentC }) -}) -``` - -With tools like Rollup and Uglify, the array-based API allows for more -aggressive reduction in file size. In my contrived example, minification -reduced byte size by about 72% with the array-based API and only about 57% -with the object-based API: - -```js -// Array -!function(){"use strict";var c={},s={},t={};l({scope:()=>[c,s,t]})}(); -// Object -!function(){"use strict";var n={},o={},t={};json,l({scope:()=>({ComponentA:n,ComponentB:o,ComponentC:t})})}(); -``` +In favor of the extra boilerplate, addon authors can inject new bindings into +JavaScript scope to customize template resolution. ### Additional Resolver-Based APIs @@ -675,5 +701,97 @@ unlock good tree-shaking and code-splitting. ## Unresolved questions -> Optional, but suggested for first drafts. What parts of the design are still -TBD? +### Intermediate Wire Format + +Given that the wire format is unstable between different versions of Ember, +it's important that all templates in an app (including those from addons) be +compiled together. + +One of [Embroider's](https://github.com/embroider-build/embroider) goals is +ensuring that addons can compile their assets when the addon is published to +npm, rather than having compilation happen over and over again every time the +host Ember application is built. + +We want an intermediate format that would allow addons to distribute +components authored with SFCs and template imports, but would defer final +template compilation to the wire format until the application was is +compiled. + +## Changelog + +This section captures substantive changes made during the RFC process and the +rationale behind the changes. + +### 2019/2/25 + +#### Simpler Scope API + +The details of how scope works have changed to better align with the planned +"strict mode" RFC for templates, and to reduce the boilerplate required in +most common scenarios. + +Previously, setting a template's scope was a two-step process: + +1. Specifying the identifiers during the `precompile` phase. +2. Specifying the associated values of the specifiers during the + `createTemplateFactory` phase. + +The API looked like this: + +```js +import { precompile } from '@ember/template-compiler'; +import { createTemplateFactory } from '@ember/template-factory'; +import User from './OtherComponent'; +import t from '../helpers/i18n/t'; + +// Provide array of string identifiers +const wireFormat = precompile(`{{t "Hello!"}} `, { + scope: ['User', 't'] +}); + +// Provide array of values +const template = createTemplateFactory(wireFormat, { + scope: () => [User, t] +}); +``` + +This API has been updated so that, by default, the expression returned from +`precompile` is assumed to be embedded in JavaScript code where the +identifiers used in the template are in lexical scope. This eliminates the +need to specify a `scope` array to either `precompile` or +`createTemplateFactory`. + +The example above would instead be written like this: + +```js +import { precompile } from '@ember/template-compiler'; +import { createTemplateFactory } from '@ember/template-factory'; +import User from './OtherComponent'; +import t from '../helpers/i18n/t'; + +// Assume any ambiguous identifiers come from our lexical scope. +const wireFormat = precompile(`{{t "Hello!"}} `); +const template = createTemplateFactory(wireFormat); +``` + +That means that the wire format can now contain JavaScript code, like in the +following example: + +```js +import { precompileTemplate } from '@ember/template-compilation'; +import User from './OtherComponent'; +import t from '../helpers/i18n/t'; + +// Provide array of string identifiers +const wireFormat = precompile(`{{t "Hello!"}} `); +/* => `{ + "id": "ANJ73B7b", + "block": "{\"statements\":[\"...\"]}", + "scope": () => [t, User] +}` */ +``` + +#### Mode Option + +A new `mode` option has been added to `precompile` to allow for future +evolution of compiler rules. \ No newline at end of file From 5aeb3d84bc70d826592b1e42644b572d1ff99c7b Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Tue, 26 Feb 2019 16:01:30 -0500 Subject: [PATCH 5/7] Re-orient around embeddable templates --- .../000-sfc-and-template-import-primitives.md | 441 ++++++++++-------- 1 file changed, 259 insertions(+), 182 deletions(-) diff --git a/text/000-sfc-and-template-import-primitives.md b/text/000-sfc-and-template-import-primitives.md index 20bf7a7626..458e85678c 100644 --- a/text/000-sfc-and-template-import-primitives.md +++ b/text/000-sfc-and-template-import-primitives.md @@ -203,16 +203,13 @@ import { inject as service } from '@ember/service'; import titleize from './helpers/titleize'; import BlogPost from './components/blog-post'; -import { createTemplateFactory, setComponentTemplate } from '@ember/template-factory'; +import { __template__, setComponentTemplate } from '@ember/template-factory'; -const wireFormat$1 = { - "id": "ANJ73B7b", - "block": "{\"statements\":[\"...\"]}", - "meta": { "moduleName": "src/ui/components/MyComponent.js" }, - "scope": () => [BlogPost, titleize] -}; - -const template$1 = createTemplateFactory(wireFormat$1); +const template$1 = __template__({ + source: `{{#let this.session.currentUser as |user|}}\n {{this.firstName}}`); -``` +While the wire format allows us to embed templates as a JavaScript data +structure, the format of that data structure changes over time. For example, +the wire format of a compiled template is likely to be slightly different +(and mutually incompatible) between Ember 3.0 and 3.8. -There is some additional processing Ember does on top of the Glimmer VM -compiler to allow compiled templates to work. This RFC proposes exposing this -functionality as public API via a `precompile` function exported from the -`@ember/template-compiler` package. +Today, addons distribute templates as source `.hbs` files. Each Ember +application is responsible for compiling addon templates as well as its own +templates, guaranteeing that the generated wire formats are compatible. -```js -import { precompile } from '@ember/template-compiler'; -const json = precompile(`

{{this.firstName}}

`); -``` +Because the wire format is not stable across Ember versions, we need an +alternate syntax for embedding templates within JavaScript code that is +stable across different Ember releases. -The exact structure of the wire format returned from `precompile` is not -specified, is not considered public API, and is likely to change across minor -versions. Other than a guarantee that it is a string that can be embedded in -JavaScript and is a valid JavaScript expression, users should treat the value -returned by `precompile` as completely opaque. +### Embeddable Templates -### Template Factories +This RFC proposes we expose a function called `createEmbeddableTemplate`, +exported from the `@ember/template-compiler` package. This function takes a +string of template source code and returns a version of that template encoded +in JavaScript syntax that can be inserted into an existing JavaScript file: -Once we have our template compiled into the wire format, we need a way to -annotate it so that Ember knows that the value represents a compiled -template. The wrapper object around the wire format that does this is called -a _template factory_. +```js +import { createEmbeddableTemplate } from '@ember/template-compiler'; -This RFC proposes exposing a function for creating template factories called -`createTemplateFactory`, exported from the `@ember/template-factory` package. +const templateSource = `

`; +const embeddableTemplate = createEmbeddableTemplate(templateSource); -Users would use this function to turn the raw wire format into a template -factory object at runtime: +const jsFile = ` +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import titleize from './helpers/titleize'; +import BlogPost from './components/blog-post'; +import { setComponentTemplate } from '@ember/template-factory'; +${embeddableTemplate.prelude}; -```js -import { createTemplateFactory } from '@ember/template-factory'; +const template$1 = ${embeddableTemplate.value}; -const wireFormat = /* embed wire format from precompile here */ -const templateFactory = createTemplateFactory(wireFormat); +export default setComponentTemplate(template$1, class MyComponent extends Component { + @service session; +}); +`; ``` -### Associating Template Factories and Classes +The return value of `createEmbeddableTemplate` is an object with two +properties, `prelude` and `value`. + +The `prelude` property is a string containing setup code, such as `import` statements, +and must be inserted in the top level of the module. If multiple templates +are being embedded in the same JavaScript module, the prelude must be +included only once. + +The `value` property is a string containing a JavaScript expression +representing the template source and additional metadata. This value will be +transformed into the final wire format version of the template when the +application is compiled. + +The proposed syntax for templates encoded in JavaScript is described in the +[Embeddable Template Format](#embeddable-template-format) section. However, +the exact JavaScript code returned from `createEmbeddableTemplate` should be +considered opaque and may change over time. + +The `createEmbeddableTemplate` function will throw an exception if the +template passed to it contains a syntax error. + +### Associating Templates and Classes In Ember, a component is made up of a template and an optional backing JavaScript class. But how does Ember know which class and template go @@ -344,15 +362,14 @@ being fully optimized. This RFC proposes a more explicit, static system for associating templates with component classes. A function called `setComponentTemplate` is exported from the `@ember/template-factory` package. To create the association, this -function is invoked with the template factory and component class as +function is invoked with the embedded template and component class as arguments: ```js import Component from '@glimmer/component'; -import { createTemplateFactory, setComponentTemplate } from '@ember/template-factory'; +import { setComponentTemplate } from '@ember/template-factory'; -const wireFormat = /* wire format here */ -const templateFactory = createTemplateFactory(wireFormat); +const template = /* embeddable template here */ class MyComponent extends Component { /* ... */ @@ -423,75 +440,53 @@ and can be confusing for new learners, we'd like to explore alternate APIs that allow users to explicitly import values and refer to them from templates. -This RFC proposes that templates compiled via the `precompile` function -resolve unknown identifiers from the ambient JavaScript scope, rather than -performing dynamic resolution through the container. +This RFC proposes that templates embedded via the `createEmbeddableTemplate` +function resolve unknown identifiers from the ambient JavaScript scope, +rather than performing dynamic resolution through the container. -To do this, `precompile` emits JavaScript code that contains references to -the same identifiers as those used in the template: +To do this, `createEmbeddableTemplate` emits JavaScript code that contains +references to the same identifiers as those used in the template: ```js -import { precompile } from '@ember/template-compiler'; +import { t } from '../helpers/t'; +import User from './user'; +import { __template__ } from '@ember/template-compiler'; -const wireFormat = precompile(`{{t "Hello!"}} `); -/* => `{ - "id": "ANJ73B7b", - "block": "{\"statements\":[\"...\"]}", - "scope": () => [t, User] -}` ^---- template identifiers emitted as JavaScript references -*/ +const template$1 = __template__({ + source: '{{t "Hello!"}} ', + mode: 1, + scope: [User, t] +}); /* ^------- actual JavaScript identifier */ ``` -If a template references an identifier that is not in lexical -scope, a `ReferenceError` will be produced at runtime: +When the embedded template is later compiled into the wire format, these references +are retained: ```js -import { precompileTemplate } from '@ember/template-compilation'; +import { t } from '../helpers/t'; +import User from './user'; +import { templateFactory } from '@ember/template-factory'; -// Compiles successfully but the generated code will throw a ReferenceError when -// evaluated. -const wireFormat = precompile(``); -/* => `{ +const template$1 = templateFactory({ "id": "ANJ73B7b", "block": "{\"statements\":[\"...\"]}", - "scope": () => [MissingComponent] -}` ^------ will produce ReferenceError -*/ + "scope": () => [User, t] +}); /* ^------- actual JavaScript identifier */ ``` -Optionally, you can provide an array of strings specifying the ambient -identifiers in scope. If a `scope` array is provided and the template -contains an identifier that is not present in the list, that reference will -fall back to today's dynamic resolution lookup behavior: +If a template references an identifier that is not in scope, a +`ReferenceError` will be produced at runtime: ```js -// While `t` comes from ambient JavaScript scope, `User` will be looked up -// through the container. -const wireFormat = precompile(`{{t "Hello!"}} `, { - mode: 2, - scope: ['t'] -}); /* => `{ +import { templateFactory } from '@ember/template-factory'; + +const template$1 = templateFactory({ "id": "ANJ73B7b", "block": "{\"statements\":[\"...\"]}", - "scope": () => [t] -}` ^----- only includes identifiers specified in `scope` -*/ -``` - -```js -import { precompile } from '@ember/template-compilation'; - -const wireFormat = precompile(``, { - scope: ['BlogPost', 'OtherComponent'] -}) -/* => Throws an exception: "MissingComponent is not defined." */ + "scope": () => [MissingComponent] +}); /* ^------- will produce a ReferenceError */ ``` -Addons that need to support existing templates that rely on container lookup -to discover components and helpers should provide the list of valid -identifiers via `scope`. Omitting this list causes all lookups to happen via -ambient scope rather than through the container. - As of this RFC, scope values resolved via ambient scope are limited to component classes and template helpers (i.e. subclasses of `Helper` or functions wrapped with `helper()` from `@ember/component/helper`). However, @@ -507,45 +502,134 @@ This RFC defines a single mode: Mode | ID | Description ---------|--------------|------------ -**Classic** | 1 | All deprecated and non-deprecated syntax supported. Mix of dynamic resolution and JavaScript scope supported. +**Strict** | 1 | Legacy template syntax becomes a compile-time error. Component and helper resolution happens via ambient JavaScript scope. + +The exact behavior of strict mode is defined in a companion RFC. Modes are specified as an integer ID to reduce the size of compiled output and to allow additional modes to be added in the future. To specify the mode, -pass a `mode` option to `precompile`: +pass a `mode` option to `createEmbeddableTemplate`: ```js -const wireFormat = precompile(`{{t "Hello!"}} `, { - mode: 1, - scope: ['t'] -}); /* => `{ - "id": "ANJ73B7b", - "block": "{\"statements\":[\"...\"]}", - "scope": () => [t] -}` -*/ +const template = createEmbeddableTemplate('
', { + mode: 1 +}); ``` -If the mode is not specified, classic mode (mode `1`) is assumed. Future RFCs +If the mode is not specified, strict mode (mode `1`) is assumed. Future RFCs may specify additional modes. -In strict mode, templates that references names that are not present in a supplied -`scope` array produce a compile-time error. In sloppy mode, references to names -that are not specified in the `scope` array will fall back to dynamic -resolution: +### Embeddable Template Format + +This section describes the JavaScript syntax generated by +`createEmbeddableTemplate` and consumed by the Ember CLI build system. +Similar to how Ember CLI automatically discovers `.hbs` files and compiles +them into the wire format, templates embedded in `.js` files with this syntax +will be replaced with the equivalent wire format at build time. + +The following constraints are important to the design described here: + +1. Parsing for embedded templates should be fast, to avoid regressing application + build times. +2. The syntax should be static and only require parsing, not evaluating, JavaScript. +3. The syntax should be valid JavaScript that will not cause incompatibility + with other tools. +4. The syntax should produce real values and use real JavaScript references + rather than relying on "inert" constructs like strings or comments. This + is to make the embedded templates resilient to tools like minifiers or + packagers that aggressively remove code that seems unused and without + side-effects. + +The format described here is not intended to be authored by hand. Addons and +other tools that need to generate embeddable templates are strongly +encouraged to use the `createEmbeddableTemplate` function rather than +generating the format manually. + +We propose a new export, `__template__`, from the `@ember/template-compiler` +package. An embedded template is defined by a `CallExpression` with the +imported `__template__` identifier (or its alias) as the callee and an +`ObjectLiteral` expression as the first and only argument: ```js -// `t` comes from ambient scope, `User` will be looked up. -const wireFormat = precompile(`{{t "Hello!"}} `, { - mode: 2, - scope: ['t'] -}); /* => `{ +import titleize from './helpers/titleize'; +import BlogPost from './components/blog-post'; +import { __template__ } from '@ember/template-compiler'; + +const template$1 = __template__({ + source: '

', + mode: 1, + scope: [BlogPost, titleize] +}); +``` + +The `ObjectLiteral` expression must contain the following property definitions: + +| `IdentifierName` | Description | `AssignmentExpression` | +|---|---|---| +| `source` | The original template source. | `StringLiteral` | +| `mode` | The mode (see [Compilation Modes](#compilation-modes)) the template should be compiled in. | `DecimalIntegerLiteral` | +| `scope` | Identifiers used in the template, sorted by identifier name. | `ArrayLiteral` containing zero or more `IdentifierName` expressions | + +While all property definitions are required, the order is unimportant and +they may be provided in any order. + +The `scope` array must be sorted by identifier name, using the sort behavior +[described in ECMA-262][262-sort]. Predictable sorting allows the template +compiler to associate scope values with the correct template identifier, even +if JavaScript identifiers have been mangled by a minifier. + +[262-sort]: https://tc39.github.io/ecma262/#sec-array.prototype.sort + +To avoid naming collisions, the `__template__` identifier may be aliased on import: + +```js +import titleize from './helpers/titleize'; +import BlogPost from './components/blog-post'; +import { __template__ as TEMPLATE } from '@ember/template-compiler'; + +// This alias is valid and will be detected as an embedded template. +const template$1 = TEMPLATE({ + source: '

', + mode: 1, + scope: [BlogPost, titleize] +}) +``` + +Any other form of aliasing is not supported: + +```js +import titleize from './helpers/titleize'; +import BlogPost from './components/blog-post'; +import { __template__ } from '@ember/template-compiler'; + +// This alias happens after the import and will either not be detected or cause +// an exception to be thrown. +const TEMPLATE = __template__ +const template$1 = TEMPLATE({ + source: '

', + mode: 1, + scope: [BlogPost, titleize] +}) +``` + +The `__template__` `CallExpression` will be replaced with the compiled wire +format: + +```js +import titleize from './helpers/titleize'; +import BlogPost from './components/blog-post'; + +const template$1 = { "id": "ANJ73B7b", "block": "{\"statements\":[\"...\"]}", - "scope": () => [t] -}` ^----- only includes identifiers specified in `scope` -*/ + "scope": () => [BlogPost, titleize] +}; ``` +Note that the wire format is not defined in this RFC, is not considered +public API, and is subject to change over time. The example above is +illustrative only. + ## How we teach this ### Guide-level Documentation @@ -561,58 +645,44 @@ pre-requisite. ### API-Level Documentation -#### precompile +#### createEmbeddableTemplate ```js -import { precompile } from '@ember/template-compiler'; +import { createEmbeddableTemplate } from '@ember/template-compiler'; ``` -Compiles a string of Glimmer template source code into an intermediate "wire -format" and returns it as a string of JavaScript code. The exact value -returned from `precompile` is not specified, is not considered public API, -and is likely to change across minor versions. The string is guaranteed to be -a valid JavaScript expression. Other than that, you should treat the value -returned by `precompile` as completely opaque. - -By default, components and helpers referenced in the passed template will be -looked up from the JavaScript scope where the wire format string is inserted. -To enable legacy container resolution behavior, a `scope` option can be -provided containing a list of identifiers that should be considered to come -from ambient JavaScript scope. Any identifiers in the template that are not -present in the `scope` list will fall back to dynamic container resolution at -runtime. +Encodes a string of Glimmer template source code as JavaScript code that can +be embedded in a host JavaScript file. The JavaScript representation will later +be compiled into the final compiled wire format by Ember CLI. + +Returns an object with a `prelude` property and a `value` property. The +`prelude` string should be inserted into the top-level scope of the host file +and includes setup code such as import statements. The `value` string +represents a JavaScript expression and may be inserted anywhere a JavaScript +expression is syntactically valid, such as the right side of an assignment. + +Components and helpers referenced in the passed template will be +looked up from the JavaScript scope where the `value` string is inserted. An optional `mode` option may also be provided. Currently, only one mode is -supported, which is the classic mode with ID of `1`. If no mode is specified, +supported, which is strict mode with ID of `1`. If no mode is specified, the default mode of `1` is assumed. Future RFCs may specify additional modes. ```ts -function precompile(templateSource: string, options?: PrecompileOptions): string; +function createEmbeddableTemplate(templateSource: string, options?: CreateEmbeddableTemplateOptions): CreateEmbeddableTemplateResult; -interface PrecompileOptions { - scope?: string[]; - mode?: PrecompileMode; +interface CreateEmbeddableTemplateOptions { + mode?: TemplateCompilerMode; } -enum PrecompileMode { - Classic = 1; +interface CreateEmbeddableTemplateResult { + prelude: string; + value: string; } -``` - -#### createTemplateFactory - -```js -import { createTemplateFactory } from '@ember/template-factory'; -``` - -Creates a wrapper object around the raw wire format data of a compiled -template. If the template had additional identifiers added to its ambient -scope when it was compiled with `precompile`, a `scope` function must be -provided when calling `createTemplateFactory` that returns an array of -reified values corresponding to each identifier. -```ts -function createTemplateFactory(wf: WireFormat): TemplateFactory; +enum TemplateCompilerMode { + Strict = 1; +} ``` #### setComponentTemplate @@ -701,28 +771,35 @@ unlock good tree-shaking and code-splitting. ## Unresolved questions -### Intermediate Wire Format +## Changelog + +This section captures substantive changes made during the RFC process and the +rationale behind the changes. -Given that the wire format is unstable between different versions of Ember, -it's important that all templates in an app (including those from addons) be -compiled together. +### 2019/02/26 -One of [Embroider's](https://github.com/embroider-build/embroider) goals is -ensuring that addons can compile their assets when the addon is published to -npm, rather than having compilation happen over and over again every time the -host Ember application is built. +#### Replace Wire Format with Embeddable Templates -We want an intermediate format that would allow addons to distribute -components authored with SFCs and template imports, but would defer final -template compilation to the wire format until the application was is -compiled. +To address the previous open question about wire format stability and how +addons should distribute templates that exist within a JavaScript scope, the +previous `precompile` API has been replaced with a new API, +`createEmbeddableTemplate`, that creates an intermediate static syntax for +template embedding that includes the original template source. -## Changelog +#### Single Scoping Mechanism, Compilation Mode -This section captures substantive changes made during the RFC process and the -rationale behind the changes. +Previous versions of this RFC contained additional API for partially opting +out of JavaScript lexical scope in a given template. This design added +complexity to the API and the process of building an SFC addon, but was done +on compatibility grounds. We now believe we have a better strategy for +compatibility that does not rely on this complexity, so it has been removed +in favor of always using ambient scope. + +This also means that the need for a sloppy mode is eliminated, so this RFC +defines the default (and only) mode as strict mode. The exact semantics of +strict mode will be defined in a companion RFC. -### 2019/2/25 +### 2019/02/25 #### Simpler Scope API From c469ad25fd0032612d67ddcf4a5e1f1f3595bb4e Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Tue, 26 Feb 2019 16:36:01 -0500 Subject: [PATCH 6/7] Small tweaks --- text/000-sfc-and-template-import-primitives.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/text/000-sfc-and-template-import-primitives.md b/text/000-sfc-and-template-import-primitives.md index 458e85678c..2a6298a87b 100644 --- a/text/000-sfc-and-template-import-primitives.md +++ b/text/000-sfc-and-template-import-primitives.md @@ -7,8 +7,8 @@ ## Summary -Expose low-level primitives for associating templates with component classes -and customizing a template's ambient scope. +Expose low-level primitives for embedding templates in JavaScript and +associating templates with component classes. These primitives unlock experimentation, allowing addons to provide highly-requested features (such as single-file components) via stable, public @@ -266,9 +266,9 @@ Today, addons distribute templates as source `.hbs` files. Each Ember application is responsible for compiling addon templates as well as its own templates, guaranteeing that the generated wire formats are compatible. -Because the wire format is not stable across Ember versions, we need an -alternate syntax for embedding templates within JavaScript code that is -stable across different Ember releases. +Because the wire format is not stable, we need an alternate syntax for +embedding templates within JavaScript code that is stable across different +Ember versions. ### Embeddable Templates From 7978c68d17b4a9f0c3595993e068a35945bfa64a Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Tue, 26 Feb 2019 16:44:07 -0500 Subject: [PATCH 7/7] Clarify that embeddable templates are replaced with template factories --- text/000-sfc-and-template-import-primitives.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/text/000-sfc-and-template-import-primitives.md b/text/000-sfc-and-template-import-primitives.md index 2a6298a87b..2943306cb0 100644 --- a/text/000-sfc-and-template-import-primitives.md +++ b/text/000-sfc-and-template-import-primitives.md @@ -613,17 +613,18 @@ const template$1 = TEMPLATE({ ``` The `__template__` `CallExpression` will be replaced with the compiled wire -format: +format and wrapped in a template factory: ```js import titleize from './helpers/titleize'; import BlogPost from './components/blog-post'; +import { createTemplateFactory } from '@ember/template-factory'; -const template$1 = { +const template$1 = createTemplateFactory({ "id": "ANJ73B7b", "block": "{\"statements\":[\"...\"]}", "scope": () => [BlogPost, titleize] -}; +}); ``` Note that the wire format is not defined in this RFC, is not considered