Skip to content

Commit

Permalink
Merge pull request #9343 from meteor/jbaxleyiii/streaming-server-render
Browse files Browse the repository at this point in the history
Improved server rendering, now with stream support.
  • Loading branch information
benjamn committed Nov 23, 2017
2 parents a8cf9c0 + 8009351 commit f3c6460
Show file tree
Hide file tree
Showing 23 changed files with 715 additions and 453 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/boilerplate-generator-tests/package.js
Expand Up @@ -7,7 +7,8 @@ Package.describe({
});

Npm.depends({
parse5: '3.0.2'
parse5: '3.0.2',
'stream-to-string': '1.1.0'
});

Package.onTest(function (api) {
Expand Down
8 changes: 5 additions & 3 deletions packages/boilerplate-generator-tests/test-lib.js
@@ -1,4 +1,6 @@
export function generateHTMLForArch(arch) {
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
const manifest = [
{
Expand Down Expand Up @@ -50,5 +52,5 @@ export function generateHTMLForArch(arch) {
},
});

return boilerplate.toHTML();
};
return streamToString(boilerplate.toHTML());
}
69 changes: 33 additions & 36 deletions packages/boilerplate-generator-tests/web.browser-tests.js
@@ -1,63 +1,60 @@
import { parse, serialize } from 'parse5';

import { generateHTMLForArch } from './test-lib';
import { _ } from 'meteor/underscore';

const html = 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) {
const formatted = serialize(parse(html));
test.isTrue(formatted.replace(/\s/g, '') === html.replace(/\s/g, ''));
});
// 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) {
test.matches(html, /foo="foobar"/);
});
// include htmlAttributes
test.matches(html, /foo="foobar"/);

Tinytest.add("boilerplate-generator-tests - web.browser - escape htmlAttributes", function (test) {
test.matches(html, /gems="&""/);
});
// escape htmlAttributes
test.matches(html, /gems="&""/);

Tinytest.add("boilerplate-generator-tests - web.browser - include js", function (test) {
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*">/);
});
// include js
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*">/);

Tinytest.add("boilerplate-generator-tests - web.browser - escape js", function (test) {
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*&amp;v=&quot;1&quot;[^<>]*">/);
});
// escape js
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*&amp;v=&quot;1&quot;[^<>]*">/);

Tinytest.add("boilerplate-generator-tests - web.browser - include css", function (test) {
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*">/);
});
// include css
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*">/);

Tinytest.add("boilerplate-generator-tests - web.browser - escape css", function (test) {
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*&amp;v=&quot;1&quot;[^<>]*">/);
});
// escape css
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*&amp;v=&quot;1&quot;[^<>]*">/);

Tinytest.add("boilerplate-generator-tests - web.browser - call rewriteHook", function (test) {
test.matches(html, /\+rewritten_url=true/);
});
// call rewriteHook
test.matches(html, /\+rewritten_url=true/);

Tinytest.add("boilerplate-generator-tests - web.browser - include runtime config", function (test) {
test.matches(html, /<script[^<>]*>[^<>]*__meteor_runtime_config__ =.*decodeURIComponent\(config123\)/);
});
// include runtime config
test.matches(html, /<script[^<>]*>[^<>]*__meteor_runtime_config__ =.*decodeURIComponent\(config123\)/);
}
);

// https://github.com/meteor/meteor/issues/9149
Tinytest.add(
Tinytest.addAsync(
"boilerplate-generator-tests - web.browser - properly render boilerplate " +
"elements when _.template settings are overridden",
function (test) {
import { _ } from 'meteor/underscore';
"elements when _.template settings are overridden",
async function (test) {
const newHtml = await generateHTMLForArch("web.browser");

_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
const newHtml = generateHTMLForArch('web.browser');

test.matches(newHtml, /foo="foobar"/);
test.matches(newHtml, /<link[^<>]*href="[^<>]*bootstrap[^<>]*">/);
test.matches(newHtml, /<script[^<>]*src="[^<>]*templating[^<>]*">/);
test.matches(newHtml, /<script>var a/);
test.matches(
newHtml,
/<script[^<>]*>[^<>]*__meteor_runtime_config__ =.*decodeURIComponent\(config123\)/
/<script[^<>]*>[^<>]*__meteor_runtime_config__ =.*decodeURIComponent\(config123\)/
);
}
);
58 changes: 28 additions & 30 deletions packages/boilerplate-generator-tests/web.cordova-tests.js
@@ -1,54 +1,52 @@
import { parse, serialize } from 'parse5';

import { generateHTMLForArch } from './test-lib';
import { _ } from 'meteor/underscore';

const html = generateHTMLForArch('web.cordova');
Tinytest.addAsync(
"boilerplate-generator-tests - web.cordova - basic output",
async function (test) {
const html = await generateHTMLForArch("web.cordova");

Tinytest.add("boilerplate-generator-tests - web.cordova - well-formed html", function (test) {
const formatted = serialize(parse(html));
test.isTrue(formatted.replace(/\s/g, '') === html.replace(/\s/g, ''));
});
// well-formed html
const formatted = serialize(parse(html));
test.isTrue(formatted.replace(/\s/g, '') === html.replace(/\s/g, ''));

Tinytest.add("boilerplate-generator-tests - web.cordova - include js", function (test) {
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*">/);
});
// include js
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*">/);

Tinytest.add("boilerplate-generator-tests - web.cordova - escape js", function (test) {
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*&amp;v=&quot;1&quot;[^<>]*">/);
});
// escape js
test.matches(html, /<script[^<>]*src="[^<>]*templating[^<>]*&amp;v=&quot;1&quot;[^<>]*">/);

Tinytest.add("boilerplate-generator-tests - web.cordova - include css", function (test) {
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*">/);
});
// include css
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*">/);

Tinytest.add("boilerplate-generator-tests - web.cordova - escape css", function (test) {
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*&amp;v=&quot;1&quot;[^<>]*">/);
});
// escape css
test.matches(html, /<link[^<>]*href="[^<>]*bootstrap[^<>]*&amp;v=&quot;1&quot;[^<>]*">/);

Tinytest.add("boilerplate-generator-tests - web.cordova - do not call rewriteHook", function (test) {
test.notMatches(html, /\+rewritten_url=true/);
});
// do not call rewriteHook
test.notMatches(html, /\+rewritten_url=true/);

Tinytest.add("boilerplate-generator-tests - web.cordova - include runtime config", function (test) {
test.matches(html, /<script[^<>]*>[^<>]*\n[^<>]*__meteor_runtime_config__ =[^<>]*decodeURIComponent\(config123\)\)/);
});
// include runtime config
test.matches(html, /<script[^<>]*>[^<>]*\n[^<>]*__meteor_runtime_config__ =[^<>]*decodeURIComponent\(config123\)\)/);
}
);

// https://github.com/meteor/meteor/issues/9149
Tinytest.add(
Tinytest.addAsync(
"boilerplate-generator-tests - web.cordova - properly render boilerplate " +
"elements when _.template settings are overridden",
function (test) {
import { _ } from 'meteor/underscore';
"elements when _.template settings are overridden",
async function (test) {
const newHtml = await generateHTMLForArch('web.cordova');

_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
const newHtml = generateHTMLForArch('web.cordova');
test.matches(newHtml, /<link[^<>]*href="[^<>]*bootstrap[^<>]*">/);
test.matches(newHtml, /<script[^<>]*src="[^<>]*templating[^<>]*">/);
test.matches(newHtml, /<script>var a/);
test.matches(
newHtml,
/<script[^<>]*>[^<>]*__meteor_runtime_config__ =.*decodeURIComponent\(config123\)/
/<script[^<>]*>[^<>]*__meteor_runtime_config__ =.*decodeURIComponent\(config123\)/
);
}
);
1 change: 1 addition & 0 deletions packages/boilerplate-generator/.npm/package/.gitignore
@@ -0,0 +1 @@
node_modules
7 changes: 7 additions & 0 deletions 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.
30 changes: 30 additions & 0 deletions packages/boilerplate-generator/.npm/package/npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 36 additions & 4 deletions packages/boilerplate-generator/generator.js
@@ -1,4 +1,5 @@
import { readFile } from 'fs';
import { create as createStream } from "combined-stream2";

import WebBrowserTemplate from './template-web.browser';
import WebCordovaTemplate from './template-web.cordova';
Expand All @@ -8,9 +9,20 @@ const readUtf8FileSync = filename => Meteor.wrapAsync(readFile)(filename, 'utf8'

const identity = value => value;

function appendToStream(chunk, stream) {
if (typeof chunk === "string") {
stream.append(Buffer.from(chunk, "utf8"));
} else if (Buffer.isBuffer(chunk) ||
typeof chunk.read === "function") {
stream.append(chunk);
}
}

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(
Expand All @@ -23,13 +35,33 @@ export class Boilerplate {
// 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 a stream
toHTML(extraData) {
if (!this.baseData || !this.template) {
if (!this.baseData || !this.headTemplate || !this.closeTemplate) {
throw new Error('Boilerplate did not instantiate correctly.');
}

return "<!DOCTYPE html>\n" +
this.template({ ...this.baseData, ...extraData });
const data = {...this.baseData, ...extraData};
const start = "<!DOCTYPE html>\n" + this.headTemplate(data);

const { body, dynamicBody } = data;

const end = this.closeTemplate(data);
const response = createStream();

appendToStream(start, response);

if (body) {
appendToStream(body, response);
}

if (dynamicBody) {
appendToStream(dynamicBody, response);
}

appendToStream(end, response);

return response;
}

// XXX Exported to allow client-side only changes to rebuild the boilerplate
Expand Down
4 changes: 4 additions & 0 deletions packages/boilerplate-generator/package.js
Expand Up @@ -3,6 +3,10 @@ Package.describe({
version: '1.3.1'
});

Npm.depends({
"combined-stream2": "1.1.2"
});

Package.onUse(api => {
api.use('ecmascript');
api.use('underscore', 'server');
Expand Down

0 comments on commit f3c6460

Please sign in to comment.