diff --git a/packages/caching-html-compiler/README.md b/packages/caching-html-compiler/README.md
new file mode 100644
index 0000000000..d64298c59f
--- /dev/null
+++ b/packages/caching-html-compiler/README.md
@@ -0,0 +1,37 @@
+# caching-html-compiler
+
+Provides a pluggable class used to compile HTML-style templates in Meteor build plugins. This abstracts out a lot of the functionality you would need to implement the following plugins:
+
+1. `templating`
+2. `static-html`
+3. `simple:markdown-templating`
+
+It provides automatic caching and handles communicating with the build plugin APIs. The actual functions that convert HTML into compiled form are passed in as arguments into the constructor, allowing those functions to be unit tested separately from the caching and file system functionality.
+
+-------
+
+### new CachingHtmlCompiler(name, tagScannerFunc, tagHandlerFunc)
+
+Constructs a new CachingHtmlCompiler that can be passed into `Plugin.registerCompiler`.
+
+#### Arguments
+
+1. `name` The name of the compiler, used when printing errors. Should probably be the same as the name of the build plugin and package it is used in.
+2. `tagScannerFunc` A function that takes a string representing a template file as input, and returns an array of Tag objects. See the README for `templating-tools` for more information about the Tag object.
+3. `tagHandlerFunc` A function that takes an array of Tag objects (the output of the previous argument) and returns an object with `js`, `body`, `head`, and `bodyAttr` properties, which will be added to the app through the build plugin API.
+
+#### Example
+
+Here is some example code from the `templating` package:
+
+```js
+Plugin.registerCompiler({
+ extensions: ['html'],
+ archMatching: 'web',
+ isTemplate: true
+}, () => new CachingHtmlCompiler(
+ "templating",
+ TemplatingTools.scanHtmlForTags,
+ TemplatingTools.compileTagsWithSpacebars
+));
+```
diff --git a/packages/caching-html-compiler/caching-html-compiler.js b/packages/caching-html-compiler/caching-html-compiler.js
new file mode 100644
index 0000000000..b4c2456ab2
--- /dev/null
+++ b/packages/caching-html-compiler/caching-html-compiler.js
@@ -0,0 +1,136 @@
+const path = Plugin.path;
+
+// The CompileResult type for this CachingCompiler is the return value of
+// htmlScanner.scan: a {js, head, body, bodyAttrs} object.
+CachingHtmlCompiler = class CachingHtmlCompiler extends CachingCompiler {
+ /**
+ * Constructor for CachingHtmlCompiler
+ * @param {String} name The name of the compiler, printed in errors -
+ * should probably always be the same as the name of the build
+ * plugin/package
+ * @param {Function} tagScannerFunc Transforms a template file (commonly
+ * .html) into an array of Tags
+ * @param {Function} tagHandlerFunc Transforms an array of tags into a
+ * results object with js, body, head, and bodyAttrs properties
+ */
+ constructor(name, tagScannerFunc, tagHandlerFunc) {
+ super({
+ compilerName: name,
+ defaultCacheSize: 1024*1024*10,
+ });
+
+ this._bodyAttrInfo = null;
+
+ this.tagScannerFunc = tagScannerFunc;
+ this.tagHandlerFunc = tagHandlerFunc;
+ }
+
+ // Implements method from CachingCompilerBase
+ compileResultSize(compileResult) {
+ function lengthOrZero(field) {
+ return field ? field.length : 0;
+ }
+ return lengthOrZero(compileResult.head) + lengthOrZero(compileResult.body) +
+ lengthOrZero(compileResult.js);
+ }
+
+ // Overrides method from CachingCompiler
+ processFilesForTarget(inputFiles) {
+ this._bodyAttrInfo = {};
+ super.processFilesForTarget(inputFiles);
+ }
+
+ // Implements method from CachingCompilerBase
+ getCacheKey(inputFile) {
+ // Note: the path is only used for errors, so it doesn't have to be part
+ // of the cache key.
+ return inputFile.getSourceHash();
+ }
+
+ // Implements method from CachingCompiler
+ compileOneFile(inputFile) {
+ const contents = inputFile.getContentsAsString();
+ const inputPath = inputFile.getPathInPackage();
+ try {
+ const tags = this.tagScannerFunc({
+ sourceName: inputPath,
+ contents: contents,
+ tagNames: ["body", "head", "template"]
+ });
+
+ return this.tagHandlerFunc(tags);
+ } catch (e) {
+ if (e instanceof TemplatingTools.CompileError) {
+ inputFile.error({
+ message: e.message,
+ line: e.line
+ });
+ return null;
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ // Implements method from CachingCompilerBase
+ addCompileResult(inputFile, compileResult) {
+ let allJavaScript = "";
+
+ if (compileResult.head) {
+ inputFile.addHtml({ section: "head", data: compileResult.head });
+ }
+
+ if (compileResult.body) {
+ inputFile.addHtml({ section: "body", data: compileResult.body });
+ }
+
+ if (compileResult.js) {
+ allJavaScript += compileResult.js;
+ }
+
+ if (! _.isEmpty(compileResult.bodyAttrs)) {
+ Object.keys(compileResult.bodyAttrs).forEach((attr) => {
+ const value = compileResult.bodyAttrs[attr];
+ if (this._bodyAttrInfo.hasOwnProperty(attr) &&
+ this._bodyAttrInfo[attr].value !== value) {
+ // two conflicting attributes on
tags in two different template
+ // files
+ inputFile.error({
+ message:
+ ` declarations have conflicting values for the '${ attr }' ` +
+ `attribute in the following files: ` +
+ this._bodyAttrInfo[attr].inputFile.getPathInPackage() +
+ `, ${ inputFile.getPathInPackage() }`
+ });
+ } else {
+ this._bodyAttrInfo[attr] = {inputFile, value};
+ }
+ });
+
+ // Add JavaScript code to set attributes on body
+ allJavaScript += `
+Meteor.startup(function() { $('body').attr(${JSON.stringify(compileResult.bodyAttrs)}); });
+`;
+ }
+
+
+ if (allJavaScript) {
+ const filePath = inputFile.getPathInPackage();
+ // XXX this path manipulation may be unnecessarily complex
+ let pathPart = path.dirname(filePath);
+ if (pathPart === '.')
+ pathPart = '';
+ if (pathPart.length && pathPart !== path.sep)
+ pathPart = pathPart + path.sep;
+ const ext = path.extname(filePath);
+ const basename = path.basename(filePath, ext);
+
+ // XXX generate a source map
+
+ inputFile.addJavaScript({
+ path: path.join(pathPart, "template." + basename + ".js"),
+ data: allJavaScript
+ });
+ }
+ }
+}
diff --git a/packages/caching-html-compiler/package.js b/packages/caching-html-compiler/package.js
new file mode 100644
index 0000000000..b601d98e61
--- /dev/null
+++ b/packages/caching-html-compiler/package.js
@@ -0,0 +1,21 @@
+Package.describe({
+ version: '1.0.0',
+ // Brief, one-line summary of the package.
+ summary: 'Pluggable class for compiling HTML into templates',
+ // By default, Meteor will default to using README.md for documentation.
+ // To avoid submitting documentation, set this field to null.
+ documentation: 'README.md'
+});
+
+Package.onUse(function(api) {
+ api.use([
+ 'underscore',
+ 'caching-compiler',
+ 'templating-tools',
+ 'ecmascript'
+ ]);
+
+ api.addFiles('caching-html-compiler.js', 'server');
+
+ api.export("CachingHtmlCompiler", 'server');
+});
diff --git a/packages/static-html/README.md b/packages/static-html/README.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/static-html/package.js b/packages/static-html/package.js
new file mode 100644
index 0000000000..ec1e8bb2b8
--- /dev/null
+++ b/packages/static-html/package.js
@@ -0,0 +1,28 @@
+Package.describe({
+ version: '0.0.1',
+ // Brief, one-line summary of the package.
+ summary: 'Define static page content in .html files',
+ // By default, Meteor will default to using README.md for documentation.
+ // To avoid submitting documentation, set this field to null.
+ documentation: 'README.md'
+});
+
+Package.registerBuildPlugin({
+ name: "compileStaticHtmlBatch",
+ use: [
+ 'caching-html-compiler',
+ 'ecmascript',
+ 'templating-tools',
+ 'underscore'
+ ],
+ sources: [
+ 'static-html.js'
+ ]
+});
+
+Package.onUse(function(api) {
+ api.use('isobuild:compiler-plugin@1.0.0');
+
+ // Body attributes are compiled to code that uses Meteor.startup
+ api.imply('meteor', 'client');
+});
diff --git a/packages/static-html/static-html.js b/packages/static-html/static-html.js
new file mode 100644
index 0000000000..2d223a65aa
--- /dev/null
+++ b/packages/static-html/static-html.js
@@ -0,0 +1,90 @@
+Plugin.registerCompiler({
+ extensions: ['html'],
+ archMatching: 'web',
+ isTemplate: true
+}, () => new CachingHtmlCompiler("static-html", TemplatingTools.scanHtmlForTags, compileTagsToStaticHtml));
+
+// Same API as TutorialTools.compileTagsWithSpacebars, but instead of compiling
+// with Spacebars, it just returns static HTML
+function compileTagsToStaticHtml(tags) {
+ var handler = new StaticHtmlTagHandler();
+
+ tags.forEach((tag) => {
+ handler.addTagToResults(tag);
+ });
+
+ return handler.getResults();
+};
+
+class StaticHtmlTagHandler {
+ constructor() {
+ this.results = {
+ head: '',
+ body: '',
+ js: '',
+ bodyAttrs: {}
+ };
+ }
+
+ getResults() {
+ return this.results;
+ }
+
+ addTagToResults(tag) {
+ this.tag = tag;
+
+ // do we have 1 or more attributes?
+ const hasAttribs = ! _.isEmpty(this.tag.attribs);
+
+ if (this.tag.tagName === "head") {
+ if (hasAttribs) {
+ this.throwCompileError("Attributes on not supported");
+ }
+
+ this.results.head += this.tag.contents;
+ return;
+ }
+
+
+ // or
+
+ try {
+ if (this.tag.tagName === "body") {
+ this.addBodyAttrs(this.tag.attribs);
+
+ // We may be one of many `` tags.
+ this.results.body += this.tag.contents;
+ } else {
+ this.throwCompileError("Expected or tag", this.tag.tagStartIndex);
+ }
+ } catch (e) {
+ if (e.scanner) {
+ // The error came from Spacebars
+ this.throwCompileError(e.message, this.tag.contentsStartIndex + e.offset);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ addBodyAttrs(attrs) {
+ Object.keys(attrs).forEach((attr) => {
+ const val = attrs[attr];
+
+ // This check is for conflicting body attributes in the same file;
+ // we check across multiple files in caching-html-compiler using the
+ // attributes on results.bodyAttrs
+ if (this.results.bodyAttrs.hasOwnProperty(attr) && this.results.bodyAttrs[attr] !== val) {
+ this.throwCompileError(
+ ` declarations have conflicting values for the '${attr}' attribute.`);
+ }
+
+ this.results.bodyAttrs[attr] = val;
+ });
+ }
+
+ throwCompileError(message, overrideIndex) {
+ TemplatingTools.throwCompileError(this.tag, message, overrideIndex);
+ }
+}
+
diff --git a/packages/templating-tools/README.md b/packages/templating-tools/README.md
new file mode 100644
index 0000000000..a8c2e016e5
--- /dev/null
+++ b/packages/templating-tools/README.md
@@ -0,0 +1,102 @@
+# templating-tools
+
+Has some conveniently abstracted functions that are used together with the `caching-html-compiler` package to implement different template compilers:
+
+1. `templating`
+2. `static-html`
+
+These functions contain some code shared between the above build plugins, and if you are building your own build plugin they can be useful too. But they aren't guaranteed to be helpful for every use case, so you should carefully decide if they are appropriate for your package.
+
+---------
+
+### TemplatingTools.scanHtmlForTags(options)
+
+Scan an HTML file for top-level tags as specified by `options.tagNames`, and return an array of `Tag` objects. See more about `Tag` objects below.
+
+#### Options
+
+1. `sourceName` the name of the input file, used when throwing errors.
+2. `contents` the contents of the input file, these are parsed to find the top-level tags
+3. `tagNames` the top-level tags to look for in the HTML.
+
+#### Example
+
+```js
+const tags = scanHtmlForTags({
+ sourceName: inputPath,
+ contents: contents,
+ tagNames: ["body", "head", "template"]
+});
+```
+
+### TemplatingTools.compileTagsWithSpacebars(tags)
+
+Transform an array of tags into a result object of the following form:
+
+```js
+{
+ js: String,
+ body: "",
+ head: String,
+ bodyAttrs: {
+ [attrName]: String
+ }
+}
+```
+
+1. The contents of every `` and `` tag will be compiled into JavaScript with `spacebars-compiler`, and the code appended to the `js` field of the result.
+2. The contents of every `` tag will be concatenated into the `head` field of the result.
+3. Any attributes found on `` tags will be added to the `bodyAtts` field of the result.
+4. Every `` tag is required to have a `name` attribute, and no other attributes.
+5. The `` tag is not allowed to have any attributes.
+
+### TemplatingTools.CompileError
+
+This error is thrown when a compilation error happens. If you catch it, look for the following fields, which are set by `TemplatingTools.throwCompileError`:
+
+1. `message` The error message to show to the user.
+2. `file` The filename where the error occured.
+3. `line` The line number where the error occured.
+
+### TemplatingTools.throwCompileError(tag, message, [overrideIndex])
+
+Throw a `TemplatingTools.CompileError` with the right properties. Handles generating the line number of the error for you.
+
+#### Arguments
+
+1. `tag` the Tag object in which this compile error occured. The fields on this object are used to populate fields on the resulting error.
+2. `message` the error message, will be displayed to the user.
+3. `overrideIndex` optional - if provided will be used to determine the line number of the error; otherwise the index of the start of the tag will be used.
+
+### Tag object
+
+The `scanHtml` and `compileTagsWithSpacebars` functions communicate via an array of Tag objects, which have the following form:
+
+```js
+{
+ // Name of the tag - "body", "head", "template", etc
+ tagName: String,
+
+ // Attributes on the tag
+ attribs: { [attrName]: String },
+
+ // Contents of the tag
+ contents: String,
+
+ // Starting index of the opening tag in the source file
+ // (used to throw informative errors)
+ tagStartIndex: Number,
+
+ // Starting index of the contents of the tag in the source file
+ // (used to throw informative errors)
+ contentsStartIndex: Number,
+
+ // The contents of the entire source file, should be used only to
+ // throw informative errors (for example, this can be used to
+ // determine the line number for an error)
+ fileContents: String,
+
+ // The file name of the initial source file, used to throw errors
+ sourceName: String
+};
+```
diff --git a/packages/templating-tools/code-generation.js b/packages/templating-tools/code-generation.js
new file mode 100644
index 0000000000..6a7f8695b7
--- /dev/null
+++ b/packages/templating-tools/code-generation.js
@@ -0,0 +1,18 @@
+TemplatingTools.generateTemplateJS =
+function generateTemplateJS(name, renderFuncCode) {
+ const nameLiteral = JSON.stringify(name);
+ const templateDotNameLiteral = JSON.stringify(`Template.${name}`);
+
+ return `
+Template.__checkName(${nameLiteral});
+Template[${nameLiteral}] = new Template(${templateDotNameLiteral}, ${renderFuncCode});
+`;
+}
+
+TemplatingTools.generateBodyJS =
+function generateBodyJS(renderFuncCode) {
+ return `
+Template.body.addContent(${renderFuncCode});
+Meteor.startup(Template.body.renderToDocument);
+`;
+}
diff --git a/packages/templating-tools/compile-tags-with-spacebars.js b/packages/templating-tools/compile-tags-with-spacebars.js
new file mode 100644
index 0000000000..7c3e2d67c2
--- /dev/null
+++ b/packages/templating-tools/compile-tags-with-spacebars.js
@@ -0,0 +1,104 @@
+TemplatingTools.compileTagsWithSpacebars = function compileTagsWithSpacebars(tags) {
+ var handler = new SpacebarsTagCompiler();
+
+ tags.forEach((tag) => {
+ handler.addTagToResults(tag);
+ });
+
+ return handler.getResults();
+};
+
+class SpacebarsTagCompiler {
+ constructor() {
+ this.results = {
+ head: '',
+ body: '',
+ js: '',
+ bodyAttrs: {}
+ };
+ }
+
+ getResults() {
+ return this.results;
+ }
+
+ addTagToResults(tag) {
+ this.tag = tag;
+
+ // do we have 1 or more attributes?
+ const hasAttribs = ! _.isEmpty(this.tag.attribs);
+
+ if (this.tag.tagName === "head") {
+ if (hasAttribs) {
+ this.throwCompileError("Attributes on not supported");
+ }
+
+ this.results.head += this.tag.contents;
+ return;
+ }
+
+
+ // or
+
+ try {
+ if (this.tag.tagName === "template") {
+ const name = this.tag.attribs.name;
+
+ if (! name) {
+ this.throwCompileError("Template has no 'name' attribute");
+ }
+
+ if (SpacebarsCompiler.isReservedName(name)) {
+ this.throwCompileError(`Template can't be named "${name}"`);
+ }
+
+ const renderFuncCode = SpacebarsCompiler.compile(this.tag.contents, {
+ isTemplate: true,
+ sourceName: `Template "${name}"`
+ });
+
+ this.results.js += TemplatingTools.generateTemplateJS(
+ name, renderFuncCode);
+ } else if (this.tag.tagName === "body") {
+ this.addBodyAttrs(this.tag.attribs);
+
+ const renderFuncCode = SpacebarsCompiler.compile(this.tag.contents, {
+ isBody: true,
+ sourceName: ""
+ });
+
+ // We may be one of many `` tags.
+ this.results.js += TemplatingTools.generateBodyJS(renderFuncCode);
+ } else {
+ this.throwCompileError("Expected , , or tag in template file", tagStartIndex);
+ }
+ } catch (e) {
+ if (e.scanner) {
+ // The error came from Spacebars
+ this.throwCompileError(e.message, this.tag.contentsStartIndex + e.offset);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ addBodyAttrs(attrs) {
+ Object.keys(attrs).forEach((attr) => {
+ const val = attrs[attr];
+
+ // This check is for conflicting body attributes in the same file;
+ // we check across multiple files in caching-html-compiler using the
+ // attributes on results.bodyAttrs
+ if (this.results.bodyAttrs.hasOwnProperty(attr) && this.results.bodyAttrs[attr] !== val) {
+ this.throwCompileError(
+ ` declarations have conflicting values for the '${attr}' attribute.`);
+ }
+
+ this.results.bodyAttrs[attr] = val;
+ });
+ }
+
+ throwCompileError(message, overrideIndex) {
+ TemplatingTools.throwCompileError(this.tag, message, overrideIndex);
+ }
+}
diff --git a/packages/templating/scanner_tests.js b/packages/templating-tools/html-scanner-tests.js
similarity index 66%
rename from packages/templating/scanner_tests.js
rename to packages/templating-tools/html-scanner-tests.js
index d85290cdc9..51066e81de 100644
--- a/packages/templating/scanner_tests.js
+++ b/packages/templating-tools/html-scanner-tests.js
@@ -1,4 +1,4 @@
-Tinytest.add("templating - html scanner", function (test) {
+Tinytest.add("templating-tools - html scanner", function (test) {
var testInString = function(actualStr, wantedContents) {
if (actualStr.indexOf(wantedContents) >= 0)
test.ok();
@@ -11,6 +11,10 @@ Tinytest.add("templating - html scanner", function (test) {
try {
f();
} catch (e) {
+ if (! e instanceof TemplatingTools.CompileError) {
+ throw e;
+ }
+
if (e.line === lineNum)
test.ok();
else
@@ -39,112 +43,122 @@ Tinytest.add("templating - html scanner", function (test) {
', (function() {\n var view = this;\n return ' + content + ';\n}));\n';
};
- var checkResults = function(results, expectJs, expectHead) {
+ var checkResults = function(results, expectJs, expectHead, expectBodyAttrs) {
test.equal(results.body, '');
test.equal(results.js, expectJs || '');
test.equal(results.head, expectHead || '');
+ test.equal(results.bodyAttrs, expectBodyAttrs || {});
};
+ function scanForTest(contents) {
+ const tags = TemplatingTools.scanHtmlForTags({
+ sourceName: "",
+ contents: contents,
+ tagNames: ["body", "head", "template"]
+ });
+
+ return TemplatingTools.compileTagsWithSpacebars(tags);
+ }
+
checkError(function() {
- return html_scanner.scan("asdf");
- }, "Expected , , or tag in template file", 1);
+ return scanForTest("asdf");
+ }, "Expected one of: , , ", 1);
// body all on one line
checkResults(
- html_scanner.scan("Hello"),
+ scanForTest("Hello"),
simpleBody('"Hello"'));
// multi-line body, contents trimmed
checkResults(
- html_scanner.scan("\n\n\n\n\nHello\n\n\n\n\n"),
+ scanForTest("\n\n\n\n\nHello\n\n\n\n\n"),
simpleBody('"Hello"'));
// same as previous, but with various HTML comments
checkResults(
- html_scanner.scan("\n\n\n"+
+ scanForTest("\n\n\n"+
"\n\nHello\n\n\n\n'.");
+ }
+
+ this.throwCompileError();
+ }
+
+ // otherwise, a
+ const tagName = matchTokenTagName.toLowerCase();
+ const tagAttribs = {}; // bare name -> value dict
+ const tagPartRegex = /^\s*((([a-zA-Z0-9:_-]+)\s*=\s*(["'])(.*?)\4)|(>))/;
+
+ // read attributes
+ let attr;
+ while ((attr = tagPartRegex.exec(this.rest))) {
+ const attrToken = attr[1];
+ const attrKey = attr[3];
+ let attrValue = attr[5];
+ this.advance(attr.index + attr[0].length);
+
+ if (attrToken === '>') {
+ break;
+ }
+
+ // XXX we don't HTML unescape the attribute value
+ // (e.g. to allow "abcd"efg") or protect against
+ // collisions with methods of tagAttribs (e.g. for
+ // a property named toString)
+ attrValue = attrValue.match(/^\s*([\s\S]*?)\s*$/)[1]; // trim
+ tagAttribs[attrKey] = attrValue;
+ }
+
+ if (! attr) { // didn't end on '>'
+ this.throwCompileError("Parse error in tag");
+ }
+
+ // find
+ const end = (new RegExp(''+tagName+'\\s*>', 'i')).exec(this.rest);
+ if (! end) {
+ this.throwCompileError("unclosed <"+tagName+">");
+ }
+
+ const tagContents = this.rest.slice(0, end.index);
+ const contentsStartIndex = this.index;
+
+ // trim the tag contents.
+ // this is a courtesy and is also relied on by some unit tests.
+ var m = tagContents.match(/^([ \t\r\n]*)([\s\S]*?)[ \t\r\n]*$/);
+ const trimmedContentsStartIndex = contentsStartIndex + m[1].length;
+ const trimmedTagContents = m[2];
+
+ const tag = {
+ tagName: tagName,
+ attribs: tagAttribs,
+ contents: trimmedTagContents,
+ contentsStartIndex: trimmedContentsStartIndex,
+ tagStartIndex: tagStartIndex,
+ fileContents: this.contents,
+ sourceName: this.sourceName
+ };
+
+ // save the tag
+ this.tags.push(tag);
+
+ // advance afterwards, so that line numbers in errors are correct
+ this.advance(end.index + end[0].length);
+ }
+ }
+
+ /**
+ * Advance the parser
+ * @param {Number} amount The amount of characters to advance
+ */
+ advance(amount) {
+ this.rest = this.rest.substring(amount);
+ this.index += amount;
+ }
+
+ throwCompileError(msg, overrideIndex) {
+ const finalIndex = (typeof overrideIndex === 'number' ? overrideIndex : this.index);
+
+ const err = new TemplatingTools.CompileError();
+ err.message = msg || "bad formatting in template file";
+ err.file = this.sourceName;
+ err.line = this.contents.substring(0, finalIndex).split('\n').length;
+
+ throw err;
+ }
+
+ throwBodyAttrsError(msg) {
+ this.parseError(msg);
+ }
+
+ getTags() {
+ return this.tags;
+ }
+}
diff --git a/packages/templating-tools/package.js b/packages/templating-tools/package.js
new file mode 100644
index 0000000000..a396b7afdc
--- /dev/null
+++ b/packages/templating-tools/package.js
@@ -0,0 +1,47 @@
+Package.describe({
+ name: 'templating-tools',
+ version: '0.0.1',
+ // Brief, one-line summary of the package.
+ summary: '',
+ // URL to the Git repository containing the source code for this package.
+ git: '',
+ // By default, Meteor will default to using README.md for documentation.
+ // To avoid submitting documentation, set this field to null.
+ documentation: 'README.md'
+});
+
+Package.onUse(function(api) {
+ api.use([
+ 'underscore',
+ 'ecmascript',
+ 'spacebars-compiler',
+
+ // minifiers is a weak dependency of spacebars-compiler; adding it here
+ // ensures that the output is minified. (Having it as a weak dependency means
+ // that we don't ship uglify etc with built apps just because
+ // boilerplate-generator uses spacebars-compiler.)
+ // XXX maybe uglify should be applied by this plugin instead of via magic
+ // weak dependency.
+ 'minifiers'
+ ]);
+
+ api.addFiles([
+ 'templating-tools.js',
+ 'html-scanner.js',
+ 'compile-tags-with-spacebars.js',
+ 'throw-compile-error.js',
+ 'code-generation.js'
+ ]);
+
+ api.export('TemplatingTools');
+});
+
+Package.onTest(function(api) {
+ api.use([
+ 'tinytest',
+ 'templating-tools',
+ 'ecmascript'
+ ]);
+
+ api.addFiles('html-scanner-tests.js', 'server');
+});
diff --git a/packages/templating-tools/templating-tools.js b/packages/templating-tools/templating-tools.js
new file mode 100644
index 0000000000..8d6c3db6bf
--- /dev/null
+++ b/packages/templating-tools/templating-tools.js
@@ -0,0 +1,4 @@
+TemplatingTools = {
+ // This type of error should be thrown during compilation
+ CompileError: class CompileError {}
+};
diff --git a/packages/templating-tools/throw-compile-error.js b/packages/templating-tools/throw-compile-error.js
new file mode 100644
index 0000000000..8bf9833251
--- /dev/null
+++ b/packages/templating-tools/throw-compile-error.js
@@ -0,0 +1,11 @@
+TemplatingTools.throwCompileError =
+function throwCompileError(tag, message, overrideIndex) {
+ const finalIndex = (typeof overrideIndex === 'number' ?
+ overrideIndex : tag.tagStartIndex);
+
+ const err = new TemplatingTools.CompileError();
+ err.message = message || "bad formatting in template file";
+ err.file = tag.sourceName;
+ err.line = tag.fileContents.substring(0, finalIndex).split('\n').length;
+ throw err;
+}
diff --git a/packages/templating/README.md b/packages/templating/README.md
index dd83a117ce..6f607564d4 100644
--- a/packages/templating/README.md
+++ b/packages/templating/README.md
@@ -1,6 +1,6 @@
# templating
-Define Blaze templates in `.html` files. Most Meteor apps use this package.
+Compiles Blaze templates defined in `.html` files. Also automatically includes Blaze on the client.
This build plugin parses all of the HTML files in your app and looks for three top-level tags:
diff --git a/packages/templating/package.js b/packages/templating/package.js
index c05dadf7f2..6118d77cc3 100644
--- a/packages/templating/package.js
+++ b/packages/templating/package.js
@@ -17,13 +17,11 @@ Package.registerBuildPlugin({
// XXX maybe uglify should be applied by this plugin instead of via magic
// weak dependency.
use: [
- 'minifiers',
- 'spacebars-compiler',
- 'caching-compiler',
- 'ecmascript'
+ 'caching-html-compiler',
+ 'ecmascript',
+ 'templating-tools'
],
sources: [
- 'plugin/html_scanner.js',
'plugin/compile-templates.js'
]
});
@@ -48,27 +46,3 @@ Package.onUse(function (api) {
api.addFiles(['dynamic.html', 'dynamic.js'], 'client');
});
-
-Package.onTest(function (api) {
- api.use('tinytest');
- api.use('htmljs');
- api.use('templating');
- api.use('underscore');
- api.use([
- 'test-helpers',
- 'session',
- 'tracker',
- 'minimongo',
- 'reactive-var',
- 'spacebars'
- ], 'client');
- api.use('spacebars-compiler');
- api.use('minifiers'); // ensure compiler output is beautified
-
- api.addFiles([
- 'plugin/html_scanner.js',
- 'scanner_tests.js'
- ], 'server');
-
- api.addFiles(["dynamic_tests.html", "dynamic_tests.js"], "client");
-});
diff --git a/packages/templating/plugin/compile-templates.js b/packages/templating/plugin/compile-templates.js
index 0fb8eee89f..c5e992c036 100644
--- a/packages/templating/plugin/compile-templates.js
+++ b/packages/templating/plugin/compile-templates.js
@@ -1,104 +1,9 @@
-const path = Npm.require('path');
-
Plugin.registerCompiler({
extensions: ['html'],
archMatching: 'web',
isTemplate: true
-}, () => new TemplateCompiler());
-
-// The CompileResult type for this CachingCompiler is the return value of
-// htmlScanner.scan: a {js, head, body, bodyAttrs} object.
-class TemplateCompiler extends CachingCompiler {
- constructor() {
- super({
- compilerName: 'templating',
- defaultCacheSize: 1024*1024*10,
- });
- this._bodyAttrInfo = null;
- }
-
- compileResultSize(compileResult) {
- function lengthOrZero(field) {
- return field ? field.length : 0;
- }
- return lengthOrZero(compileResult.head) + lengthOrZero(compileResult.body) +
- lengthOrZero(compileResult.js);
- }
-
- processFilesForTarget(inputFiles) {
- this._bodyAttrInfo = {};
- super.processFilesForTarget(inputFiles);
- }
-
- getCacheKey(inputFile) {
- // Note: the path is only used for errors, so it doesn't have to be part
- // of the cache key.
- return inputFile.getSourceHash();
- }
-
- compileOneFile(inputFile) {
- const contents = inputFile.getContentsAsString();
- const path = inputFile.getPathInPackage();
- try {
- return html_scanner.scan(contents, path);
- } catch (e) {
- if ((e instanceof html_scanner.ParseError) ||
- (e instanceof html_scanner.BodyAttrsError)) {
- inputFile.error({
- message: e.message,
- line: e.line
- });
- return null;
- } else {
- throw e;
- }
- }
- }
-
- addCompileResult(inputFile, compileResult) {
- if (compileResult.head) {
- inputFile.addHtml({ section: "head", data: compileResult.head });
- }
-
- if (compileResult.body) {
- inputFile.addHtml({ section: "body", data: compileResult.body });
- }
-
- if (compileResult.js) {
- const filePath = inputFile.getPathInPackage();
- // XXX this path manipulation may be unnecessarily complex
- let pathPart = path.dirname(filePath);
- if (pathPart === '.')
- pathPart = '';
- if (pathPart.length && pathPart !== path.sep)
- pathPart = pathPart + path.sep;
- const ext = path.extname(filePath);
- const basename = path.basename(filePath, ext);
-
- // XXX generate a source map
-
- inputFile.addJavaScript({
- path: path.join(pathPart, "template." + basename + ".js"),
- data: compileResult.js
- });
- }
-
- Object.keys(compileResult.bodyAttrs).forEach((attr) => {
- const value = compileResult.bodyAttrs[attr];
- if (this._bodyAttrInfo.hasOwnProperty(attr) &&
- this._bodyAttrInfo[attr].value !== value) {
- // two conflicting attributes on tags in two different template
- // files
- inputFile.error({
- message:
- ` declarations have conflicting values for the '${ attr }' ` +
- `attribute in the following files: ` +
- this._bodyAttrInfo[attr].inputFile.getPathInPackage() +
- `, ${ inputFile.getPathInPackage() }`
- });
- } else {
- this._bodyAttrInfo[attr] = {inputFile, value};
- }
- });
- }
-}
+}, () => new CachingHtmlCompiler(
+ "templating",
+ TemplatingTools.scanHtmlForTags,
+ TemplatingTools.compileTagsWithSpacebars
+));
diff --git a/packages/templating/plugin/html_scanner.js b/packages/templating/plugin/html_scanner.js
deleted file mode 100644
index 1b2690641d..0000000000
--- a/packages/templating/plugin/html_scanner.js
+++ /dev/null
@@ -1,224 +0,0 @@
-html_scanner = {
- // Scan a template file for , , and
- // tags and extract their contents.
- //
- // This is a primitive, regex-based scanner. It scans
- // top-level tags, which are allowed to have attributes,
- // and ignores top-level HTML comments.
-
- // Has fields 'message', 'line', 'file'
- ParseError: function () {},
- BodyAttrsError: function () {},
-
- // Note: source_name is only used for errors (so it's not part of the cache
- // key in compile-templates.js).
- scan: function (contents, source_name) {
- var rest = contents;
- var index = 0;
-
- var advance = function(amount) {
- rest = rest.substring(amount);
- index += amount;
- };
-
- var throwSpecialError = function (msg, errorClass, overrideIndex) {
- var ret = new errorClass;
- ret.message = msg;
- ret.file = source_name;
- var theIndex = (typeof overrideIndex === 'number' ? overrideIndex : index);
- ret.line = contents.substring(0, theIndex).split('\n').length;
- throw ret;
- };
- var throwParseError = function (msg, overrideIndex) {
- throwSpecialError(
- msg || "bad formatting in template file",
- html_scanner.ParseError,
- overrideIndex);
- };
- var throwBodyAttrsError = function (msg) {
- throwSpecialError(msg, html_scanner.BodyAttrsError);
- };
-
- var results = html_scanner._initResults();
- var rOpenTag = /^((<(template|head|body)\b)|('.");
- }
- throwParseError();
- }
-
- // otherwise, a
- var tagName = matchTokenTagName.toLowerCase();
- var tagAttribs = {}; // bare name -> value dict
- var rTagPart = /^\s*((([a-zA-Z0-9:_-]+)\s*=\s*(["'])(.*?)\4)|(>))/;
- var attr;
- // read attributes
- while ((attr = rTagPart.exec(rest))) {
- var attrToken = attr[1];
- var attrKey = attr[3];
- var attrValue = attr[5];
- advance(attr.index + attr[0].length);
- if (attrToken === '>')
- break;
- // XXX we don't HTML unescape the attribute value
- // (e.g. to allow "abcd"efg") or protect against
- // collisions with methods of tagAttribs (e.g. for
- // a property named toString)
- attrValue = attrValue.match(/^\s*([\s\S]*?)\s*$/)[1]; // trim
- tagAttribs[attrKey] = attrValue;
- }
- if (! attr) // didn't end on '>'
- throwParseError("Parse error in tag");
- // find
- var end = (new RegExp(''+tagName+'\\s*>', 'i')).exec(rest);
- if (! end)
- throwParseError("unclosed <"+tagName+">");
- var tagContents = rest.slice(0, end.index);
- var contentsStartIndex = index;
-
- if (tagName === 'body') {
- this._addBodyAttrs(results, tagAttribs, throwBodyAttrsError);
- }
-
- // act on the tag
- html_scanner._handleTag(results, tagName, tagAttribs, tagContents,
- throwParseError, contentsStartIndex,
- tagStartIndex);
-
- // advance afterwards, so that line numbers in errors are correct
- advance(end.index + end[0].length);
- }
-
- return results;
- },
-
- _initResults: function() {
- var results = {};
- results.head = '';
- results.body = '';
- results.js = '';
- results.bodyAttrs = {};
- return results;
- },
-
- _addBodyAttrs: function (results, attrs, throwBodyAttrsError) {
- Object.keys(attrs).forEach(function (attr) {
- var val = attrs[attr];
-
- if (results.bodyAttrs.hasOwnProperty(attr) && results.bodyAttrs[attr] !== val) {
- throwBodyAttrsError(
- " declarations have conflicting values for the '" + attr + "' attribute.");
- }
-
- results.bodyAttrs[attr] = val;
- });
- },
-
- _handleTag: function (results, tag, attribs, contents, throwParseError,
- contentsStartIndex, tagStartIndex) {
-
- // trim the tag contents.
- // this is a courtesy and is also relied on by some unit tests.
- var m = contents.match(/^([ \t\r\n]*)([\s\S]*?)[ \t\r\n]*$/);
- contentsStartIndex += m[1].length;
- contents = m[2];
-
- // do we have 1 or more attribs?
- var hasAttribs = false;
- for(var k in attribs) {
- if (attribs.hasOwnProperty(k)) {
- hasAttribs = true;
- break;
- }
- }
-
- if (tag === "head") {
- if (hasAttribs)
- throwParseError("Attributes on not supported");
- results.head += contents;
- return;
- }
-
-
- // or
-
- try {
- if (tag === "template") {
- var name = attribs.name;
- if (! name)
- throwParseError("Template has no 'name' attribute");
-
- if (SpacebarsCompiler.isReservedName(name))
- throwParseError("Template can't be named \"" + name + "\"");
-
- var renderFuncCode = SpacebarsCompiler.compile(
- contents, {
- isTemplate: true,
- sourceName: 'Template "' + name + '"'
- });
-
- var nameLiteral = JSON.stringify(name);
- var templateDotNameLiteral = JSON.stringify("Template." + name);
-
- results.js += "\nTemplate.__checkName(" + nameLiteral + ");\n" +
- "Template[" + nameLiteral + "] = new Template(" +
- templateDotNameLiteral + ", " + renderFuncCode + ");\n";
- } else {
- //
- if (hasAttribs) {
- results.js += "\nMeteor.startup(function() { $('body').attr(" + JSON.stringify(attribs) + "); });\n";
- }
-
- var renderFuncCode = SpacebarsCompiler.compile(
- contents, {
- isBody: true,
- sourceName: ""
- });
-
- // We may be one of many `` tags.
- results.js += "\nTemplate.body.addContent(" + renderFuncCode + ");\nMeteor.startup(Template.body.renderToDocument);\n";
- }
- } catch (e) {
- if (e.scanner) {
- // The error came from Spacebars
- throwParseError(e.message, contentsStartIndex + e.offset);
- } else {
- throw e;
- }
- }
- }
-};
diff --git a/tools/tests/apps/compiler-plugin-static-html-error/.meteor/.gitignore b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/.gitignore
new file mode 100644
index 0000000000..4083037423
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/tools/tests/apps/compiler-plugin-static-html-error/.meteor/.id b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/.id
new file mode 100644
index 0000000000..9f90746a43
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+# - ensuring you don't accidentally deploy one app on top of another
+# - providing package authors with aggregated statistics
+
+o2vi6w1m82bge9k2mhb
diff --git a/tools/tests/apps/compiler-plugin-static-html-error/.meteor/packages b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/packages
new file mode 100644
index 0000000000..1a1a3f6cc1
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/packages
@@ -0,0 +1,16 @@
+# Meteor packages used by this project, one per line.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+meteor
+webapp
+logging
+tracker
+ddp
+mongo
+check
+jquery
+reload
+autoupdate
+
+static-html
diff --git a/tools/tests/apps/compiler-plugin-static-html-error/.meteor/platforms b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/platforms
new file mode 100644
index 0000000000..8a3a35f9f6
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/platforms
@@ -0,0 +1,2 @@
+browser
+server
diff --git a/tools/tests/apps/compiler-plugin-static-html-error/.meteor/release b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/release
new file mode 100644
index 0000000000..621e94f0ec
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/release
@@ -0,0 +1 @@
+none
diff --git a/tools/tests/apps/compiler-plugin-static-html-error/.meteor/versions b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/versions
new file mode 100644
index 0000000000..991f343178
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html-error/.meteor/versions
@@ -0,0 +1,53 @@
+autoupdate@1.2.2-plugins.0
+babel-compiler@5.8.3-plugins.0_5
+babel-runtime@0.1.2
+base64@1.0.4-plugins.0
+binary-heap@1.0.4-plugins.0
+blaze@2.1.3-plugins.0
+blaze-tools@1.0.4-plugins.0
+boilerplate-generator@1.0.4-plugins.0
+caching-compiler@1.0.0-plugins.1
+caching-html-compiler@1.0.0
+callback-hook@1.0.4-plugins.0
+check@1.0.6-plugins.0
+ddp@1.2.0-plugins.0
+ddp-client@1.2.0-plugins.0
+ddp-common@1.2.0-plugins.0
+ddp-server@1.2.0-plugins.1
+deps@1.0.8-plugins.0
+diff-sequence@1.0.0-plugins.0
+ecmascript@0.1.3-plugins.1
+ejson@1.0.7-plugins.0
+geojson-utils@1.0.4-plugins.0
+html-tools@1.0.5-plugins.0
+htmljs@1.0.5-plugins.0
+http@1.1.1-plugins.0
+id-map@1.0.4-plugins.0
+jquery@1.11.3-plugins.0_3
+json@1.0.4-plugins.0
+logging@1.0.8-plugins.0
+meteor@1.1.7-plugins.1
+minifiers@1.1.6-plugins.0
+minimongo@1.0.9-plugins.0
+mongo@1.1.1-plugins.0
+mongo-id@1.0.0-plugins.0
+npm-mongo@1.4.32-plugins.0_1
+observe-sequence@1.0.7-plugins.0
+ordered-dict@1.0.4-plugins.0
+promise@0.4.2-plugins.0
+random@1.0.4-plugins.0
+reactive-var@1.0.6-plugins.0
+reload@1.1.4-plugins.0
+retry@1.0.4-plugins.0
+routepolicy@1.0.6-plugins.0
+spacebars@1.0.7-plugins.0
+spacebars-compiler@1.0.7-plugins.1
+static-html@0.0.1
+templating@1.1.2-plugins.1
+templating-tools@0.0.1
+tracker@1.0.8-plugins.0
+ui@1.0.7-plugins.0
+underscore@1.0.4-plugins.0
+url@1.0.5-plugins.0
+webapp@1.2.1-plugins.0
+webapp-hashing@1.0.4-plugins.0
diff --git a/tools/tests/apps/compiler-plugin-static-html-error/static.html b/tools/tests/apps/compiler-plugin-static-html-error/static.html
new file mode 100644
index 0000000000..1ed52a7785
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html-error/static.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+ I have a body, yet no Blaze!
+
diff --git a/tools/tests/apps/compiler-plugin-static-html/.meteor/.gitignore b/tools/tests/apps/compiler-plugin-static-html/.meteor/.gitignore
new file mode 100644
index 0000000000..4083037423
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/tools/tests/apps/compiler-plugin-static-html/.meteor/.id b/tools/tests/apps/compiler-plugin-static-html/.meteor/.id
new file mode 100644
index 0000000000..9f90746a43
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+# - ensuring you don't accidentally deploy one app on top of another
+# - providing package authors with aggregated statistics
+
+o2vi6w1m82bge9k2mhb
diff --git a/tools/tests/apps/compiler-plugin-static-html/.meteor/packages b/tools/tests/apps/compiler-plugin-static-html/.meteor/packages
new file mode 100644
index 0000000000..1a1a3f6cc1
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html/.meteor/packages
@@ -0,0 +1,16 @@
+# Meteor packages used by this project, one per line.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+meteor
+webapp
+logging
+tracker
+ddp
+mongo
+check
+jquery
+reload
+autoupdate
+
+static-html
diff --git a/tools/tests/apps/compiler-plugin-static-html/.meteor/platforms b/tools/tests/apps/compiler-plugin-static-html/.meteor/platforms
new file mode 100644
index 0000000000..8a3a35f9f6
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html/.meteor/platforms
@@ -0,0 +1,2 @@
+browser
+server
diff --git a/tools/tests/apps/compiler-plugin-static-html/.meteor/release b/tools/tests/apps/compiler-plugin-static-html/.meteor/release
new file mode 100644
index 0000000000..621e94f0ec
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html/.meteor/release
@@ -0,0 +1 @@
+none
diff --git a/tools/tests/apps/compiler-plugin-static-html/.meteor/versions b/tools/tests/apps/compiler-plugin-static-html/.meteor/versions
new file mode 100644
index 0000000000..991f343178
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html/.meteor/versions
@@ -0,0 +1,53 @@
+autoupdate@1.2.2-plugins.0
+babel-compiler@5.8.3-plugins.0_5
+babel-runtime@0.1.2
+base64@1.0.4-plugins.0
+binary-heap@1.0.4-plugins.0
+blaze@2.1.3-plugins.0
+blaze-tools@1.0.4-plugins.0
+boilerplate-generator@1.0.4-plugins.0
+caching-compiler@1.0.0-plugins.1
+caching-html-compiler@1.0.0
+callback-hook@1.0.4-plugins.0
+check@1.0.6-plugins.0
+ddp@1.2.0-plugins.0
+ddp-client@1.2.0-plugins.0
+ddp-common@1.2.0-plugins.0
+ddp-server@1.2.0-plugins.1
+deps@1.0.8-plugins.0
+diff-sequence@1.0.0-plugins.0
+ecmascript@0.1.3-plugins.1
+ejson@1.0.7-plugins.0
+geojson-utils@1.0.4-plugins.0
+html-tools@1.0.5-plugins.0
+htmljs@1.0.5-plugins.0
+http@1.1.1-plugins.0
+id-map@1.0.4-plugins.0
+jquery@1.11.3-plugins.0_3
+json@1.0.4-plugins.0
+logging@1.0.8-plugins.0
+meteor@1.1.7-plugins.1
+minifiers@1.1.6-plugins.0
+minimongo@1.0.9-plugins.0
+mongo@1.1.1-plugins.0
+mongo-id@1.0.0-plugins.0
+npm-mongo@1.4.32-plugins.0_1
+observe-sequence@1.0.7-plugins.0
+ordered-dict@1.0.4-plugins.0
+promise@0.4.2-plugins.0
+random@1.0.4-plugins.0
+reactive-var@1.0.6-plugins.0
+reload@1.1.4-plugins.0
+retry@1.0.4-plugins.0
+routepolicy@1.0.6-plugins.0
+spacebars@1.0.7-plugins.0
+spacebars-compiler@1.0.7-plugins.1
+static-html@0.0.1
+templating@1.1.2-plugins.1
+templating-tools@0.0.1
+tracker@1.0.8-plugins.0
+ui@1.0.7-plugins.0
+underscore@1.0.4-plugins.0
+url@1.0.5-plugins.0
+webapp@1.2.1-plugins.0
+webapp-hashing@1.0.4-plugins.0
diff --git a/tools/tests/apps/compiler-plugin-static-html/static.html b/tools/tests/apps/compiler-plugin-static-html/static.html
new file mode 100644
index 0000000000..27685020e5
--- /dev/null
+++ b/tools/tests/apps/compiler-plugin-static-html/static.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+ I have a body, yet no Blaze!
+
diff --git a/tools/tests/static-html.js b/tools/tests/static-html.js
new file mode 100644
index 0000000000..e58a0af915
--- /dev/null
+++ b/tools/tests/static-html.js
@@ -0,0 +1,60 @@
+var _ = require('underscore');
+var selftest = require('../selftest.js');
+var files = require('../files.js');
+import { getUrl } from '../http-helpers.js';
+import { sleepMs } from '../utils.js';
+
+var Sandbox = selftest.Sandbox;
+
+var MONGO_LISTENING =
+ { stdout: " [initandlisten] waiting for connections on port" };
+
+function startRun(sandbox) {
+ var run = sandbox.run();
+ run.waitSecs(90); // Running from checkout can take a _long_ time
+ run.match("myapp");
+ run.match("proxy");
+ run.tellMongo(MONGO_LISTENING);
+ run.match("MongoDB");
+ return run;
+};
+
+// Test that the static-html package works. It's hard to do this from a unit
+// test.
+selftest.define("static-html - add static content to head and body", () => {
+ const s = new Sandbox({ fakeMongo: true });
+
+ s.createApp('myapp', 'compiler-plugin-static-html');
+ s.cd('myapp');
+
+ const run = startRun(s);
+
+ // Test that static content is present in HTML response.
+ const html = getUrl('http://localhost:3000/');
+ selftest.expectTrue(
+ html.indexOf(
+ ``
+ ) !== -1
+ );
+
+ selftest.expectTrue(
+ html.indexOf(
+ `I have a body, yet no Blaze!
`
+ ) !== -1
+ );
+
+ run.stop();
+});
+
+// Test that the static-html package throws the right error
+selftest.define("static-html - throws error", () => {
+ const s = new Sandbox({ fakeMongo: true });
+
+ s.createApp('myapp', 'compiler-plugin-static-html-error');
+ s.cd('myapp');
+
+ const run = startRun(s);
+ run.match("Attributes on not supported");
+
+ run.stop();
+});