Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions packages/@ember/helper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import {
concat as glimmerConcat,
get as glimmerGet,
fn as glimmerFn,
and as glimmerAnd,
or as glimmerOr,
not as glimmerNot,
eq as glimmerEq,
neq as glimmerNeq,
lt as glimmerLt,
lte as glimmerLte,
gt as glimmerGt,
gte as glimmerGte,
} from '@glimmer/runtime';
import { uniqueId as glimmerUniqueId } from '@ember/-internals/glimmer';
import { type Opaque } from '@ember/-internals/utility-types';
Expand Down Expand Up @@ -492,3 +501,120 @@ export const uniqueId = glimmerUniqueId;
export type UniqueIdHelper = typeof uniqueId;

/* eslint-enable @typescript-eslint/no-empty-object-type */

/**
* `{{and}}` returns the first falsy value or the last value if all are truthy.
*
* ```js
* import { and } from '@ember/helper';
*
* <template>
* {{if (and this.isActive this.isVerified) "Ready" "Not ready"}}
* </template>
* ```
*/
export const and = glimmerAnd;

/**
* `{{or}}` returns the first truthy value or the last value if all are falsy.
*
* ```js
* import { or } from '@ember/helper';
*
* <template>
* {{if (or this.isAdmin this.isModerator) "Has access" "No access"}}
* </template>
* ```
*/
export const or = glimmerOr;

/**
* `{{not}}` returns the boolean negation of its argument.
*
* ```js
* import { not } from '@ember/helper';
*
* <template>
* {{if (not this.isActive) "Inactive" "Active"}}
* </template>
* ```
*/
export const not = glimmerNot;

/**
* `{{eq}}` checks strict equality (`===`) between two values.
*
* ```js
* import { eq } from '@ember/helper';
*
* <template>
* {{if (eq this.status "active") "Active" "Inactive"}}
* </template>
* ```
*/
export const eq = glimmerEq;

/**
* `{{neq}}` checks strict inequality (`!==`) between two values.
*
* ```js
* import { neq } from '@ember/helper';
*
* <template>
* {{if (neq this.status "active") "Not active" "Active"}}
* </template>
* ```
*/
export const neq = glimmerNeq;

/**
* `{{lt}}` checks if the first value is less than the second.
*
* ```js
* import { lt } from '@ember/helper';
*
* <template>
* {{if (lt this.count 10) "Under 10" "10 or more"}}
* </template>
* ```
*/
export const lt = glimmerLt;

/**
* `{{lte}}` checks if the first value is less than or equal to the second.
*
* ```js
* import { lte } from '@ember/helper';
*
* <template>
* {{if (lte this.count 10) "10 or under" "Over 10"}}
* </template>
* ```
*/
export const lte = glimmerLte;

/**
* `{{gt}}` checks if the first value is greater than the second.
*
* ```js
* import { gt } from '@ember/helper';
*
* <template>
* {{if (gt this.count 10) "Over 10" "10 or under"}}
* </template>
* ```
*/
export const gt = glimmerGt;

/**
* `{{gte}}` checks if the first value is greater than or equal to the second.
*
* ```js
* import { gte } from '@ember/helper';
*
* <template>
* {{if (gte this.count 10) "10 or more" "Under 10"}}
* </template>
* ```
*/
export const gte = glimmerGte;
50 changes: 43 additions & 7 deletions packages/@ember/template-compiler/lib/compile-options.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { on } from '@ember/modifier';
import { fn, hash, array, and, or, not, eq, neq, lt, lte, gt, gte } from '@ember/helper';
import { assert } from '@ember/debug';
import {
RESOLUTION_MODE_TRANSFORMS,
Expand All @@ -14,11 +16,30 @@ function malformedComponentLookup(string: string) {
return string.indexOf('::') === -1 && string.indexOf(':') > -1;
}

const RUNTIME_KEYWORDS_NAME = '__ember_keywords__';
export const keywords = {
on,
fn,
hash,
array,
and,
or,
not,
eq,
neq,
lt,
lte,
gt,
gte,
};

// Not worth adding a type
(globalThis as any)[RUNTIME_KEYWORDS_NAME] = keywords;

function buildCompileOptions(_options: EmberPrecompileOptions): EmberPrecompileOptions {
let moduleName = _options.moduleName;

let options: EmberPrecompileOptions & Partial<EmberPrecompileOptions> = {
meta: {},
let options = {
isProduction: false,
plugins: { ast: [] },
..._options,
Expand All @@ -35,8 +56,25 @@ function buildCompileOptions(_options: EmberPrecompileOptions): EmberPrecompileO
},
};

if ('eval' in options) {
const localScopeEvaluator = options.eval as (value: string) => unknown;
options.meta ||= {};
options.meta.emberRuntime ||= {
/**
* NOTE: when stepping through lexicalScope, or other callbacks here,
* we first detect the keywords as "not in scope",
* and that is what we want, so that we can import them.
*/
lookupKeyword(name: string): string {
assert(
`${name} is not a known keyword. Available keywords: ${Object.keys(keywords).join(', ')}`,
name in keywords
);

return `globalThis.${RUNTIME_KEYWORDS_NAME}.${name}`;
},
};

if ('eval' in options && options.eval) {
const localScopeEvaluator = options.eval;
const globalScopeEvaluator = (value: string) => new Function(`return ${value};`)();

options.lexicalScope = (variable: string) => {
Expand All @@ -57,9 +95,7 @@ function buildCompileOptions(_options: EmberPrecompileOptions): EmberPrecompileO
if ('scope' in options) {
const scope = (options.scope as () => Record<string, unknown>)();

options.lexicalScope = (variable: string) => {
return variable in scope;
};
options.lexicalScope = (variable: string) => variable in scope || variable in keywords;

delete options.scope;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { AST, ASTPlugin } from '@glimmer/syntax';
import type { EmberASTPluginEnvironment } from '../types';
import { isPath, trackLocals } from './utils';

/**
@module ember
*/

/**
A Glimmer2 AST transformation that makes importable keywords work

@private
@class AutoImportBuiltins
*/

const MODIFIER_KEYWORDS: Record<string, string> = {
on: '@ember/modifier',
};

const HELPER_KEYWORDS: Record<string, string> = {
fn: '@ember/helper',
hash: '@ember/helper',
array: '@ember/helper',
and: '@ember/helper',
or: '@ember/helper',
not: '@ember/helper',
eq: '@ember/helper',
neq: '@ember/helper',
lt: '@ember/helper',
lte: '@ember/helper',
gt: '@ember/helper',
gte: '@ember/helper',
};

export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTPlugin {
let { hasLocal, visitor } = trackLocals(env);

function rewrite(
node: AST.ElementModifierStatement | AST.MustacheStatement | AST.SubExpression,
modulePath: string,
name: string
) {
if (env.meta?.jsutils) {
(node.path as AST.PathExpression).original = env.meta.jsutils.bindImport(
modulePath,
name,
node,
{ name }
);
} else if (env.meta?.emberRuntime) {
(node.path as AST.PathExpression).original = env.meta.emberRuntime.lookupKeyword(name);
}
}

return {
name: 'auto-import-built-ins',

visitor: {
...visitor,
ElementModifierStatement(node: AST.ElementModifierStatement) {
if (!isPath(node.path) || hasLocal(node.path.original)) return;
let modulePath = MODIFIER_KEYWORDS[node.path.original];
if (modulePath) {
rewrite(node, modulePath, node.path.original);
}
},
MustacheStatement(node: AST.MustacheStatement) {
if (!isPath(node.path) || hasLocal(node.path.original)) return;
let modulePath = HELPER_KEYWORDS[node.path.original];
if (modulePath) {
rewrite(node, modulePath, node.path.original);
}
},
SubExpression(node: AST.SubExpression) {
if (!isPath(node.path) || hasLocal(node.path.original)) return;
let modulePath = HELPER_KEYWORDS[node.path.original];
if (modulePath) {
rewrite(node, modulePath, node.path.original);
}
},
},
};
}
3 changes: 3 additions & 0 deletions packages/@ember/template-compiler/lib/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import TransformInElement from './transform-in-element';
import TransformQuotedBindingsIntoJustBindings from './transform-quoted-bindings-into-just-bindings';
import TransformResolutions from './transform-resolutions';
import TransformWrapMountAndOutlet from './transform-wrap-mount-and-outlet';
import AutoImportBuiltins from './auto-import-builtins';

export const INTERNAL_PLUGINS = {
AutoImportBuiltins,
AssertAgainstAttrs,
AssertAgainstNamedOutlets,
AssertInputHelperWithoutBlock,
Expand Down Expand Up @@ -40,6 +42,7 @@ export const RESOLUTION_MODE_TRANSFORMS = Object.freeze([
]);

export const STRICT_MODE_TRANSFORMS = Object.freeze([
AutoImportBuiltins,
TransformQuotedBindingsIntoJustBindings,
AssertReservedNamedArguments,
TransformActionSyntax,
Expand Down
32 changes: 21 additions & 11 deletions packages/@ember/template-compiler/lib/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { precompile as glimmerPrecompile } from '@glimmer/compiler';
import type { SerializedTemplateWithLazyBlock } from '@glimmer/interfaces';
import { setComponentTemplate } from '@glimmer/manager';
import { templateFactory } from '@glimmer/opcode-compiler';
import compileOptions from './compile-options';
import compileOptions, { keywords } from './compile-options';
import type { EmberPrecompileOptions } from './types';

type ComponentClass = abstract new (...args: any[]) => object;
Expand Down Expand Up @@ -237,38 +237,48 @@ export function template(
templateString: string,
providedOptions?: BaseTemplateOptions | BaseClassTemplateOptions<any>
): object {
const options: EmberPrecompileOptions = { strictMode: true, ...providedOptions };
const evaluate = buildEvaluator(options);
const options = { strictMode: true, ...providedOptions };

const evaluate = buildEvaluator(options);
const normalizedOptions = compileOptions(options);
const component = normalizedOptions.component ?? templateOnly();

const source = glimmerPrecompile(templateString, normalizedOptions);
const template = templateFactory(evaluate(`(${source})`) as SerializedTemplateWithLazyBlock);
const wire = evaluate(`(${source})`) as SerializedTemplateWithLazyBlock;

const template = templateFactory(wire);

setComponentTemplate(template, component);

return component;
}

const evaluator = (source: string) => {
return new Function(`return ${source}`)();
return new Function(`return ${source}`)();
};

function buildEvaluator(options: Partial<EmberPrecompileOptions> | undefined) {
if (options === undefined) {
return evaluator;
}

/**
* Builds the source wireformat JSON block
*
* @param options
* @returns
*/
function buildEvaluator(options: Partial<EmberPrecompileOptions>) {
if (options.eval) {
return options.eval;
} else {
const scope = options.scope?.();
/**
* This is ran before the template is compiled,
* so we cannot use any information gathered during template compilation.
*/
let scope = options.scope?.();

if (!scope) {
return evaluator;
}

scope = Object.assign({}, keywords, scope);

return (source: string) => {
let hasThis = Object.prototype.hasOwnProperty.call(scope, 'this');
let thisValue = hasThis ? (scope as { this?: unknown }).this : undefined;
Expand Down
Loading
Loading