Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Boilerplate refactor #8820

Merged
merged 26 commits into from Jul 10, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cd50ba4
boilerplate refactor wip
stevenhao Jun 19, 2017
e4fdd29
rename files
stevenhao Jun 19, 2017
ad2601e
make switching between old/new easier
stevenhao Jun 19, 2017
4365d86
refactor and modernize boilerplate-generator
stevenhao Jun 21, 2017
cedbf0f
add cordova template code
stevenhao Jun 22, 2017
f78f69a
delete old boilerplate-generator
stevenhao Jun 22, 2017
4bfa9d1
small style fixes
stevenhao Jun 22, 2017
5a66a8d
address comments
stevenhao Jun 22, 2017
2a9d5f1
address review comments again
stevenhao Jun 26, 2017
827c854
remove boilerplate generated-by comment
stevenhao Jun 27, 2017
c4e5fcc
delete spacebars templates
stevenhao Jun 27, 2017
bb85861
add boilerplate-generator-tests
stevenhao Jun 28, 2017
3371297
bump boilerplate-generator version
stevenhao Jun 30, 2017
54fe867
dummy commit
stevenhao Jul 5, 2017
fc24b59
Revert "dummy commit"
stevenhao Jul 5, 2017
59f85ed
update tests
stevenhao Jul 5, 2017
b993d2f
refactor parameter destructuring
stevenhao Jul 5, 2017
40fb493
fix style
stevenhao Jul 5, 2017
cd84309
modernize boilerplate generator a bit
stevenhao Jul 5, 2017
efad586
Merge branch 'devel' into boilerplate-refactor-2
stevenhao Jul 5, 2017
aa77651
refactor boilerplate-generator
stevenhao Jul 7, 2017
b7e5596
fix web browser template
stevenhao Jul 7, 2017
a3d57fb
refactor boilerplate-generator-tests
stevenhao Jul 7, 2017
9d78ea6
rename files using hyphens
stevenhao Jul 7, 2017
e89a013
Remove spaces after object-shorthand method names.
abernix Jul 7, 2017
61b9f45
Add some space for legibility between conditionals.
abernix Jul 7, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
106 changes: 106 additions & 0 deletions packages/boilerplate-generator/boilerplate-generator-old.js
@@ -0,0 +1,106 @@
var fs = Npm.require('fs');
var path = Npm.require('path');

// Copied from webapp_server
var readUtf8FileSync = function (filename) {
return Meteor.wrapAsync(fs.readFile)(filename, 'utf8');
};

Boilerplate = function (arch, manifest, options) {
var self = this;
options = options || {};
self.template = _getTemplate(arch);
self.baseData = null;
self.func = null;

self._generateBoilerplateFromManifestAndSource(
manifest,
self.template,
options
);
};

// 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).
Boilerplate.prototype.toHTML = function (extraData) {
var self = this;

if (! self.baseData || ! self.func)
throw new Error('Boilerplate did not instantiate correctly.');

return "<!DOCTYPE html>\n" +
Blaze.toHTML(Blaze.With(_.extend(self.baseData, extraData),
self.func));
};

// XXX Exported to allow client-side only changes to rebuild the boilerplate
// without requiring a full server restart.
// Produces an HTML string with given manifest and boilerplateSource.
// Optionally takes urlMapper in case urls from manifest need to be prefixed
// or rewritten.
// Optionally takes pathMapper for resolving relative file system paths.
// Optionally allows to override fields of the data context.
Boilerplate.prototype._generateBoilerplateFromManifestAndSource =
function (manifest, boilerplateSource, options) {
var self = this;
// map to the identity by default
var urlMapper = options.urlMapper || _.identity;
var pathMapper = options.pathMapper || _.identity;

var boilerplateBaseData = {
css: [],
js: [],
head: '',
body: '',
meteorManifest: JSON.stringify(manifest)
};

// allow the caller to extend the default base data
_.extend(boilerplateBaseData, options.baseDataExtension);

_.each(manifest, function (item) {
var urlPath = urlMapper(item.url);
var itemObj = { url: urlPath };

if (options.inline) {
itemObj.scriptContent = readUtf8FileSync(
pathMapper(item.path));
itemObj.inline = true;
}

if (item.type === 'css' && item.where === 'client') {
boilerplateBaseData.css.push(itemObj);
}
if (item.type === 'js' && item.where === 'client' &&
// Dynamic JS modules should not be loaded eagerly in the
// initial HTML of the app.
! item.path.startsWith('dynamic/')) {
boilerplateBaseData.js.push(itemObj);
}
if (item.type === 'head') {
boilerplateBaseData.head =
readUtf8FileSync(pathMapper(item.path));
}
if (item.type === 'body') {
boilerplateBaseData.body =
readUtf8FileSync(pathMapper(item.path));
}
});
var boilerplateRenderCode = SpacebarsCompiler.compile(
boilerplateSource, { isBody: true });

// Note that we are actually depending on eval's local environment capture
// so that UI and HTML are visible to the eval'd code.
// XXX the template we are evaluating relies on the fact that UI is globally
// available.
global.UI = UI;
self.func = eval(boilerplateRenderCode);
self.baseData = boilerplateBaseData;
};

var _getTemplate = _.memoize(function (arch) {
var filename = 'boilerplate_' + arch + '.html';
return Assets.getText(filename);
});
32 changes: 12 additions & 20 deletions packages/boilerplate-generator/boilerplate-generator.js
Expand Up @@ -11,11 +11,9 @@ Boilerplate = function (arch, manifest, options) {
options = options || {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really more of a question for @benjamn or @abernix but should new code be written with ES6 these days? import instead of Npm.require (it's ok even in packages to use import for built in packages like this one, right?), no var, class, etc? Using mainModule and imports instead of file lists and package-local variables?

And specifically should this refactored class be rewritten to new style or left alone?

Copy link
Contributor

@benjamn benjamn Jun 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's modernize this code! It runs on the server anyway. Specifically, @stevenhao, we should put api.use("ecmascript", "server") in package.js, define an api.mainModule, and use class syntax (etc.) in this file.

self.template = _getTemplate(arch);
self.baseData = null;
self.func = null;

self._generateBoilerplateFromManifestAndSource(
self._generateBoilerplateFromManifest(
manifest,
self.template,
options
);
};
Expand All @@ -27,12 +25,10 @@ Boilerplate = function (arch, manifest, options) {
Boilerplate.prototype.toHTML = function (extraData) {
var self = this;

if (! self.baseData || ! self.func)
if (! self.baseData || ! self.template)
throw new Error('Boilerplate did not instantiate correctly.');

return "<!DOCTYPE html>\n" +
Blaze.toHTML(Blaze.With(_.extend(self.baseData, extraData),
self.func));
return "<!DOCTYPE html>\n" + self.template(_.extend(self.baseData, extraData));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_.extend modifies self.baseData in place — is that intentional? I think maybe _.extend({}, self.baseData, extraData) may be better. I see that's in the existing code too but maybe that's a bug.

Copy link
Contributor

@benjamn benjamn Jun 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's an idea you can ignore for the time being:

I would love to enable more interesting extensions of self.baseData here, specifically to support server-side rendering. For example, if this code called some custom callback with self.baseData, then the callback could parse the self.baseData.body HTML and attach server-rendered fragments to DOM nodes with particular ids. Then the client could use document.getElementById to find the same nodes and rehydrate the server-generated HTML.

For now, let's focus on feature parity, but I wanted to plant that seed. 🌱

Copy link
Contributor Author

@stevenhao stevenhao Jun 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, i'll change it to _.extend({}, self.baseData, extraData), or if it's safe to use ES6, Object.assign({}, self.baseData, extraData)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ES6 is definitely fine in server code and I think in client code too.

};

// XXX Exported to allow client-side only changes to rebuild the boilerplate
Expand All @@ -42,8 +38,8 @@ Boilerplate.prototype.toHTML = function (extraData) {
// or rewritten.
// Optionally takes pathMapper for resolving relative file system paths.
// Optionally allows to override fields of the data context.
Boilerplate.prototype._generateBoilerplateFromManifestAndSource =
function (manifest, boilerplateSource, options) {
Boilerplate.prototype._generateBoilerplateFromManifest =
function (manifest, options) {
var self = this;
// map to the identity by default
var urlMapper = options.urlMapper || _.identity;
Expand Down Expand Up @@ -88,19 +84,15 @@ Boilerplate.prototype._generateBoilerplateFromManifestAndSource =
readUtf8FileSync(pathMapper(item.path));
}
});
var boilerplateRenderCode = SpacebarsCompiler.compile(
boilerplateSource, { isBody: true });

// Note that we are actually depending on eval's local environment capture
// so that UI and HTML are visible to the eval'd code.
// XXX the template we are evaluating relies on the fact that UI is globally
// available.
global.UI = UI;
self.func = eval(boilerplateRenderCode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to see this eval go away!

self.baseData = boilerplateBaseData;
};

var _getTemplate = _.memoize(function (arch) {
var filename = 'boilerplate_' + arch + '.html';
return Assets.getText(filename);
if (arch === 'web.browser') {
return Boilerplate_Web_Browser_Template;
} else if (arch === 'web.cordova') {
throw new Error('Cordova template not implemented');
} else {
throw new Error('Unsupported arch: ' + arch);
}
});
90 changes: 90 additions & 0 deletions packages/boilerplate-generator/boilerplate_web_template.js
@@ -0,0 +1,90 @@
// Template function for rendering the boilerplate html
// Replicates the template defined in boilerplate_web.browser.html
// XXX Does not necessarily preserve formatting (e.g. additionalStaticJs newlines)
// Arguments: root : { htmlAttributes, css : [{ url }], bundledJsCssUrlRewriteHook : Function, head, dynamicHead, body, dynamicBody, inlineScriptsAllowed, additionalStaticJs, meteorRuntimeConfig }

Boilerplate_Web_Browser_Template = function(root) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't our normal variable naming style. I think BoilerplateWebBrowserTemplate, or even just WebBrowserTemplate since we're not exporting it out of the package.

return [].concat(

// XXX is htmlAttributes ever anything but {}?
// may just be a generic Blaze/Spacebars thing.
function props1(htmlAttributes) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need to name these functions that are only called immediately. But I also am unsure why this is necessary — can't you just write root.htmlAttributes directly? Is this based on starting with some generated code? I'd simplify this to look more like functions you'd write yourself.

return [
['<html'].concat(Object.keys(htmlAttributes).map(function(key) {

// XXX probably need to wrap strings in "".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely want to have some escaping here! Not just wrapping, also doing proper encoding

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I've been delaying implementing this because I'm not sure what the best library for this kind of stuff.

From what I can tell after reading https://github.com/meteor/blaze/blob/master/packages/htmljs/visitors.js#L198, https://html.spec.whatwg.org/multipage/syntax.html#attributes-2,

an attribute pair should stringify to _.template(' <%= key %>="<%- value %>"')({key, value}).

Potential issues:

  • The attribute namespace isn't explicitly handled and must be directly attached to the key of htmlAttributes, e.g. htmlAttributes may need to have entries like {"xmlns:h": "http://www.w3.org/2000/xmlns/"}.
  • _.template("<%- ... %>") does HTML escaping, does not distinguish between attribute mode. this project has an isAttributeValue option that may be useful. It seems to me that the attribute mode distinction isn't hugely important, and that most browsers handle unnecessarily escaped characters gracefully.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we chatted about this in person and decided that we need to HTML-encode attribute values but not names, and don't need to URL-encode anything.

return key + '=' + htmlAttributes[key];
})).join(' ') + '>'
];
}(root.htmlAttributes),

[
'<head>'
],

function each1(css) {
var bundledJsCssUrlRewriteHook = root.bundledJsCssUrlRewriteHook;
return _.map(css, function(obj) {
var url = obj.url;
return ' <link rel="stylesheet" type="text/css" class="__meteor-css__" href="' + bundledJsCssUrlRewriteHook(url) + '">';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url needs escaping (anything in the original template that's {{}} rather than {{{}}} should be escaped). Note that escaping is either URL-escaping or HTML-escaping depending on where it is, I think?

});
}(root.css),

[
root.head,
root.dynamicHead,
'</head>',
'<body>',
root.body,
root.dynamicBody,
''
],

function if1(inlineScriptsAllowed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one might be easier just as a ?: thing?

var meteorRuntimeConfig = root.meteorRuntimeConfig;
var rootUrlPathPrefix = root.rootUrlPathPrefix;

if (inlineScriptsAllowed) {
return [
'<script type="text/javascript">__meteor_runtime_config__ = JSON.parse(decodeURIComponent(' + meteorRuntimeConfig + '));</script>' // XXX add 2 spaces to fix indentation
];
} else {
return [
'<script type="text/javascript" src="' + rootUrlPathPrefix + '/meteor_runtime_config.js"></script>' // XXX add 2 spaces to fix indentation
];
}
}(root.inlineScriptsAllowed),

[''],

function each2(js) {
var bundledJsCssUrlRewriteHook = root.bundledJsCssUrlRewriteHook;
return _.map(js, function(obj) {
var url = obj.url;
return ' <script type="text/javascript" src="' + bundledJsCssUrlRewriteHook(url) + '"></script>';
});
}(root.js),

function each3(additionalStaticJs) {
var inlineScriptsAllowed = root.inlineScriptsAllowed;
var rootUrlPathPrefix = root.rootUrlPathPrefix;
return _.map(additionalStaticJs, function(obj) {
var contents = obj.contents;
var pathname = obj.pathname;
if (inlineScriptsAllowed) {
return ' <script>' + contents + '</script>';
} else {
return " <script type='text/javascript' src='" + rootUrlPathPrefix + pathname + "'></script>";
}
});
}(root.additionalStaticJs),

[
'', '',
'</body>',
'</html>'
],

['', '<!-- Generated by boilerplate generator -->'] // to help distinguishes old generator from new [just for testing]
).join('\n'); // undefined is treated as empty string (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join)
}
18 changes: 17 additions & 1 deletion packages/boilerplate-generator/package.js
Expand Up @@ -3,6 +3,9 @@ Package.describe({
version: '1.1.0'
});

var USE_OLD_BOILERPLATE_GENERATOR = true;
USE_OLD_BOILERPLATE_GENERATOR = false; // uncomment this line to use the new boilerplate generator
if(USE_OLD_BOILERPLATE_GENERATOR){
Package.onUse(function (api) {
api.use([
'underscore',
Expand All @@ -11,7 +14,7 @@ Package.onUse(function (api) {
'htmljs',
'ui',
], 'server');
api.addFiles(['boilerplate-generator.js'], 'server');
api.addFiles(['boilerplate-generator-old.js'], 'server');
api.export(['Boilerplate'], 'server');
// These are spacebars templates, but we process them manually with the
// spacebars compiler rather than letting the 'templating' package (which
Expand All @@ -22,3 +25,16 @@ Package.onUse(function (api) {
'boilerplate_web.cordova.html'
], 'server');
});
} else {

Package.onUse(function (api) {
api.use([
'underscore',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we're relying on underscore anyway, maybe just use _.template with a template stored in a file and read by Assets.getText?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had no idea this existed! I'll try it out.

], 'server');
api.addFiles([
'boilerplate_web_template.js',
'boilerplate-generator.js'
], 'server');
api.export(['Boilerplate'], 'server');
});
}