ember-source
Version:
A JavaScript framework for creating ambitious web applications
249 lines (239 loc) • 8.7 kB
JavaScript
import './plugins/allowed-globals.js';
import templateOnly from '../../component/template-only.js';
import '../../../shared-chunks/fragment-Cc5k9Oy4.js';
import '../../../shared-chunks/debug-to-string-CFb7h0lY.js';
import '../../../@glimmer/wire-format/index.js';
import { p as precompile } from '../../../shared-chunks/compiler-CNj62pww.js';
import '../../../@glimmer/global-context/index.js';
import '../../../@glimmer/destroyable/index.js';
import '../../../@glimmer/validator/index.js';
import '../../../shared-chunks/reference-C3TKDRnP.js';
import '../../../shared-chunks/capabilities-O_xc7Yqk.js';
import { s as setComponentTemplate } from '../../../shared-chunks/template-kM-7TTcc.js';
import { t as templateFactory } from '../../../shared-chunks/index-Q7JnrdBn.js';
import compileOptions from './compile-options.js';
/**
* All possible options passed to `template()` may specify a `moduleName`.
*/
/**
* When using `template` in a class, you call it in a `static` block and pass
* the class as the `component` option.
*
* ```ts
* class MyComponent extends Component {
* static {
* template('{{this.greeting}}, {{@place}}!',
* { component: this },
* // explicit or implicit option goes here
* );
* }
* }
* ```
*
* For the full explicit form, see {@linkcode ExplicitClassOptions}. For the
* full implicit form, see {@linkcode ImplicitClassOptions}.
*/
/**
* When using `template` outside of a class (i.e. a "template-only component"), you can pass
* a `scope` option that explicitly provides the lexical scope for the template.
*
* This is called the "explicit form".
*
* ```ts
* const greeting = 'Hello';
* const HelloWorld = template('{{greeting}} World!', { scope: () => ({ greeting }) });
* ```
*/
/**
* When using `template` *inside* a class (see
* {@linkcode BaseClassTemplateOptions}), you can pass a `scope` option that
* explicitly provides the lexical scope for the template, just like a template-only
* component (see {@linkcode ExplicitTemplateOnlyOptions}).
*
* ```ts
* class MyComponent extends Component {
* static {
* template('{{this.greeting}}, {{@place}}!',
* { component: this },
* // explicit or implicit option goes here
* );
* }
* }
* ```
*
* ## The Scope Function's `instance` Parameter
*
* However, the explicit `scope` function in a *class* also takes an `instance` option
* that provides access to the component's instance.
*
* Once it's supported in Handlebars, this will make it possible to represent private
* fields when using the explicit form.
*
* ```ts
* class MyComponent extends Component {
* static {
* template('{{this.#greeting}}, {{@place}}!',
* { component: this },
* scope: (instance) => ({ '#greeting': instance.#greeting }),
* );
* }
* }
* ```
*/
/**
* The *implicit* form of the `template` function takes an `eval` option that
* allows the runtime compiler to evaluate local template variables without
* needing to maintain an explicit list of the local variables used in the
* template scope.
*
* The eval options *must* be passed in the following form:
*
* ```ts
* {
* eval() { return eval(arguments[0]) }
* }
* ```
*
* ## Requirements of the `eval` Option
*
* **The syntactic form presented above is the only form you should use when
* passing an `eval` option.**
*
* This is _required_ if you want your code to be compatible with the
* compile-time implementation of `@ember/template-compiler`. While the runtime
* compiler offers a tiny bit of additional wiggle room, you still need to follow
* very strict rules.
*
* We don't recommend trying to memorize the rules. Instead, we recommend using
* the snippet presented above and supported by the compile-time implementation.
*
* ### The Technical Requirements of the `eval` Option
*
* The `eval` function is passed a single parameter that is a JavaScript
* identifier. This will be extended in the future to support private fields.
*
* Since keywords in JavaScript are contextual (e.g. `await` and `yield`), the
* parameter might be a keyword. The `@ember/template-compiler/runtime` expects
* the function to throw a `SyntaxError` if the identifier name is not valid in
* the current scope. (The direct `eval` function takes care of this out of the
* box.)
*
* Requirements:
*
* 1. The `eval` method must receive its parameter as `arguments[0]`, which
* ensures that the variable name passed to `eval()` is not shadowed by the
* function's parameter name.
* 2. The `eval` option must be a function or concise method, and not an arrow.
* This is because arrows do not have their own `arguments`, which breaks
* (1).
* 3. The `eval` method must call "*direct* `eval`", and not an alias of `eval`.
* Direct `eval` evaluates the code in the scope it was called from, while
* aliased versions of `eval` (including `new Function`) evaluate the code in
* the global scope.
* 4. The `eval` method must return the result of calling "direct `eval`".
*
* The easiest way to achieve these requirements is to use the exact syntax
* presented above. This is *also* the only way to be compatible
*
* ## Rationale
*
* This is useful for two reasons:
*
* 1. This form is a useful _intermediate_ form for the compile-time toolchain.
* It allows the content-tag preprocessor to convert the `<template>` syntax
* into valid JavaScript without needing to involve full-fledged lexical
* analysis.
* 2. This form is a convenient form for manual prototyping when using the
* runtime compiler directly. While it requires some extra typing relative to
* `<template>`, it's a mechanical 1:1 transformation of the syntax.
*
* In practice, implementations that use a runtime compiler (for example, a
* playground running completely in the browser) should probably use the
* `content-tag` preprocessor to convert the template into the implicit form,
* and then rely on `@ember/template-compiler/runtime` to evaluate the template.
*/
/**
* When using `template` outside of a class (i.e. a "template-only component"), you can pass
* an `eval` option that _implicitly_ provides the lexical scope for the template.
*
* This is called the "implicit form".
*
* ```ts
* const greeting = 'Hello';
* const HelloWorld = template('{{greeting}} World!', {
* eval() { return arguments[0] }
* });
* ```
*
* For more details on the requirements of the `eval` option, see {@linkcode ImplicitEvalOption}.
*/
/**
* When using `template` inside of a class, you can pass an `eval` option that
* _implicitly_ provides the lexical scope for the template, just as you can
* with a {@linkcode ImplicitTemplateOnlyOptions | template-only component}.
*
* This is called the "implicit form".
*
* ```ts
* class MyComponent extends Component {
* static {
* template('{{this.greeting}}, {{@place}}!',
* { component: this },
* eval() { return arguments[0] }
* );
* }
* }
* ```
*
* ## Note on Private Fields
*
* The current implementation of `@ember/template-compiler` does not support
* private fields, but once the Handlebars parser adds support for private field
* syntax and it's implemented in the Glimmer compiler, the implicit form should
* be able to support them.
*/
function template(templateString, providedOptions) {
const options = {
strictMode: true,
...providedOptions
};
const evaluate = buildEvaluator(options);
const normalizedOptions = compileOptions(options);
const component = normalizedOptions.component ?? templateOnly();
const source = precompile(templateString, normalizedOptions);
const template = templateFactory(evaluate(`(${source})`));
setComponentTemplate(template, component);
return component;
}
const evaluator = source => {
return new Function(`return ${source}`)();
};
function buildEvaluator(options) {
if (options === undefined) {
return evaluator;
}
if (options.eval) {
return options.eval;
} else {
const scope = options.scope?.();
if (!scope) {
return evaluator;
}
return source => {
let hasThis = Object.prototype.hasOwnProperty.call(scope, 'this');
let thisValue = hasThis ? scope.this : undefined;
let argNames = [];
let argValues = [];
for (let [name, value] of Object.entries(scope)) {
if (name === 'this') {
continue;
}
argNames.push(name);
argValues.push(value);
}
let fn = new Function(...argNames, `return (${source})`);
return hasThis ? fn.call(thisValue, ...argValues) : fn(...argValues);
};
}
}
export { template };