From a5866e2781189916868105efeec8448eddb67e5d Mon Sep 17 00:00:00 2001 From: James Baxley Date: Fri, 10 Nov 2017 09:37:46 -0500 Subject: [PATCH 01/14] initial working stream support --- packages/boilerplate-generator/generator.js | 38 +++++- .../template-web.browser.js | 124 +++++++++--------- .../template-web.cordova.js | 122 +++++++++-------- .../.npm/package/npm-shrinkwrap.json | 11 +- packages/server-render/package.js | 1 + packages/server-render/server-register.js | 56 ++++++-- packages/server-render/server-sink.js | 31 ++++- packages/webapp/webapp_server.js | 16 ++- 8 files changed, 245 insertions(+), 154 deletions(-) diff --git a/packages/boilerplate-generator/generator.js b/packages/boilerplate-generator/generator.js index f555ec46a1c..511ed8db256 100644 --- a/packages/boilerplate-generator/generator.js +++ b/packages/boilerplate-generator/generator.js @@ -1,4 +1,5 @@ import { readFile } from 'fs'; +import { Readable } from 'stream'; import WebBrowserTemplate from './template-web.browser'; import WebCordovaTemplate from './template-web.cordova'; @@ -10,7 +11,9 @@ const identity = value => value; export class Boilerplate { constructor(arch, manifest, options = {}) { - this.template = _getTemplate(arch); + const { headTemplate, closeTemplate } = _getTemplate(arch); + this.headTemplate = headTemplate; + this.closeTemplate = closeTemplate; this.baseData = null; this._generateBoilerplateFromManifest( @@ -19,17 +22,44 @@ export class Boilerplate { ); } + stringToStream(str) { + if (!str) str = ""; + + // this is a stream + if (typeof str !== "string") return str; + + const stream = new Readable(); + stream._read = () => {}; + stream.push(str); + // end the stream + stream.push(null); + + return stream; + } + // The 'extraData' argument can be used to extend 'self.baseData'. Its // purpose is to allow you to specify data that you might not know at // the time that you construct the Boilerplate object. (e.g. it is used // by 'webapp' to specify data that is only known at request-time). + // this returns three writeable objects: + // - a head that is a string for immediate flushing + // - a stream of the body + // - a closing body and scripts to flush and end the req toHTML(extraData) { - if (!this.baseData || !this.template) { + if (!this.baseData || !this.headTemplate || !this.closeTemplate) { throw new Error('Boilerplate did not instantiate correctly.'); } - return "\n" + - this.template({ ...this.baseData, ...extraData }); + const data = {...this.baseData, ...extraData}; + const start = "\n" + this.headTemplate(data); + + const { body, dynamicBody } = data; + const stream = this.stringToStream(body) + // .pipe(this.stringToStream(dynamicBody)); + + const end = this.closeTemplate(data); + + return { start, stream, end } } // XXX Exported to allow client-side only changes to rebuild the boilerplate diff --git a/packages/boilerplate-generator/template-web.browser.js b/packages/boilerplate-generator/template-web.browser.js index bba061d5594..67e3b5349b8 100644 --- a/packages/boilerplate-generator/template-web.browser.js +++ b/packages/boilerplate-generator/template-web.browser.js @@ -1,76 +1,76 @@ import template from './template'; +export const headTemplate = ({ + css, + htmlAttributes, + bundledJsCssUrlRewriteHook, + head, + dynamicHead, +}) => [].concat( + [ + ' + template(' <%= attrName %>="<%- attrValue %>"')({ + attrName: key, + attrValue: htmlAttributes[key] + }) + ).join('') + '>', + '' + ], + + (css || []).map(({ url }) => + template(' ')({ + href: bundledJsCssUrlRewriteHook(url) + }) + ), + [ + head, + dynamicHead, + '', + '', + ], +).join('\n') + // Template function for rendering the boilerplate html for browsers -export default function({ +export const closeTemplate = ({ meteorRuntimeConfig, rootUrlPathPrefix, inlineScriptsAllowed, - css, js, additionalStaticJs, - htmlAttributes, bundledJsCssUrlRewriteHook, - head, - body, - dynamicHead, - dynamicBody, -}) { - return [].concat( - [ - ' - template(' <%= attrName %>="<%- attrValue %>"')({ - attrName: key, - attrValue: htmlAttributes[key] - }) - ).join('') + '>', - '' - ], - - (css || []).map(({ url }) => - template(' ')({ - href: bundledJsCssUrlRewriteHook(url) +}) => [].concat( + [ + '', + (inlineScriptsAllowed + ? template(' ')({ + conf: meteorRuntimeConfig + }) + : template(' ')({ + src: rootUrlPathPrefix }) - ), + ) , + '' + ], - [ - head, - dynamicHead, - '', - '', - body, - dynamicBody, - '', - (inlineScriptsAllowed - ? template(' ')({ - conf: meteorRuntimeConfig - }) - : template(' ')({ - src: rootUrlPathPrefix - }) - ) , - '' - ], + (js || []).map(({ url }) => + template(' ')({ + src: bundledJsCssUrlRewriteHook(url) + }) + ), - (js || []).map(({ url }) => - template(' ')({ - src: bundledJsCssUrlRewriteHook(url) + (additionalStaticJs || []).map(({ contents, pathname }) => ( + (inlineScriptsAllowed + ? template(' ')({ + contents: contents }) - ), - - (additionalStaticJs || []).map(({ contents, pathname }) => ( - (inlineScriptsAllowed - ? template(' ')({ - contents: contents - }) - : template(' ')({ - src: rootUrlPathPrefix + pathname - })) - )), + : template(' ')({ + src: rootUrlPathPrefix + pathname + })) + )), - [ - '', '', - '', - '' - ], - ).join('\n'); -} + [ + '', '', + '', + '' + ], +).join('\n'); diff --git a/packages/boilerplate-generator/template-web.cordova.js b/packages/boilerplate-generator/template-web.cordova.js index 68a82bfcf03..b6a9a013931 100644 --- a/packages/boilerplate-generator/template-web.cordova.js +++ b/packages/boilerplate-generator/template-web.cordova.js @@ -1,7 +1,7 @@ import template from './template'; // Template function for rendering the boilerplate html for cordova -export default function({ +export const headTemplate = ({ meteorRuntimeConfig, rootUrlPathPrefix, inlineScriptsAllowed, @@ -11,69 +11,65 @@ export default function({ htmlAttributes, bundledJsCssUrlRewriteHook, head, - body, dynamicHead, - dynamicBody, -}) { - return [].concat( - [ - '', - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ], - // We are explicitly not using bundledJsCssUrlRewriteHook: in cordova we serve assets up directly from disk, so rewriting the URL does not make sense - (css || []).map(({ url }) => - template(' ')({ - href: url - }) - ), - [ - ' ', - '', - ' ' - ], - (js || []).map(({ url }) => - template(' ')({ - src: url +}) => [].concat( + [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ], + // We are explicitly not using bundledJsCssUrlRewriteHook: in cordova we serve assets up directly from disk, so rewriting the URL does not make sense + (css || []).map(({ url }) => + template(' ')({ + href: url + }) + ), + [ + ' ', + '', + ' ' + ], + (js || []).map(({ url }) => + template(' ')({ + src: url + }) + ), + + (additionalStaticJs || []).map(({ contents, pathname }) => ( + (inlineScriptsAllowed + ? template(' ')({ + contents: contents }) - ), + : template(' ')({ + src: rootUrlPathPrefix + pathname + })) + )), - (additionalStaticJs || []).map(({ contents, pathname }) => ( - (inlineScriptsAllowed - ? template(' ')({ - contents: contents - }) - : template(' ')({ - src: rootUrlPathPrefix + pathname - })) - )), + [ + '', + head, + '', + '', + '', + ], +).join('\n'); - [ - '', - head, - '', - '', - '', - body, - '', - '' - ], - ).join('\n'); -} +export const closeTemplate = () => + [].concat([ '','' ]).join('\n'); diff --git a/packages/server-render/.npm/package/npm-shrinkwrap.json b/packages/server-render/.npm/package/npm-shrinkwrap.json index 9909fab1c6a..412a14c5e78 100644 --- a/packages/server-render/.npm/package/npm-shrinkwrap.json +++ b/packages/server-render/.npm/package/npm-shrinkwrap.json @@ -1,6 +1,11 @@ { "lockfileVersion": 1, "dependencies": { + "combine-streams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/combine-streams/-/combine-streams-1.0.0.tgz", + "integrity": "sha1-U2W6BKdmGM8zshPDFtM+XBs8RgQ=" + }, "magic-string": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.21.3.tgz", @@ -12,9 +17,9 @@ "integrity": "sha1-Be/1fw70V3+xRKefi5qWemzERRA=" }, "vlq": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.2.tgz", - "integrity": "sha1-4xbVJXtAuGu0PLjV/qXX9U1rDKE=" + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", + "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==" } } } diff --git a/packages/server-render/package.js b/packages/server-render/package.js index 8787212d906..f3bf1cb5d72 100644 --- a/packages/server-render/package.js +++ b/packages/server-render/package.js @@ -6,6 +6,7 @@ Package.describe({ }); Npm.depends({ + "combine-streams": "1.0.0", "magic-string": "0.21.3", "parse5": "3.0.2" }); diff --git a/packages/server-render/server-register.js b/packages/server-render/server-register.js index 8fecf9e2a96..8ae8dc00224 100644 --- a/packages/server-render/server-register.js +++ b/packages/server-render/server-register.js @@ -1,9 +1,19 @@ import { WebAppInternals } from "meteor/webapp"; -import { SAXParser } from "parse5"; +import { Readable } from "stream"; import MagicString from "magic-string"; -import { ServerSink } from "./server-sink.js"; +import { SAXParser } from "parse5"; +import combine from "combine-streams"; +import { ServerSink, isReadable } from "./server-sink.js"; import { onPageLoad } from "./server.js"; +function stringToStream(str) { + const stream = new Readable(); + stream._read = function() {}; + stream.push(str); + stream.push(null);; + return stream; +} + WebAppInternals.registerBoilerplateDataCallback( "meteor/server-render", (request, data, arch) => { @@ -29,22 +39,40 @@ WebAppInternals.registerBoilerplateDataCallback( locationInfo: true }); - parser.on("startTag", (name, attrs, selfClosing, loc) => { - attrs.some(attr => { - if (attr.name === "id") { - const html = sink.htmlById[attr.value]; - if (html) { - magic.appendRight(loc.endOffset, html); - reallyMadeChanges = true; + data[property] = parser; + + if (Object.keys(sink.htmlById).length) { + // create an empty stream; + const stream = combine(); + + let lastStart = magic.start; + parser.on("startTag", (name, attrs, selfClosing, loc) => { + attrs.some(attr => { + if (attr.name === "id") { + let html = sink.htmlById[attr.value]; + if (typeof html === "string") { + html = stringToStream(html); + } + if (html && isReadable(html)) { + reallyMadeChanges = true; + const start = magic.slice(lastStart, loc.endOffset); + const end = magic.slice(loc.endOffset); + stream + .append(start) + .append(html) + .append(end) + .append(null); + lastStart = loc.endOffset; + } + return true; } - return true; - } + }); }); - }); - parser.write(html); + data[property] = stream; + } - data[property] = magic.toString(); + parser.write(html, parser.end.bind(parser)); } if (sink.head) { diff --git a/packages/server-render/server-sink.js b/packages/server-render/server-sink.js index d6cecebdc90..8680889e7ee 100644 --- a/packages/server-render/server-sink.js +++ b/packages/server-render/server-sink.js @@ -30,6 +30,30 @@ export class ServerSink { this.htmlById[id] = ""; this.appendToElementById(id, html); } + + redirect(code, location) { + + } + + // server only methods + setStatusCode(code) { + + } + + getHeaders() { + + } +} + +export function isReadable(stream) { + return ( + stream !== null && + typeof stream === 'object' && + typeof stream.pipe === 'function' && + stream.readable !== false && + typeof stream._read === 'function' && + typeof stream._readableState === 'object' + ); } function appendContent(object, property, content) { @@ -41,10 +65,13 @@ function appendContent(object, property, content) { madeChanges = true; } }); + } else if (isReadable(content)) { + object[property] = content; + madeChanges = true; } else if ((content = content && content.toString("utf8"))) { object[property] = (object[property] || "") + content; madeChanges = true; - } - + // should we join streams here? + } return madeChanges; } diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 5f4be983a50..8e78ce7f181 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -314,9 +314,9 @@ function getBoilerplateAsync(request, arch) { madeChanges ); - if (! useMemoized) { + // if (! useMemoized) { return boilerplate.toHTML(data); - } + // } // The only thing that changes from request to request (unless extra // content is added to the head or body, or boilerplateDataCallbacks @@ -799,12 +799,16 @@ function runWebAppServer() { return getBoilerplateAsync( request, archKey - ).then(boilerplate => { + ).then(({ start, stream, end }) => { var statusCode = res.statusCode ? res.statusCode : 200; res.writeHead(statusCode, headers); - res.write(boilerplate); - res.end(); - }, error => { + res.write(start); + stream.pipe(res, { end: false }) + stream.on("end", () => { + res.write(end); + res.end(); + }) + }).catch(error => { Log.error("Error running template: " + error.stack); res.writeHead(500, headers); res.end(); From 2ef27e415193e19b7ab2867e25a64f785863f984 Mon Sep 17 00:00:00 2001 From: James Baxley Date: Fri, 10 Nov 2017 09:47:24 -0500 Subject: [PATCH 02/14] easier support for strings in template --- .../.npm/package/.gitignore | 1 + .../boilerplate-generator/.npm/package/README | 7 ++++++ .../.npm/package/npm-shrinkwrap.json | 10 ++++++++ packages/boilerplate-generator/generator.js | 23 ++++--------------- packages/boilerplate-generator/package.js | 4 ++++ 5 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 packages/boilerplate-generator/.npm/package/.gitignore create mode 100644 packages/boilerplate-generator/.npm/package/README create mode 100644 packages/boilerplate-generator/.npm/package/npm-shrinkwrap.json diff --git a/packages/boilerplate-generator/.npm/package/.gitignore b/packages/boilerplate-generator/.npm/package/.gitignore new file mode 100644 index 00000000000..3c3629e647f --- /dev/null +++ b/packages/boilerplate-generator/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/boilerplate-generator/.npm/package/README b/packages/boilerplate-generator/.npm/package/README new file mode 100644 index 00000000000..3d492553a43 --- /dev/null +++ b/packages/boilerplate-generator/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/boilerplate-generator/.npm/package/npm-shrinkwrap.json b/packages/boilerplate-generator/.npm/package/npm-shrinkwrap.json new file mode 100644 index 00000000000..1357f2b8145 --- /dev/null +++ b/packages/boilerplate-generator/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,10 @@ +{ + "lockfileVersion": 1, + "dependencies": { + "combine-streams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/combine-streams/-/combine-streams-1.0.0.tgz", + "integrity": "sha1-U2W6BKdmGM8zshPDFtM+XBs8RgQ=" + } + } +} diff --git a/packages/boilerplate-generator/generator.js b/packages/boilerplate-generator/generator.js index 511ed8db256..9f5a383f1d2 100644 --- a/packages/boilerplate-generator/generator.js +++ b/packages/boilerplate-generator/generator.js @@ -1,5 +1,5 @@ import { readFile } from 'fs'; -import { Readable } from 'stream'; +import combine from "combine-streams"; import WebBrowserTemplate from './template-web.browser'; import WebCordovaTemplate from './template-web.cordova'; @@ -22,21 +22,6 @@ export class Boilerplate { ); } - stringToStream(str) { - if (!str) str = ""; - - // this is a stream - if (typeof str !== "string") return str; - - const stream = new Readable(); - stream._read = () => {}; - stream.push(str); - // end the stream - stream.push(null); - - return stream; - } - // The 'extraData' argument can be used to extend 'self.baseData'. Its // purpose is to allow you to specify data that you might not know at // the time that you construct the Boilerplate object. (e.g. it is used @@ -54,8 +39,10 @@ export class Boilerplate { const start = "\n" + this.headTemplate(data); const { body, dynamicBody } = data; - const stream = this.stringToStream(body) - // .pipe(this.stringToStream(dynamicBody)); + const stream = combine() + .append(body || "") + .append(dynamicBody || "") + .append(null); const end = this.closeTemplate(data); diff --git a/packages/boilerplate-generator/package.js b/packages/boilerplate-generator/package.js index bd2710ccc05..bb93ae55936 100644 --- a/packages/boilerplate-generator/package.js +++ b/packages/boilerplate-generator/package.js @@ -3,6 +3,10 @@ Package.describe({ version: '1.3.1' }); +Npm.depends({ + "combine-streams": "1.0.0" +}); + Package.onUse(api => { api.use('ecmascript'); api.use('underscore', 'server'); From 037a0be319c1dac225cc118b002c568dd5abf83f Mon Sep 17 00:00:00 2001 From: James Baxley Date: Fri, 10 Nov 2017 09:57:07 -0500 Subject: [PATCH 03/14] simplify stream parsing [ci skip] --- packages/server-render/server-register.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/server-render/server-register.js b/packages/server-render/server-register.js index 8ae8dc00224..d1e2d2a009f 100644 --- a/packages/server-render/server-register.js +++ b/packages/server-render/server-register.js @@ -1,18 +1,10 @@ import { WebAppInternals } from "meteor/webapp"; -import { Readable } from "stream"; import MagicString from "magic-string"; import { SAXParser } from "parse5"; import combine from "combine-streams"; import { ServerSink, isReadable } from "./server-sink.js"; import { onPageLoad } from "./server.js"; -function stringToStream(str) { - const stream = new Readable(); - stream._read = function() {}; - stream.push(str); - stream.push(null);; - return stream; -} WebAppInternals.registerBoilerplateDataCallback( "meteor/server-render", @@ -50,10 +42,7 @@ WebAppInternals.registerBoilerplateDataCallback( attrs.some(attr => { if (attr.name === "id") { let html = sink.htmlById[attr.value]; - if (typeof html === "string") { - html = stringToStream(html); - } - if (html && isReadable(html)) { + if (html) { reallyMadeChanges = true; const start = magic.slice(lastStart, loc.endOffset); const end = magic.slice(loc.endOffset); From 3a7f34d610da1e23790bdbd3ae78b1b2b5dc27dc Mon Sep 17 00:00:00 2001 From: James Baxley Date: Fri, 10 Nov 2017 11:04:51 -0500 Subject: [PATCH 04/14] support redirect, headers, cookies, and client side alignment --- packages/server-render/client-sink.js | 27 +++++++++++++++++++++++ packages/server-render/server-register.js | 10 +++++++++ packages/server-render/server-sink.js | 22 ++++++++++++++---- packages/webapp/webapp_server.js | 19 ++++++++++++---- 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/packages/server-render/client-sink.js b/packages/server-render/client-sink.js index 3743e278f9a..4a02b251afb 100644 --- a/packages/server-render/client-sink.js +++ b/packages/server-render/client-sink.js @@ -2,6 +2,10 @@ const doc = document; const head = doc.getElementsByTagName("head")[0]; const body = doc.body; +const isoError = (method) => { + return `sink.${method} was called on the client when + it should only be called on the server.`; +} export class ClientSink { appendToHead(nodeOrHtml) { appendContent(head, nodeOrHtml); @@ -22,8 +26,31 @@ export class ClientSink { } appendContent(element, nodeOrHtml); } + + redirect(location) { + // code can't be set on the client + window.location = location; + } + + // server only methods + setStatusCode() { + console.error(isoError("setStatusCode")); + } + + setHeader() { + console.error(isoError("setHeader")); + } + + getHeaders() { + console.error(isoError("getHeaders")); + } + + getCookies() { + console.error(isoError("getCookies")); + } } + function appendContent(destination, nodeOrHtml) { if (typeof nodeOrHtml === "string") { // Make a shallow clone of the destination node to ensure the new diff --git a/packages/server-render/server-register.js b/packages/server-render/server-register.js index d1e2d2a009f..8a97800009a 100644 --- a/packages/server-render/server-register.js +++ b/packages/server-render/server-register.js @@ -81,6 +81,16 @@ WebAppInternals.registerBoilerplateDataCallback( reallyMadeChanges = true; } + if (sink.statusCode) { + data.statusCode = sink.statusCode; + reallyMadeChanges = true; + } + + if (Object.keys(sink.responseHeaders)){ + data.headers = sink.responseHeaders; + reallyMadeChanges = true; + } + return reallyMadeChanges; }); } diff --git a/packages/server-render/server-sink.js b/packages/server-render/server-sink.js index 8680889e7ee..48f69366438 100644 --- a/packages/server-render/server-sink.js +++ b/packages/server-render/server-sink.js @@ -6,6 +6,8 @@ export class ServerSink { this.body = ""; this.htmlById = Object.create(null); this.maybeMadeChanges = false; + this.statusCode = null; + this.responseHeaders = {}; } appendToHead(html) { @@ -31,17 +33,29 @@ export class ServerSink { this.appendToElementById(id, html); } - redirect(code, location) { - + redirect(location, code) { + this.maybeMadeChanges = true; + this.statusCode = code; + this.responseHeaders.Location = location; } // server only methods setStatusCode(code) { - + this.maybeMadeChanges = true; + this.statusCode = code; + } + + setHeader(key, value) { + this.maybeMadeChanges = true; + this.responseHeaders[key] = value; } getHeaders() { - + return this.request.headers; + } + + getCookies() { + return this.request.cookies; } } diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 8e78ce7f181..4f587113c7c 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -122,7 +122,7 @@ WebApp.categorizeRequest = function (req) { return _.extend({ browser: identifyBrowser(req.headers['user-agent']), url: parseUrl(req.url, true) - }, _.pick(req, 'dynamicHead', 'dynamicBody')); + }, _.pick(req, 'dynamicHead', 'dynamicBody', 'headers', 'cookies')); }; // HTML attribute hooks: functions to be called to determine any attributes to @@ -315,7 +315,11 @@ function getBoilerplateAsync(request, arch) { ); // if (! useMemoized) { - return boilerplate.toHTML(data); + return { + ...boilerplate.toHTML(data), + statusCode: data.statusCode, + headers: data.headers, + }; // } // The only thing that changes from request to request (unless extra @@ -799,8 +803,15 @@ function runWebAppServer() { return getBoilerplateAsync( request, archKey - ).then(({ start, stream, end }) => { - var statusCode = res.statusCode ? res.statusCode : 200; + ).then(({ start, stream, end, statusCode, headers: newHeaders }) => { + if (!statusCode) { + statusCode = res.statusCode ? res.statusCode : 200; + } + if (newHeaders) { + headers = {...headers, ...newHeaders }; + } + + console.log("setting status code") res.writeHead(statusCode, headers); res.write(start); stream.pipe(res, { end: false }) From fb4e2b6f73e7fab9d13d2043b3bf36977ab2cc5e Mon Sep 17 00:00:00 2001 From: James Baxley Date: Fri, 10 Nov 2017 13:07:59 -0500 Subject: [PATCH 05/14] fix tests --- .../boilerplate-generator-tests/test-lib.js | 28 +- .../web.browser-tests.js | 125 +++++---- .../web.cordova-tests.js | 109 ++++---- packages/server-render/server-register.js | 10 +- packages/server-render/server-render-tests.js | 188 +++++++------ packages/server-render/server-sink.js | 1 - packages/webapp/webapp_server.js | 1 - packages/webapp/webapp_tests.js | 261 ++++++++++-------- 8 files changed, 417 insertions(+), 306 deletions(-) diff --git a/packages/boilerplate-generator-tests/test-lib.js b/packages/boilerplate-generator-tests/test-lib.js index 98b445b7c3a..aa743329476 100644 --- a/packages/boilerplate-generator-tests/test-lib.js +++ b/packages/boilerplate-generator-tests/test-lib.js @@ -1,4 +1,4 @@ -export function generateHTMLForArch(arch) { +export async function generateHTMLForArch(arch) { // Use a dummy manifest. None of these paths will be read from the filesystem, but css / js should be handled differently const manifest = [ { @@ -50,5 +50,29 @@ export function generateHTMLForArch(arch) { }, }); - return boilerplate.toHTML(); + + const { start, stream, end } = boilerplate.toHTML(); + + const body = await toString(stream); + + return start + body + end; }; + + +function toString(stream) { + return new Promise((success, fail) => { + var string = '' + stream.on('readable', function(buffer) { + var part = buffer.read().toString(); + string += part; + }); + + stream.on('end', function() { + success(string) + }); + + stream.on('error', function(error) { + fail(error); + }); + }); +} diff --git a/packages/boilerplate-generator-tests/web.browser-tests.js b/packages/boilerplate-generator-tests/web.browser-tests.js index f69df8dbeff..dbf383cd192 100644 --- a/packages/boilerplate-generator-tests/web.browser-tests.js +++ b/packages/boilerplate-generator-tests/web.browser-tests.js @@ -2,62 +2,69 @@ import { parse, serialize } from 'parse5'; import { generateHTMLForArch } from './test-lib'; -const html = generateHTMLForArch('web.browser'); - -Tinytest.add("boilerplate-generator-tests - web.browser - well-formed html", function (test) { - const formatted = serialize(parse(html)); - test.isTrue(formatted.replace(/\s/g, '') === html.replace(/\s/g, '')); -}); - -Tinytest.add("boilerplate-generator-tests - web.browser - include htmlAttributes", function (test) { - test.matches(html, /foo="foobar"/); -}); - -Tinytest.add("boilerplate-generator-tests - web.browser - escape htmlAttributes", function (test) { - test.matches(html, /gems="&""/); -}); - -Tinytest.add("boilerplate-generator-tests - web.browser - include js", function (test) { - test.matches(html, /]*src="[^<>]*templating[^<>]*">/); -}); - -Tinytest.add("boilerplate-generator-tests - web.browser - escape js", function (test) { - test.matches(html, /]*src="[^<>]*templating[^<>]*&v="1"[^<>]*">/); -}); - -Tinytest.add("boilerplate-generator-tests - web.browser - include css", function (test) { - test.matches(html, /]*href="[^<>]*bootstrap[^<>]*">/); -}); - -Tinytest.add("boilerplate-generator-tests - web.browser - escape css", function (test) { - test.matches(html, /]*href="[^<>]*bootstrap[^<>]*&v="1"[^<>]*">/); -}); - -Tinytest.add("boilerplate-generator-tests - web.browser - call rewriteHook", function (test) { - test.matches(html, /\+rewritten_url=true/); -}); - -Tinytest.add("boilerplate-generator-tests - web.browser - include runtime config", function (test) { - test.matches(html, /]*>[^<>]*__meteor_runtime_config__ =.*decodeURIComponent\(config123\)/); -}); - -// https://github.com/meteor/meteor/issues/9149 -Tinytest.add( - "boilerplate-generator-tests - web.browser - properly render boilerplate " + - "elements when _.template settings are overridden", - function (test) { - import { _ } from 'meteor/underscore'; - _.templateSettings = { - interpolate: /\{\{(.+?)\}\}/g - }; - const newHtml = generateHTMLForArch('web.browser'); - test.matches(newHtml, /foo="foobar"/); - test.matches(newHtml, /]*href="[^<>]*bootstrap[^<>]*">/); - test.matches(newHtml, /]*src="[^<>]*templating[^<>]*">/); - test.matches(newHtml, /')({ - conf: meteorRuntimeConfig - }) - : template(' ')({ - src: rootUrlPathPrefix - }) - ) , - '' - ], +}) => [ + '', + inlineScriptsAllowed + ? template(' ')({ + conf: meteorRuntimeConfig, + }) + : template(' ')({ + src: rootUrlPathPrefix, + }), + '', - (js || []).map(({ url }) => + ...(js || []).map(file => template(' ')({ - src: bundledJsCssUrlRewriteHook(url) + src: bundledJsCssUrlRewriteHook(file.url), }) ), - (additionalStaticJs || []).map(({ contents, pathname }) => ( - (inlineScriptsAllowed + ...(additionalStaticJs || []).map(({ contents, pathname }) => ( + inlineScriptsAllowed ? template(' ')({ - contents: contents + contents, }) : template(' ')({ - src: rootUrlPathPrefix + pathname - })) + src: rootUrlPathPrefix + pathname, + }) )), - [ - '', '', - '', - '' - ], -).join('\n'); + '', + '', + '', + '' +].join('\n'); diff --git a/packages/boilerplate-generator/template-web.cordova.js b/packages/boilerplate-generator/template-web.cordova.js index b6a9a013931..a2870e3066a 100644 --- a/packages/boilerplate-generator/template-web.cordova.js +++ b/packages/boilerplate-generator/template-web.cordova.js @@ -12,64 +12,61 @@ export const headTemplate = ({ bundledJsCssUrlRewriteHook, head, dynamicHead, -}) => [].concat( - [ - '', - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ], +}) => [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + // We are explicitly not using bundledJsCssUrlRewriteHook: in cordova we serve assets up directly from disk, so rewriting the URL does not make sense - (css || []).map(({ url }) => + ...(css || []).map(file => template(' ')({ - href: url + href: file.url, }) ), - [ - ' ', - '', - ' ' - ], - (js || []).map(({ url }) => + + ' ', + '', + ' ', + + ...(js || []).map(file => template(' ')({ - src: url + src: file.url, }) ), - (additionalStaticJs || []).map(({ contents, pathname }) => ( - (inlineScriptsAllowed + ...(additionalStaticJs || []).map(({ contents, pathname }) => ( + inlineScriptsAllowed ? template(' ')({ - contents: contents + contents, }) : template(' ')({ src: rootUrlPathPrefix + pathname - })) + }) )), + '', + head, + '', + '', + '', +].join('\n'); - [ - '', - head, - '', - '', - '', - ], -).join('\n'); - -export const closeTemplate = () => - [].concat([ '','' ]).join('\n'); +export function closeTemplate() { + return "\n"; +} From 7f12cfde95e60fe14b6b824b78d6bcb1cc148f50 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 22 Nov 2017 17:08:18 -0500 Subject: [PATCH 10/14] Simplify boilerplate-generator-tests. --- .../boilerplate-generator-tests/test-lib.js | 8 +- .../web.browser-tests.js | 82 ++++++++----------- .../web.cordova-tests.js | 75 ++++++++--------- 3 files changed, 71 insertions(+), 94 deletions(-) diff --git a/packages/boilerplate-generator-tests/test-lib.js b/packages/boilerplate-generator-tests/test-lib.js index 39917659f85..7871db49844 100644 --- a/packages/boilerplate-generator-tests/test-lib.js +++ b/packages/boilerplate-generator-tests/test-lib.js @@ -1,4 +1,4 @@ -import toString from "stream-to-string"; +import streamToString from "stream-to-string"; export async function generateHTMLForArch(arch) { // Use a dummy manifest. None of these paths will be read from the filesystem, but css / js should be handled differently @@ -52,7 +52,5 @@ export async function generateHTMLForArch(arch) { }, }); - - return await toString(boilerplate.toHTML()); -}; - + return streamToString(boilerplate.toHTML()); +} diff --git a/packages/boilerplate-generator-tests/web.browser-tests.js b/packages/boilerplate-generator-tests/web.browser-tests.js index dbf383cd192..7500d160925 100644 --- a/packages/boilerplate-generator-tests/web.browser-tests.js +++ b/packages/boilerplate-generator-tests/web.browser-tests.js @@ -1,70 +1,60 @@ import { parse, serialize } from 'parse5'; - import { generateHTMLForArch } from './test-lib'; +import { _ } from 'meteor/underscore'; -const start = async () => { - const html = await generateHTMLForArch('web.browser'); +Tinytest.addAsync( + "boilerplate-generator-tests - web.browser - basic output", + async function (test) { + const html = await generateHTMLForArch("web.browser"); - Tinytest.add("boilerplate-generator-tests - web.browser - well-formed html", function (test) { + // well-formed html const formatted = serialize(parse(html)); test.isTrue(formatted.replace(/\s/g, '') === html.replace(/\s/g, '')); - }); - Tinytest.add("boilerplate-generator-tests - web.browser - include htmlAttributes", function (test) { + // include htmlAttributes test.matches(html, /foo="foobar"/); - }); - Tinytest.add("boilerplate-generator-tests - web.browser - escape htmlAttributes", function (test) { + // escape htmlAttributes test.matches(html, /gems="&""/); - }); - Tinytest.add("boilerplate-generator-tests - web.browser - include js", function (test) { + // include js test.matches(html, /]*src="[^<>]*templating[^<>]*">/); - }); - Tinytest.add("boilerplate-generator-tests - web.browser - escape js", function (test) { + // escape js test.matches(html, /]*src="[^<>]*templating[^<>]*&v="1"[^<>]*">/); - }); - Tinytest.add("boilerplate-generator-tests - web.browser - include css", function (test) { + // include css test.matches(html, /]*href="[^<>]*bootstrap[^<>]*">/); - }); - Tinytest.add("boilerplate-generator-tests - web.browser - escape css", function (test) { + // escape css test.matches(html, /]*href="[^<>]*bootstrap[^<>]*&v="1"[^<>]*">/); - }); - Tinytest.add("boilerplate-generator-tests - web.browser - call rewriteHook", function (test) { + // call rewriteHook test.matches(html, /\+rewritten_url=true/); - }); - Tinytest.add("boilerplate-generator-tests - web.browser - include runtime config", function (test) { + // include runtime config test.matches(html, /]*>[^<>]*__meteor_runtime_config__ =.*decodeURIComponent\(config123\)/); - }); + } +); - // https://github.com/meteor/meteor/issues/9149 - Tinytest.addAsync( - "boilerplate-generator-tests - web.browser - properly render boilerplate " + +// https://github.com/meteor/meteor/issues/9149 +Tinytest.addAsync( + "boilerplate-generator-tests - web.browser - properly render boilerplate " + "elements when _.template settings are overridden", - function (test, onComplete) { - const run = async () => { - import { _ } from 'meteor/underscore'; - _.templateSettings = { - interpolate: /\{\{(.+?)\}\}/g - }; - const newHtml = await generateHTMLForArch('web.browser'); - test.matches(newHtml, /foo="foobar"/); - test.matches(newHtml, /]*href="[^<>]*bootstrap[^<>]*">/); - test.matches(newHtml, /]*src="[^<>]*templating[^<>]*">/); - test.matches(newHtml, /