Skip to content

Commit

Permalink
wip: create scopes for if and for/iterator directives
Browse files Browse the repository at this point in the history
  • Loading branch information
jodarove committed Jul 15, 2021
1 parent 5100fc0 commit b9258ee
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 26 deletions.
23 changes: 22 additions & 1 deletion packages/@lwc/template-compiler/src/codegen/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ResolvedConfig } from '../config';
import * as t from '../shared/estree';
import { IRElement, LWCDirectiveRenderMode } from '../shared/types';
import { toPropertyName } from '../shared/utils';
import { Scope } from './scope';

type RenderPrimitive =
| 'iterator'
Expand Down Expand Up @@ -68,6 +69,9 @@ export default class CodeGen {
*/
readonly scopeFragmentId: boolean;

scopesCount = 0;
currentScope = new Scope(0);

currentId = 0;
currentKey = 0;

Expand All @@ -94,6 +98,23 @@ export default class CodeGen {
this.scopeFragmentId = scopeFragmentId;
}

createScope() {
const newScope = new Scope(++this.scopesCount);

newScope.parentScope = this.currentScope;
this.currentScope.childScopes.push(newScope);

this.currentScope = newScope;
}

popScope() {
if (this.currentScope.parentScope === null) {
throw new Error('Trying to pop root scope');
}

this.currentScope = this.currentScope.parentScope;
}

generateKey() {
return this.currentKey++;
}
Expand Down Expand Up @@ -143,7 +164,7 @@ export default class CodeGen {
return this._renderApiCall(RENDER_APIS.comment, [t.literal(value)]);
}

genIterator(iterable: t.Expression, callback: t.FunctionExpression) {
genIterator(iterable: t.Expression, callback: t.Identifier) {
return this._renderApiCall(RENDER_APIS.iterator, [iterable, callback]);
}

Expand Down
85 changes: 60 additions & 25 deletions packages/@lwc/template-compiler/src/codegen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ function transform(codeGen: CodeGen): t.Expression {

let res = applyTemplateIf(element, children);

if (t.isSpreadElement(res)) {
res = t.arrayExpression([res]);
}

if (element.forEach) {
res = applyTemplateFor(element, res);
} else if (element.forOf) {
Expand Down Expand Up @@ -122,6 +126,15 @@ function transform(codeGen: CodeGen): t.Expression {
let expr;

if (isElement(child)) {
// When the same element have if and forEach/forOf directives, we must create two scopes.
if (child.if) {
codeGen.createScope();
}

if (child.forEach || child.forOf) {
codeGen.createScope();
}

expr = isTemplate(child) ? transformTemplate(child) : transformElement(child);
} else if (isTextNode(child)) {
expr = transformText(child);
Expand All @@ -146,16 +159,21 @@ function transform(codeGen: CodeGen): t.Expression {
function applyInlineIf(
element: IRElement,
node: t.Expression,
testExpression?: t.Expression,
falseValue?: t.Expression
): t.Expression {
if (!element.if) {
return node;
}

if (!testExpression) {
testExpression = bindExpression(element.if!, element);
}
const ifFn = codeGen.currentScope.setFn(
[],
t.blockStatement([t.returnStatement(node)]),
'if'
);

codeGen.popScope();

const testExpression = bindExpression(element.if!, element);

let leftExpression: t.Expression;
const modifier = element.ifModifier!;
Expand All @@ -171,7 +189,11 @@ function transform(codeGen: CodeGen): t.Expression {
});
}

return t.conditionalExpression(leftExpression, node, falseValue ?? t.literal(null));
const falsyArray = t.isArrayExpression(node)
? t.arrayExpression(node.elements.map(() => falseValue ?? t.literal(null)))
: falseValue ?? t.literal(null);

return t.conditionalExpression(leftExpression, t.callExpression(ifFn, []), falsyArray);
}

function applyInlineFor(element: IRElement, node: t.Expression) {
Expand All @@ -185,12 +207,14 @@ function transform(codeGen: CodeGen): t.Expression {
params.push(index);
}

const iterable = bindExpression(expression, element);
const iterationFunction = t.functionExpression(
null,
const iterationFunction = codeGen.currentScope.setFn(
params,
t.blockStatement([t.returnStatement(node)])
t.blockStatement([t.returnStatement(node)]),
'foreach'
);
codeGen.popScope();

const iterable = bindExpression(expression, element);

return codeGen.genIterator(iterable, iterationFunction);
}
Expand All @@ -217,18 +241,21 @@ function transform(codeGen: CodeGen): t.Expression {
)
);

const iterable = bindExpression(expression, element);
const iterationFunction = t.functionExpression(
null,
const iterationFunction = codeGen.currentScope.setFn(
iteratorArgs,
t.blockStatement([
t.variableDeclaration('const', [
t.variableDeclarator(t.identifier(iteratorName), iteratorObjet),
]),
t.returnStatement(node),
])
]),
'forof'
);

codeGen.popScope();

const iterable = bindExpression(expression, element);

return codeGen.genIterator(iterable, iterationFunction);
}

Expand All @@ -250,25 +277,31 @@ function transform(codeGen: CodeGen): t.Expression {
return applyInlineFor(element, expression);
}

function applyTemplateIf(element: IRElement, fragmentNodes: t.Expression): t.Expression {
function applyTemplateIf(
element: IRElement,
fragmentNodes: t.Expression
): t.Expression | t.SpreadElement {
if (!element.if) {
return fragmentNodes;
}

if (t.isArrayExpression(fragmentNodes)) {
// Bind the expression once for all the template children
const testExpression = bindExpression(element.if!, element);

return t.arrayExpression(
fragmentNodes.elements.map((child) =>
child !== null
? applyInlineIf(element, child as t.Expression, testExpression)
: null
)
);
// Notice that no optimization can be done when there's only one element and is a spread element.
if (
fragmentNodes.elements.length === 1 &&
fragmentNodes.elements[0]?.type !== 'SpreadElement'
) {
return applyInlineIf(element, fragmentNodes.elements[0]!);
}

const ifConditional = applyInlineIf(element, fragmentNodes);

// This will generate something like ...$cv1_0 ? if2_0() : [null, null]; notice that the
// conditional expression has precedence
return t.spreadElement(ifConditional);
} else {
// If the template has a single children, make sure the ternary expression returns an array
return applyInlineIf(element, fragmentNodes, undefined, t.arrayExpression([]));
return applyInlineIf(element, fragmentNodes, t.arrayExpression([]));
}
}

Expand Down Expand Up @@ -484,6 +517,8 @@ function generateTemplateFunction(codeGen: CodeGen): t.FunctionDeclaration {
]),
];

codeGen.currentScope.serializeInto(body);

if (Object.keys(codeGen.usedSlots).length) {
body.push(
t.variableDeclaration('const', [
Expand Down
34 changes: 34 additions & 0 deletions packages/@lwc/template-compiler/src/codegen/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,40 @@ import { TEMPLATE_PARAMS } from '../shared/constants';
import { isComponentProp } from '../shared/ir';
import { IRNode, TemplateExpression } from '../shared/types';

function dumpScope(scope: Scope, body: t.Statement[]) {
for (const childScope of scope.childScopes) {
body.unshift(childScope.scopeFn!);
dumpScope(childScope, childScope.scopeFn?.body!.body!);
}
}

export class Scope {
id: number;
parentScope: Scope | null = null;
childScopes: Scope[] = [];
scopeFn: t.FunctionDeclaration | null = null;

constructor(id: number) {
this.id = id;
}

setFn(
params: t.FunctionExpression['params'],
body: t.FunctionExpression['body'],
kind: string
) {
const id = t.identifier(`${kind}${this.id}_${this.childScopes.length}`);

this.scopeFn = t.functionDeclaration(id, params, body);

return id;
}

serializeInto(body: t.Statement[]) {
dumpScope(this, body);
}
}

/**
* Bind the passed expression to the component instance. It applies the following transformation to the expression:
* - {value} --> {$cmp.value}
Expand Down
16 changes: 16 additions & 0 deletions packages/@lwc/template-compiler/src/shared/estree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export function isArrayExpression(node: t.BaseNode): node is t.ArrayExpression {
return node.type === 'ArrayExpression';
}

export function isSpreadElement(node: t.BaseNode): node is t.SpreadElement {
return node.type === 'SpreadElement';
}

export function identifier(name: string, config?: Partial<t.Identifier>): t.Identifier {
return {
type: 'Identifier',
Expand Down Expand Up @@ -378,6 +382,17 @@ export function program(body: t.Program['body'], config?: Partial<t.Program>): t
};
}

export function spreadElement(
argument: t.Expression,
config?: Partial<t.SpreadElement>
): t.SpreadElement {
return {
type: 'SpreadElement',
argument,
...config,
};
}

export type BaseNode = t.BaseNode;
export type Identifier = t.Identifier;
export type MemberExpression = t.MemberExpression;
Expand Down Expand Up @@ -407,5 +422,6 @@ export type ImportDeclaration = t.ImportDeclaration;
export type ImportDefaultSpecifier = t.ImportDefaultSpecifier;
export type ImportSpecifier = t.ImportSpecifier;
export type ExportDefaultDeclaration = t.ExportDefaultDeclaration;
export type SpreadElement = t.SpreadElement;
export type Statement = t.Statement;
export type Program = t.Program;

0 comments on commit b9258ee

Please sign in to comment.