From 2e428c8ef3472ce1c282339072470bfeefc9065e Mon Sep 17 00:00:00 2001 From: Nathan Muir Date: Tue, 30 Oct 2018 17:39:29 +1000 Subject: [PATCH 1/9] webapp: add runtime config overrides when inline scripts are disabled When generating boilerplate, meteor runtime config includes additional options based on the arch. However, these additional options were not present when generating the response to `/meteor_runtime_config.js`, which is used when inline scripts are disabled. This change fixes Meteor.isModern in those circumstances. --- packages/webapp/webapp_server.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 1fa2daa65c..ccbfb6081f 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -386,12 +386,7 @@ WebAppInternals.staticFilesMiddleware = async function ( res.end(); }; - if (pathname === "/meteor_runtime_config.js" && - ! WebAppInternals.inlineScriptsAllowed()) { - serveStaticJs("__meteor_runtime_config__ = " + - JSON.stringify(__meteor_runtime_config__) + ";"); - return; - } else if (_.has(additionalStaticJs, pathname) && + if (_.has(additionalStaticJs, pathname) && ! WebAppInternals.inlineScriptsAllowed()) { serveStaticJs(additionalStaticJs[pathname]); return; @@ -404,7 +399,14 @@ WebAppInternals.staticFilesMiddleware = async function ( // If pauseClient(arch) has been called, program.paused will be a // Promise that will be resolved when the program is unpaused. - await WebApp.clientPrograms[arch].paused; + const program = WebApp.clientPrograms[arch]; + await program.paused; + + if (path === "/meteor_runtime_config.js" && + ! WebAppInternals.inlineScriptsAllowed()) { + serveStaticJs(`__meteor_runtime_config__ = ${program.meteorRuntimeConfig};`); + return; + } const info = getStaticFileInfo(pathname, path, arch); if (! info) { @@ -789,13 +791,18 @@ function runWebAppServer() { function generateBoilerplateForArch(arch) { const program = WebApp.clientPrograms[arch]; + const additionalOptions = defaultOptionsForArch[arch] || {}; const { baseData } = boilerplateByArch[arch] = WebAppInternals.generateBoilerplateInstance( arch, program.manifest, - defaultOptionsForArch[arch], + additionalOptions, ); - + // we need the runtime config w/ overrides for meteor_runtime_config.js + program.meteorRuntimeConfig = JSON.stringify(_.extend( + _.clone(__meteor_runtime_config__), + additionalOptions.runtimeConfigOverrides || {} + )); program.refreshableAssets = baseData.css.map(file => ({ url: bundledJsCssUrlRewriteHook(file.url), })); @@ -835,7 +842,7 @@ function runWebAppServer() { // Do this before the next middleware destroys req.url if a path prefix // is set to close #10111. app.use(query()); - + function getPathParts(path) { const parts = path.split("/"); while (parts[0] === "") parts.shift(); From 40ac2de412dfe4d3e11ded0738fe88055183c449 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 21 Nov 2018 11:34:01 -0500 Subject: [PATCH 2/9] Use object ...spread syntax rather than _.extend. --- packages/webapp/webapp_server.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index ccbfb6081f..1037d44aa7 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -798,11 +798,11 @@ function runWebAppServer() { program.manifest, additionalOptions, ); - // we need the runtime config w/ overrides for meteor_runtime_config.js - program.meteorRuntimeConfig = JSON.stringify(_.extend( - _.clone(__meteor_runtime_config__), - additionalOptions.runtimeConfigOverrides || {} - )); + // We need the runtime config with overrides for meteor_runtime_config.js: + program.meteorRuntimeConfig = JSON.stringify({ + ...__meteor_runtime_config__, + ...(additionalOptions.runtimeConfigOverrides || null), + }); program.refreshableAssets = baseData.css.map(file => ({ url: bundledJsCssUrlRewriteHook(file.url), })); From fc02c2592cc8939bfc512d09a17ef61d68eabbf7 Mon Sep 17 00:00:00 2001 From: idmadj Date: Fri, 23 Nov 2018 13:19:50 -0500 Subject: [PATCH 3/9] Avoid object shorthand syntax in oauth package. (#10349) Fixes #10347. --- packages/oauth/end_of_popup_response.js | 4 ++-- packages/oauth/package.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/oauth/end_of_popup_response.js b/packages/oauth/end_of_popup_response.js index fd6a1ae20b..ed554160db 100644 --- a/packages/oauth/end_of_popup_response.js +++ b/packages/oauth/end_of_popup_response.js @@ -9,8 +9,8 @@ if (config.isCordova) { var credentialString = JSON.stringify({ - credentialToken, - credentialSecret, + credentialToken: credentialToken, + credentialSecret: credentialSecret }); window.location.hash = credentialString; diff --git a/packages/oauth/package.js b/packages/oauth/package.js index 524b0aeed9..841f327a07 100644 --- a/packages/oauth/package.js +++ b/packages/oauth/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Common code for OAuth-based services", - version: "1.2.6" + version: "1.2.7" }); Package.onUse(api => { From 2a37f61066f97fefdc508d50f0f6cd3b4ab2972b Mon Sep 17 00:00:00 2001 From: Kevin Newman Date: Fri, 23 Nov 2018 10:21:09 -0800 Subject: [PATCH 4/9] Use setTimeout in prefetchInChunks to avoid blocking UI (#10354) Addresses #10350. --- packages/dynamic-import/dynamic-versions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/dynamic-import/dynamic-versions.js b/packages/dynamic-import/dynamic-versions.js index 41b749a2f7..93494a9a7f 100644 --- a/packages/dynamic-import/dynamic-versions.js +++ b/packages/dynamic-import/dynamic-versions.js @@ -69,7 +69,9 @@ function precacheOnLoad(event) { return module.prefetch(id); })).then(function () { if (modules.length > 0) { - prefetchInChunks(modules, amount); + setTimeout(function () { + prefetchInChunks(modules, amount); + }, 0); } }); } From 4aad077a9fa6bf414d723e8994c18c27494aeb1b Mon Sep 17 00:00:00 2001 From: Hugh Willson Date: Fri, 23 Nov 2018 14:04:49 -0500 Subject: [PATCH 5/9] Remove Blaze dependencies from static-html (#10267) These changes create a new copy of the static-html and caching-html-compiler packages in core, as well as a new package called html-scanner, to house the html-scanner.js functionality from the templating-tools package. With these changes in place, we're able to remove all Blaze dependencies from static-html, which benefits React based Meteor apps. We don't need the extra `CompileError` class, and using it was throwing off `caching-html-compiler` error handling. Errors with messages and line numbers weren't being interpreted / formatted properly. --- History.md | 22 +- packages/caching-html-compiler/README.md | 49 +++++ .../caching-html-compiler.js | 147 ++++++++++++++ packages/caching-html-compiler/package.js | 19 ++ packages/html-scanner/README.md | 54 +++++ packages/html-scanner/html-scanner-tests.js | 191 ++++++++++++++++++ packages/html-scanner/html-scanner.js | 172 ++++++++++++++++ packages/html-scanner/package.js | 20 ++ packages/static-html/README.md | 14 ++ packages/static-html/package.js | 25 +++ packages/static-html/static-html.js | 100 +++++++++ 11 files changed, 804 insertions(+), 9 deletions(-) create mode 100644 packages/caching-html-compiler/README.md create mode 100644 packages/caching-html-compiler/caching-html-compiler.js create mode 100644 packages/caching-html-compiler/package.js create mode 100644 packages/html-scanner/README.md create mode 100644 packages/html-scanner/html-scanner-tests.js create mode 100644 packages/html-scanner/html-scanner.js create mode 100644 packages/html-scanner/package.js create mode 100644 packages/static-html/README.md create mode 100644 packages/static-html/package.js create mode 100644 packages/static-html/static-html.js diff --git a/History.md b/History.md index 66ccbfed23..b3f61a53e9 100644 --- a/History.md +++ b/History.md @@ -3,11 +3,15 @@ ### Breaking changes N/A -### Migration steps +### Migration Steps N/A ### Changes +* The `static-html` package is now part of the Meteor core, and no longer has + any dependencies on Blaze templating tools. + [PR #10267](https://github.com/meteor/meteor/pull/10267) + ## v1.8.0.1, 2018-11-23 ### Breaking changes @@ -419,7 +423,7 @@ N/A ### Migration Steps -* Update `@babel/runtime` (as well as other Babel-related packages) and +* Update `@babel/runtime` (as well as other Babel-related packages) and `meteor-node-stubs` to their latest versions: ```sh meteor npm install @babel/runtime@latest meteor-node-stubs@latest @@ -804,7 +808,7 @@ N/A N/A ### Migration Steps -* Update `@babel/runtime` npm package and any custom Babel plugin enabled in +* Update `@babel/runtime` npm package and any custom Babel plugin enabled in `.babelrc` ```sh meteor npm install @babel/runtime@latest @@ -839,7 +843,7 @@ N/A values are not first converted to `null`, when inserted/updated. `undefined` values are now removed from all Mongo queries and insert/update documents. - This is a potentially breaking change if you are upgrading an existing app + This is a potentially breaking change if you are upgrading an existing app from an earlier version of Meteor. For example: @@ -849,11 +853,11 @@ N/A userId: currentUser._id // undefined }); ``` - Assuming there are no documents in the `privateUserData` collection with - `userId: null`, in Meteor versions prior to 1.6.1 this query will return - zero documents. From Meteor 1.6.1 onwards, this query will now return - _every_ document in the collection. It is highly recommend you review all - your existing queries to ensure that any potential usage of `undefined` in + Assuming there are no documents in the `privateUserData` collection with + `userId: null`, in Meteor versions prior to 1.6.1 this query will return + zero documents. From Meteor 1.6.1 onwards, this query will now return + _every_ document in the collection. It is highly recommend you review all + your existing queries to ensure that any potential usage of `undefined` in query objects won't lead to problems. ### Migration Steps diff --git a/packages/caching-html-compiler/README.md b/packages/caching-html-compiler/README.md new file mode 100644 index 0000000000..b1d461a55a --- /dev/null +++ b/packages/caching-html-compiler/README.md @@ -0,0 +1,49 @@ +# 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..68a415b285 --- /dev/null +++ b/packages/caching-html-compiler/caching-html-compiler.js @@ -0,0 +1,147 @@ +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(); + + // Since we can't control node_modules based HTML content, we'll skip + // over it here to avoid trying to handle HTML files that don't + // fit within Meteor's HTML file handling rules (e.g. Meteor doesn't + // allow files that have a DOCTYPE specified, since it adds its own). + if (!inputPath.startsWith('node_modules')) { + try { + const tags = this.tagScannerFunc({ + sourceName: inputPath, + contents: contents, + tagNames: ["body", "head", "template"] + }); + return this.tagHandlerFunc(tags); + } catch (e) { + if (e.message && e.line) { + 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 (Object.keys(compileResult.bodyAttrs).length !== 0) { + 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() { + 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 = 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..703ce6382b --- /dev/null +++ b/packages/caching-html-compiler/package.js @@ -0,0 +1,19 @@ +Package.describe({ + name: 'caching-html-compiler', + summary: "Pluggable class for compiling HTML into templates", + version: '1.1.3', + git: 'https://github.com/meteor/meteor.git' +}); + +Package.onUse(function (api) { + api.use([ + 'caching-compiler', + 'ecmascript' + ]); + + api.export('CachingHtmlCompiler', 'server'); + + api.addFiles([ + 'caching-html-compiler.js' + ], 'server'); +}); diff --git a/packages/html-scanner/README.md b/packages/html-scanner/README.md new file mode 100644 index 0000000000..81a238cbdf --- /dev/null +++ b/packages/html-scanner/README.md @@ -0,0 +1,54 @@ +# html-scanner + +## `scanHtmlForTags(options)` + +Scan an HTML file for top-level tags as specified by `options.tagNames`, and +return an array of `Tag` objects. + +### 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"] +}); +``` + +### Tag object + +```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/html-scanner/html-scanner-tests.js b/packages/html-scanner/html-scanner-tests.js new file mode 100644 index 0000000000..e30add08d0 --- /dev/null +++ b/packages/html-scanner/html-scanner-tests.js @@ -0,0 +1,191 @@ +import { scanHtmlForTags, CompileError } from './html-scanner.js'; + +Tinytest.add("html-scanner - 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.line) { + 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 = scanHtmlForTags({ + sourceName: "", + contents: contents, + tagNames: ["body", "head", "template"] + }); + + return TemplatingTools.compileTagsWithSpacebars(tags); + } + + checkError(function() { + return scanForTest("asdf"); + }, "Expected one of: , ,