zodern

Implementing Flare - Part 3

This is part 3 of a series of blog posts creating the Flare Meteor package to support single file Blaze templates.

So far, we are able to put the template and js in a single file, and use js directly in our template tags, and call any function defined or imported in the file from a template tag. In this post we will allow adding event listeners directly in the template.

<script>
function onClick() {
alert('Clicked!');
}
</script>

<button on:click={{onClick}}>Click me</button>

We use the syntax on:eventName={{functionName}}. This is very similar to svelte, without all of the extra features it has. Implementing this is similar to what we did in the previous step. We must find these event listeners, remove them from the template, and add them to the js with Template.<templateName>.events.

To find them, we will modify findTemplateTags with an additional if statement while iterating the attributes:

if (attr.name.startsWith('on:')) {
const event = attr.name.slice('on:'.length);
// Make sure value is in correct format
if (!attr.value.startsWith('{{') || !attr.value.endsWith('}}')) {
throw new Error('Event listener is invalid');
}

const handler = attr.value.slice('{{'.length, attr.value.length - '}}'.length);
result.push({
node,
stringPosition: {
type: 'attr',
attr
},
event,
handler
});
}

It extracts the event name and the name of the handler. We do some basic validation. Later we will come back and make all of our errors more helpful.

The full findTemplateTags method looks like:

class Compiler {
findTemplateTags(ast) {
const result = [];

function walkChildren(children, handler) {
children.forEach(child => {
handler(child);
if (child.childNodes) {
walkChildren(child.childNodes, handler);
}
});
}

walkChildren(ast.childNodes, node => {
function onTag(scanner) {
const tagStart = scanner.index;
let allowJsExpression = true;
scanner.allowWhitespace();

if (
scanner.eat('>') ||
scanner.eat('/')
) {
// Inclusion tag or closing tag, which we don't support js in
allowJsExpression = false;
}

if (scanner.eat('else')) {
allowJsExpression = false;
}

// Make sure it supports js, and jump the
// parser to where the js expression would start
if (scanner.eat('#')) {
scanner.allowWhitespace();

allowJsExpression = scanner.eat('if') ||
scanner.eat('unless') ||
scanner.eat('each');
}

let expression;
if (allowJsExpression) {
expression = acorn.parseExpressionAt(
scanner.content,
scanner.index,
{
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
sourceType: 'module',
ecmaVersion: 'latest',
}
);
scanner.index = expression.end;
}

scanner.eatUntil('}}');

if (scanner.match('}')) {
// Was a triple bracket
scanner.eat('}')
}

result.push({
node,
stringPosition: scanner.contentContext,
tagStart,
tagEnd: scanner.index,
expression,
});
}

if (node.nodeName === '#text') {
new TagScanner(node.value, { type: 'nodeValue' }).findTags(onTag);
}
if (node.attrs) {
node.attrs.forEach(attr => {
if (attr.name.includes('{')) {
new TagScanner(attr.name, { type: 'attrName', attr }).findTags(onTag);
} else if (attr.name.startsWith('on:')) {
const event = attr.name.slice('on:'.length);
// Make sure value is in correct format
if (!attr.value.startsWith('{{') || !attr.value.endsWith('}}')) {
throw new Error('Event listener is invalid');
}

const handler = attr.value.slice('{{'.length, attr.value.length - '}}'.length);
result.push({
node,
stringPosition: {
type: 'attr',
attr
},
event,
handler
});
} else if (typeof attr.value === 'string') {
new TagScanner(attr.value, { type: 'attrValue', attr }).findTags(onTag);
}
});
}
});

return result;
}
}

Next, we will need to remove the attribute. We will need a way to reference this specific element though so we can add an event listener to it. To do this we will add a unique class name to the element. This is all done in prepareTemplateTags:

templateTags.slice().reverse().forEach(tag => {
if (tag.expression) {
tag.helperName = `flame_${Math.floor(Math.random() * 100000)}`;
spliceContent(
tag,
tag.expression.start,
tag.expression.end - tag.expression.start,
tag.helperName
);
}

if (tag.event) {
const node = tag.node;

tag.className = `flame_${Math.floor(Math.random() * 1000000)}`;

let classAttr = node.attrs.find(attr => attr.name === 'class');
if (!classAttr) {
classAttr = { name: 'class', value: '' };
node.attrs.push(classAttr);
}
classAttr.value += ` ${tag.className}`;

const eventAttrIndex = node.attrs.indexOf(tag.stringPosition.attr);
node.attrs.splice(eventAttrIndex, 1);
}
});

For event listeners, it creates a random class name it can use to reference the element. Then it adds the class to the node, and removes the attribute for the event listener.

Finally, we need to call Template.<templateName>.events for these event listeners. This is similar to how we added helpers in step 2, but much simpler since we only need to call the handler. The output will look like this:

Template.templateName.events({
'click .flame_123'(event, template) {
return handler.call(this, event, template);
}
});

We could instead do something like

Template.templateName.events({
'click .flame_123': handler
});

but by calling handler at run time we support the handler being any expression that evaluates to a function. For example, the handler could be currentUser().activate, and it will run currentUser() when clicked and call activate instead of evaluating this expression when the file is initially ran.

At the moment you could create in-line event handlers by wrapping a function expression in parentheses, for example (() => alert('clicked!')). Eventually we could parse the handler to identify inline functions so it doesn't have to be wrapped with parenthesis.

To generate this, we will add an addTemplateEvents method. It is very similar to addHelpers, but simpler since we don't have to worry about analyzing the scope:

class Compiler {
// ... rest of compiler

addTemplateEvents(templateTags, templateName) {
const events = [];

templateTags.forEach(tag => {
if (!tag.event) {
return;
}

const helperMethod = b.objectMethod(
"method",
b.literal(`${tag.event} .${tag.className}`),
[b.identifier('event'), b.identifier('template')],
b.blockStatement([
b.expressionStatement(
b.callExpression(
b.memberExpression(b.identifier(tag.handler), b.identifier("call")),
[b.thisExpression(), b.identifier('event'), b.identifier('template')]
)
)
]),
);
events.push(helperMethod);
});

const completedAst = b.expressionStatement(
b.callExpression(
b.memberExpression(b.memberExpression(b.identifier("Template"), b.identifier(templateName)), b.identifier('events')),
[b.objectExpression(events)]
)
);

return print(completedAst).code;
}
}

Finally, we can update processFilesForTarget to use our new method.

class Compiler {
// ... rest of class

processFilesForTarget(files) {
files.forEach((file) => {
// find the contents of the template and script
const tags = this.parseTags(file);
const {
templateName = `${file.getPackageName() || 'app'}/${file.getPathInPackage()}`,
ast
} = this.parseJs(tags.script, file.getPathInPackage());

const addedHelpers = this.addHelpers(tags.templateTags, ast, templateName);
const addedEvents = this.addTemplateEvents(tags.templateTags, templateName);

tags.script += '\n' + addedHelpers + '\n' + addedEvents;

const output = TemplatingTools.compileTagsWithSpacebars([{
tagName: 'template',
attribs: { name: templateName },
contents: tags.template,
sourceName: file.getPathInPackage()
}]);
const scriptContent = this.babelCompiler.processOneFileForTarget(
file, tags.script
).data;

file.addJavaScript({
data: `${output.js}\n${scriptContent}`,
path: file.getPathInPackage()
});
});
}
}

We can now update counter.blaze to use the new syntax for events. Remove Template.counter.events, add an increment function, and add an event listener in the HTML:

<script>
import { ReactiveVar } from 'meteor/reactive-var';

export const name = 'counter';

Template.counter.onCreated(function helloOnCreated() {
this.counter = new ReactiveVar(0);
});

function increment(_event, instance) {
instance.counter.set(instance.counter.get() + 1);
}

function counter() {
return Template.instance().counter.get();
}
</script>

<button on:click={{increment}}>Click Me</button>
{{#if counter()}}
<p>You've pressed the button {{counter()}} times.</p>
{{/if}}

{{#with x=1 y=2}}
<p>x: {{this.x}}, y: {{this.y}}</p>
{{console.log('the current context is', this)}}
{{/with}}

When you click the button, it should increase the counter as it did before.

Since we did most of the hard work in the previous step, this went fairly quickly. Let's move on to the next change, using a single bracket instead of the 2 or 3 spacebars normally does.

Most spacebar tags uses 2, but one uses 3 - the raw HTML tag ({{{ rawHtml }}}). Usually spacebars escapes special characters in strings to avoid XSS, but this tag does not. We can not leave it as is since Blaze expects objects in some situations ({ class: getClass(), style: getStyle() }), and those could mess up our logic for finding template tags. To be consistent with other tags, we can use a single { and start the tag with a special character. It must be a character that can not be at the start of a js expression, and preferably would have some meaning in English that shows this tag is potentially dangerous. The closest I can think of is ?, which would result in a tag that looks like {? createRawHtml()}.

We need to update our logic for finding tags to support a single brace. Update up our scanner.js file, remove any logic for the raw HTML tag, and change it to look for a single bracket.

class Scanner {
// ... rest of class

findTags(onTag) {
while(this.index < this.content.length) {
if (this.eat('{')) {
if (this.eat('|')) {
// Spacebars allows escaping brackets with | so they are included in the html
continue;
}

onTag(this);
} else {
this.index += 1;
}
}
}
}

We also need to make similar changes to findTemplateTags, and check for raw HTML tags:

class Compiler {
findTemplateTags(ast) {
const result = [];

walkChildren(ast.childNodes, node => {
function onTag(scanner) {
const tagStart = scanner.index;
let allowJsExpression = true;
let isRawHtml = false;
scanner.allowWhitespace();

if (
scanner.eat('>') ||
scanner.eat('/')
) {
// Inclusion tag or closing tag, which we don't support js in
allowJsExpression = false;
} else if (scanner.eat('else')) {
allowJsExpression = false;
} else if (scanner.eat('#')) {
scanner.allowWhitespace();

allowJsExpression = scanner.eat('if') ||
scanner.eat('unless') ||
scanner.eat('each');
} else if (scanner.eat('?')) {
isRawHtml = true;
}

let expression;
if (allowJsExpression) {
expression = acorn.parseExpressionAt(
scanner.content,
scanner.index,
{
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
sourceType: 'module',
ecmaVersion: 'latest',
}
);
scanner.index = expression.end;
}

scanner.eatUntil('}');

result.push({
node,
stringPosition: scanner.contentContext,
tagStart,
tagEnd: scanner.index,
expression,
isRawHtml
});
}

if (node.nodeName === '#text') {
new TagScanner(node.value, { type: 'nodeValue' }).findTags(onTag);
}
if (node.attrs) {
node.attrs.forEach(attr => {
if (attr.name.includes('{')) {
new TagScanner(attr.name, { type: 'attrName', attr }).findTags(onTag);
} else if (attr.name.startsWith('on:')) {
const event = attr.name.slice('on:'.length);
// Make sure value is in correct format
if (!attr.value.startsWith('{') || !attr.value.endsWith('}')) {
throw new Error('Event listener is invalid');
}

const handler = attr.value.slice('{'.length, attr.value.length - '}'.length);
result.push({
node,
stringPosition: {
type: 'attr',
attr
},
event,
handler
});
} else if (typeof attr.value === 'string') {
new TagScanner(attr.value, { type: 'attrValue', attr }).findTags(onTag);
}
});
}
});

return result;
}
}

We are setting isRawHtml to true when a template tag starts with ?, and storing it in the results so we can convert it to a normal spacebars raw HTML tag later.

Next we need to next modify these tags to be valid spacebars syntax before passing it to the spacebars compiler. We will do this in prepareTemplateTags.

class Compiler {
// ... rest of compiler

prepareTemplateTags(templateTags) {
function spliceContent(templateTag, startIndex, toRemove, replacement) {
const replace = (string) => {
const startText = string.slice(0, startIndex);
const endText = string.slice(startIndex + toRemove);

return startText + replacement + endText;
}

switch (templateTag.stringPosition.type) {
case 'nodeValue':
templateTag.node.value = replace(templateTag.node.value);
break;
case 'attrName':
templateTag.stringPosition.attr.name = replace(templateTag.stringPosition.attr.name);
break;
case 'attrValue':
templateTag.stringPosition.attr.value = replace(templateTag.stringPosition.attr.value);
}
}

templateTags.slice().reverse().forEach(tag => {
const openingBracketIndex = tag.tagStart - 1;
const closingBracketIndex = tag.tagEnd - 1;
const bracketsNeeded = tag.isRawHtml ? 3 : 2;

if (!tag.event) {
spliceContent(tag, closingBracketIndex, 1, '}'.repeat(bracketsNeeded));
}

if (tag.expression) {
tag.helperName = `flame_${Math.floor(Math.random() * 100000)}`;
spliceContent(
tag,
tag.expression.start,
tag.expression.end - tag.expression.start,
tag.helperName
);
}

if (tag.event) {
const node = tag.node;

tag.className = `flame_${Math.floor(Math.random() * 1000000)}`;

let classAttr = node.attrs.find(attr => attr.name === 'class');
if (!classAttr) {
classAttr = { name: 'class', value: '' };
node.attrs.push(classAttr);
}
classAttr.value += ` ${tag.className}`;

const eventAttrIndex = node.attrs.indexOf(tag.stringPosition.attr);
node.attrs.splice(eventAttrIndex, 1);
}

if (!tag.event) {
// For raw HTML tags, we need to also remove the "?" character.
const toReplace = tag.isRawHtml ? 2 : 1;
spliceContent(tag, openingBracketIndex, toReplace, '{'.repeat(bracketsNeeded));
}
});
}
}

For any template tag that isn't for events (since those are removed), we replace the brackets around a template tag with the valid number of brackets for spacebars, and remove the ? for the raw HTML tags. While doing this, we have to be careful about not changing any indexes of content we still need to find. So we modify the closing brackets right away before it's index is changed, and modify the opening brackets at the end so it doesn't affect the expression's position.

At this point, it will fail when compiling any existing .blaze files since they have too many brackets. We need to go and modify our files to use a single bracket for any spacebar tags, and we can add a raw HMTL tag to test it. Our counter.blaze file will now look like:

<script>
import { ReactiveVar } from 'meteor/reactive-var';

export const name = 'counter';

Template.counter.onCreated(function helloOnCreated() {
this.counter = new ReactiveVar(0);
});

function increment(_event, instance) {
instance.counter.set(instance.counter.get() + 1);
}

function counter() {
return Template.instance().counter.get();
}

const rawHtml = '<p>This is from raw html</p>'
</script>

<button on:click={increment}>Click Me</button>
{#if counter()}
<p>You've pressed the button {counter()} times.</p>
{/if}

{#with x=1 y=2}
<p>x: {this.x}, y: {this.y}</p>
{console.log('the current context is', this)}
{/with}

{? rawHtml }

We are using the rawHtml constant we defined in the script directly in the template. We should only do this for constants since Blaze will not update if a variable is modified.

This is looking much closer to our desired .blaze file from step one. In the next post, we will implement $state for template state, and $parent to access the enclosing context (same as using .. within a template).

At this point, the scanner.js file looks like:

const whitespace = /[ \t\r\n]/;

export default class Scanner {
constructor(content, contentContext) {
this.content = content;
this.contentContext = contentContext;
this.index = 0;
}

match (text) {
return this.content.slice(this.index, this.index + text.length) === text;
}

eat (text, mustMatch) {
if (this.match(text)) {
this.index += text.length;
return true;
}

if (mustMatch) {
throw new Error(`Did not find string: ${text}`);
}

return false;
}

eatUntil (text) {
while(!this.eat(text) && this.index < this.content.length) {
this.index += 1;
}
}

allowWhitespace() {
while(whitespace.test(this.content[this.index])) {
this.index += 1;
}
}

findTags(onTag) {
while(this.index < this.content.length) {
if (this.eat('{')) {
if (this.eat('|')) {
// Spacebars allows escaping brackets with | so they are included in the html
continue;
}

onTag(this);
} else {
this.index += 1;
}
}
}
}

and the compiler.js file contains:

const parse5 = require('parse5');
const acorn = require('acorn');
const { visit } = require('ast-types');
const os = require('os');
import TagScanner from './scanner';
import periscopic from 'periscopic';
import { types, print } from 'recast';
import { browser as browserGlobals } from 'globals';

const b = types.builders;

class Compiler {
constructor() {
this.babelCompiler = new BabelCompiler;
this.babelCompiler.setDiskCacheDirectory(os.tmpdir() + '/meteor-babel-' + Math.random() + Date.now());
}

findTemplateTags(ast) {
const result = [];

function walkChildren(children, handler) {
children.forEach(child => {
handler(child);
if (child.childNodes) {
walkChildren(child.childNodes, handler);
}
});
}

walkChildren(ast.childNodes, node => {
function onTag(scanner) {
const tagStart = scanner.index;
let allowJsExpression = true;
let isRawHtml = false;
scanner.allowWhitespace();

if (
scanner.eat('>') ||
scanner.eat('/')
) {
// Inclusion tag or closing tag, which we don't support js in
allowJsExpression = false;
} else if (scanner.eat('else')) {
allowJsExpression = false;
} else if (scanner.eat('#')) {
scanner.allowWhitespace();

allowJsExpression = scanner.eat('if') ||
scanner.eat('unless') ||
scanner.eat('each');
} else if (scanner.eat('?')) {
isRawHtml = true;
}

let expression;
if (allowJsExpression) {
expression = acorn.parseExpressionAt(
scanner.content,
scanner.index,
{
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
sourceType: 'module',
ecmaVersion: 'latest',
}
);
scanner.index = expression.end;
}

scanner.eatUntil('}');

result.push({
node,
stringPosition: scanner.contentContext,
tagStart,
tagEnd: scanner.index,
expression,
isRawHtml
});
}

if (node.nodeName === '#text') {
new TagScanner(node.value, { type: 'nodeValue' }).findTags(onTag);
}
if (node.attrs) {
node.attrs.forEach(attr => {
if (attr.name.includes('{')) {
new TagScanner(attr.name, { type: 'attrName', attr }).findTags(onTag);
} else if (attr.name.startsWith('on:')) {
const event = attr.name.slice('on:'.length);
// Make sure value is in correct format
if (!attr.value.startsWith('{') || !attr.value.endsWith('}')) {
throw new Error('Event listener is invalid');
}

const handler = attr.value.slice('{'.length, attr.value.length - '}'.length);
result.push({
node,
stringPosition: {
type: 'attr',
attr
},
event,
handler
});
} else if (typeof attr.value === 'string') {
new TagScanner(attr.value, { type: 'attrValue', attr }).findTags(onTag);
}
});
}
});

return result;
}

spliceString(string, start, toRemove, replacement = '') {
const startText = string.slice(0, start);
const endText = string.slice(start + toRemove);

return startText + replacement + endText;
}

prepareTemplateTags(templateTags) {
function spliceContent(templateTag, startIndex, toRemove, replacement) {
const replace = (string) => {
const startText = string.slice(0, startIndex);
const endText = string.slice(startIndex + toRemove);

return startText + replacement + endText;
}

switch (templateTag.stringPosition.type) {
case 'nodeValue':
templateTag.node.value = replace(templateTag.node.value);
break;
case 'attrName':
templateTag.stringPosition.attr.name = replace(templateTag.stringPosition.attr.name);
break;
case 'attrValue':
templateTag.stringPosition.attr.value = replace(templateTag.stringPosition.attr.value);
}
}

templateTags.slice().reverse().forEach(tag => {
const openingBracketIndex = tag.tagStart - 1;
const closingBracketIndex = tag.tagEnd - 1;
const bracketsNeeded = tag.isRawHtml ? 3 : 2;
if (!tag.event) {
// For raw HTML tags, we need to also remove the "?" character.
spliceContent(tag, closingBracketIndex, 1, '}'.repeat(bracketsNeeded));
}

if (tag.expression) {
tag.helperName = `flame_${Math.floor(Math.random() * 100000)}`;
spliceContent(
tag,
tag.expression.start,
tag.expression.end - tag.expression.start,
tag.helperName
);
}

if (tag.event) {
const node = tag.node;

tag.className = `flame_${Math.floor(Math.random() * 1000000)}`;

let classAttr = node.attrs.find(attr => attr.name === 'class');
if (!classAttr) {
classAttr = { name: 'class', value: '' };
node.attrs.push(classAttr);
}
classAttr.value += ` ${tag.className}`;

const eventAttrIndex = node.attrs.indexOf(tag.stringPosition.attr);
node.attrs.splice(eventAttrIndex, 1);
}

if (!tag.event) {
// For raw HTML tags, we need to also remove the "?" character.
const toReplace = tag.isRawHtml ? 2 : 1;
spliceContent(tag, openingBracketIndex, toReplace, '{'.repeat(bracketsNeeded));
}
});
}

parseTags(file) {
// Parse the file's content to get the AST
const document = parse5.parseFragment(file.getContentsAsString());
let scripts = [];

// document.childNodes has all of the top-level tags in the file
document.childNodes = bodyContent = document.childNodes.filter(tag => {
if (tag.nodeName === 'script') {
scripts.push(tag);
return false;
}

return true;
});

const templateTags = this.findTemplateTags(document);
this.prepareTemplateTags(templateTags);

if (scripts.length !== 1) {
throw new Error(`File must have exactly one script tag in ${file.getPathInPackage()}`);
}

return {
// The childNode is a text node with the contents
script: scripts[0].childNodes[0].value,
template: parse5.serialize(document),
templateTags,
};
}

parseJs(js, filePath) {
const ast = acorn.parse(js, {
sourceType: 'module',
ecmaVersion: 'latest',
allowImportExportEverywhere: true,
locations: true,
ranges: true
});

let templateName = null;

visit(ast, {
visitExportNamedDeclaration(path) {
path.node.declaration.declarations.forEach(declaration => {
const {
id,
init
} = declaration;

if (id.type !== 'Identifier' || id.name !== 'name') {
return;
}

if (init.type !== 'Literal') {
throw new Error(`name export must be a literal in ${filePath}`);
}

templateName = declaration.init.value;
});

return false;
}
});

return {
templateName,
ast
};
}

addHelpers(templateTags, scriptAst, templateName) {
const helpers = [];
const {
scope
} = periscopic.analyze(scriptAst);

// AST for Template.instance().view.lookup
const lookUpFunc = b.memberExpression(
b.memberExpression(
b.callExpression(
b.memberExpression(
b.identifier('Template'),
b.identifier("instance")
),
[]
),
b.identifier("view")),
b.identifier("lookup")
);

templateTags.forEach(tag => {
if (!tag.expression || !tag.helperName) {
return;
}

// The expression shouldn't have any variable declarations
// so any variables it references are considered globals by periscopic
const {
globals
} = periscopic.analyze(tag.expression);

// If the variable referenced by the expression is defined
// within the scrip tag or is a global, we don't need to do
// anything. Otherwise, we must assume it is a template helper.
const helperLookups = Array.from(globals.keys()).filter(key => {
return !scope.has(key) && !(key in browserGlobals);
}).map(name => {
// AST for let <name> = Template.instance().view.lookup(<name>);
return b.variableDeclaration("let", [
b.variableDeclarator(
b.identifier(name),
b.callExpression(
lookUpFunc,
[b.literal(name)])
)
]);
});

const helperMethod = b.objectMethod(
"method",
b.literal(tag.helperName),
[],
b.blockStatement([
...helperLookups,
b.returnStatement(tag.expression)
])
);
helpers.push(helperMethod);
});

const completedAst = b.expressionStatement(
b.callExpression(
b.memberExpression(b.memberExpression(b.identifier("Template"), b.identifier(templateName)), b.identifier('helpers')),
[b.objectExpression(helpers)]
)
);

return print(completedAst).code;
}

addTemplateEvents(templateTags, templateName) {
const events = [];

templateTags.forEach(tag => {
if (!tag.event) {
return;
}

const helperMethod = b.objectMethod(
"method",
b.literal(`${tag.event} .${tag.className}`),
[b.identifier('event'), b.identifier('template')],
b.blockStatement([
b.expressionStatement(
b.callExpression(
b.memberExpression(b.identifier(tag.handler), b.identifier("call")),
[b.thisExpression(), b.identifier('event'), b.identifier('template')]
)
)
]),
);
events.push(helperMethod);
});

const completedAst = b.expressionStatement(
b.callExpression(
b.memberExpression(b.memberExpression(b.identifier("Template"), b.identifier(templateName)), b.identifier('events')),
[b.objectExpression(events)]
)
);

return print(completedAst).code;
}

processFilesForTarget(files) {
files.forEach((file) => {
// find the contents of the template and script
const tags = this.parseTags(file);
const {
templateName = `${file.getPackageName() || 'app'}/${file.getPathInPackage()}`,
ast
} = this.parseJs(tags.script, file.getPathInPackage());

const addedHelpers = this.addHelpers(tags.templateTags, ast, templateName);
const addedEvents = this.addTemplateEvents(tags.templateTags, templateName);

tags.script += '\n' + addedHelpers + '\n' + addedEvents;

const output = TemplatingTools.compileTagsWithSpacebars([{
tagName: 'template',
attribs: { name: templateName },
contents: tags.template,
sourceName: file.getPathInPackage()
}]);
const scriptContent = this.babelCompiler.processOneFileForTarget(
file, tags.script
).data;

file.addJavaScript({
data: `${output.js}\n${scriptContent}`,
path: file.getPathInPackage()
});
});
}
}

Plugin.registerCompiler({
extensions: ['blaze'],
archMatching: 'web',
isTemplate: true
}, () => new Compiler());

Have a comment? Please Send me an email


Want More? Join my monthly newsletter for new blog posts and news about Meteor and my projects.

Have a Meteor app in production? I run Monti APM for production monitoring to help improve performance and reduce errors.