diff --git a/packages/caching-compiler/package.js b/packages/caching-compiler/package.js index 46f8aa9dfc..94bcc4fcab 100644 --- a/packages/caching-compiler/package.js +++ b/packages/caching-compiler/package.js @@ -7,7 +7,7 @@ Package.describe({ Npm.depends({ 'lru-cache': '6.0.0' -}) +}); Package.onUse(function(api) { api.use(['ecmascript', 'random']); diff --git a/packages/non-core/blaze b/packages/non-core/blaze index c4e523d7d3..0856ca8bf7 160000 --- a/packages/non-core/blaze +++ b/packages/non-core/blaze @@ -1 +1 @@ -Subproject commit c4e523d7d3be55f4f7784d4729d6675caa48a882 +Subproject commit 0856ca8bf7730fbe1944142642a0c5be82fb9999 diff --git a/packages/static-html-tools/caching-html-compiler.js b/packages/static-html-tools/caching-html-compiler.js new file mode 100644 index 0000000000..f72f6b42b8 --- /dev/null +++ b/packages/static-html-tools/caching-html-compiler.js @@ -0,0 +1,148 @@ +import { CompileError } from './throw-compile-error'; +import isEmpty from 'lodash.isempty'; +import { CachingCompiler } from 'meteor/caching-compiler'; + +const path = Plugin.path; + +// The CompileResult type for this CachingCompiler is the return value of +// htmlScanner.scan: a {js, head, body, bodyAttrs} object. +export 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 + // eslint-disable-next-line class-methods-use-this + compileResultSize(compileResult) { + const lengthOrZero = (field) => field ? field.length : 0; + const headSize = lengthOrZero(compileResult.head); + const bodySize = lengthOrZero(compileResult.body); + const jsSize = lengthOrZero(compileResult.js); + return headSize + bodySize + jsSize; + } + + // Overrides method from CachingCompiler + processFilesForTarget(inputFiles) { + this._bodyAttrInfo = {}; + return super.processFilesForTarget(inputFiles); + } + + // Implements method from CachingCompilerBase + // eslint-disable-next-line class-methods-use-this + getCacheKey(inputFile) { + // Note: the path is only used for errors, so it doesn't have to be part + // of the cache key. + return [ + inputFile.getArch(), + inputFile.getSourceHash(), + inputFile.hmrAvailable && inputFile.hmrAvailable(), + ]; + } + + // Implements method from CachingCompiler + compileOneFile(inputFile) { + const contents = inputFile.getContentsAsString(); + const inputPath = inputFile.getPathInPackage(); + try { + const tags = this.tagScannerFunc({ + sourceName: inputPath, + contents, + tagNames: ['body', 'head', 'template'], + }); + + return this.tagHandlerFunc(tags, inputFile.hmrAvailable && inputFile.hmrAvailable()); + } catch (e) { + if (e instanceof CompileError) { + inputFile.error({ + message: e.message, + line: e.line, + }); + return null; + } + 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 (Object.prototype.hasOwnProperty.call(this._bodyAttrInfo, 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() { + var attrs = ${JSON.stringify(compileResult.bodyAttrs)}; + for (var prop in attrs) { + document.body.setAttribute(prop, attrs[prop]); + } +}); +`; + } + + + 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 += 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/static-html-tools/html-scanner-tests.js b/packages/static-html-tools/html-scanner-tests.js new file mode 100644 index 0000000000..367c75dbc4 --- /dev/null +++ b/packages/static-html-tools/html-scanner-tests.js @@ -0,0 +1,191 @@ +import { TemplatingTools } from 'meteor/templating-tools'; + +Tinytest.add("static-html-tools - html scanner", function (test) { + var testInString = function(actualStr, wantedContents) { + if (actualStr.indexOf(wantedContents) >= 0) + test.ok(); + else + test.fail("Expected "+JSON.stringify(wantedContents)+ + " in "+JSON.stringify(actualStr)); + }; + + var checkError = function(f, msgText, lineNum) { + try { + f(); + } catch (e) { + if (! e instanceof TemplatingTools.CompileError) { + throw e; + } + + if (e.line === lineNum) + test.ok(); + else + test.fail("Error should have been on line " + lineNum + ", not " + + e.line); + testInString(e.message, msgText); + return; + } + test.fail("Parse error didn't throw exception"); + }; + + // returns the appropriate code to put content in the body, + // where content is something simple like the string "Hello" + // (passed in as a source string including the quotes). + var simpleBody = function (content) { + return "\nTemplate.body.addContent((function() {\n var view = this;\n return " + content + ";\n}));\nMeteor.startup(Template.body.renderToDocument);\n"; + }; + + // arguments are quoted strings like '"hello"' + var simpleTemplate = function (templateName, content) { + // '"hello"' into '"Template.hello"' + var viewName = templateName.slice(0, 1) + 'Template.' + templateName.slice(1); + + return '\nTemplate.__checkName(' + templateName + ');\nTemplate[' + templateName + + '] = new Template(' + viewName + + ', (function() {\n var view = this;\n return ' + content + ';\n}));\n'; + }; + + 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 scanForTest("asdf"); + }, "Expected one of: , , ", 1); + + // body all on one line + checkResults( + scanForTest("Hello"), + simpleBody('"Hello"')); + + // multi-line body, contents trimmed + checkResults( + scanForTest("\n\n\n\n\nHello\n\n\n\n\n"), + simpleBody('"Hello"')); + + // same as previous, but with various HTML comments + checkResults( + scanForTest("\n\n\n"+ + "\n\nHello\n\n\n\n'."); + } + + this.throwCompileError(); + } + + // otherwise, a