2017-07-29 15:30:22 +09:00
|
|
|
import { readFile } from 'fs';
|
2018-01-27 16:12:06 +09:00
|
|
|
import { create as createStream } from "combined-stream2";
|
2017-07-29 15:30:22 +09:00
|
|
|
|
|
|
|
import WebBrowserTemplate from './template-web.browser';
|
|
|
|
import WebCordovaTemplate from './template-web.cordova';
|
|
|
|
|
|
|
|
// Copied from webapp_server
|
|
|
|
const readUtf8FileSync = filename => Meteor.wrapAsync(readFile)(filename, 'utf8');
|
|
|
|
|
2018-01-27 16:12:06 +09:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let shouldWarnAboutToHTMLDeprecation = ! Meteor.isProduction;
|
|
|
|
|
2017-07-29 15:30:22 +09:00
|
|
|
export class Boilerplate {
|
|
|
|
constructor(arch, manifest, options = {}) {
|
2018-06-10 10:37:33 +09:00
|
|
|
const { headTemplate, closeTemplate } = getTemplate(arch);
|
2018-01-27 16:12:06 +09:00
|
|
|
this.headTemplate = headTemplate;
|
|
|
|
this.closeTemplate = closeTemplate;
|
2017-07-29 15:30:22 +09:00
|
|
|
this.baseData = null;
|
|
|
|
|
|
|
|
this._generateBoilerplateFromManifest(
|
|
|
|
manifest,
|
|
|
|
options
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-01-27 16:12:06 +09:00
|
|
|
toHTML(extraData) {
|
|
|
|
if (shouldWarnAboutToHTMLDeprecation) {
|
|
|
|
shouldWarnAboutToHTMLDeprecation = false;
|
|
|
|
console.error(
|
|
|
|
"The Boilerplate#toHTML method has been deprecated. " +
|
|
|
|
"Please use Boilerplate#toHTMLStream instead."
|
|
|
|
);
|
|
|
|
console.trace();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calling .await() requires a Fiber.
|
2018-06-10 10:37:33 +09:00
|
|
|
return this.toHTMLAsync(extraData).await();
|
2018-01-27 16:12:06 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
// Returns a Promise that resolves to a string of HTML.
|
|
|
|
toHTMLAsync(extraData) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const stream = this.toHTMLStream(extraData);
|
|
|
|
const chunks = [];
|
|
|
|
stream.on("data", chunk => chunks.push(chunk));
|
|
|
|
stream.on("end", () => {
|
|
|
|
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
|
|
});
|
|
|
|
stream.on("error", reject);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-07-29 15:30:22 +09:00
|
|
|
// 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).
|
2018-01-27 16:12:06 +09:00
|
|
|
// this returns a stream
|
|
|
|
toHTMLStream(extraData) {
|
|
|
|
if (!this.baseData || !this.headTemplate || !this.closeTemplate) {
|
2017-07-29 15:30:22 +09:00
|
|
|
throw new Error('Boilerplate did not instantiate correctly.');
|
|
|
|
}
|
|
|
|
|
2018-01-27 16:12:06 +09:00
|
|
|
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;
|
2017-07-29 15:30:22 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
_generateBoilerplateFromManifest(manifest, {
|
2018-01-27 16:12:06 +09:00
|
|
|
urlMapper = identity,
|
|
|
|
pathMapper = identity,
|
2017-07-29 15:30:22 +09:00
|
|
|
baseDataExtension,
|
|
|
|
inline,
|
|
|
|
} = {}) {
|
|
|
|
|
|
|
|
const boilerplateBaseData = {
|
|
|
|
css: [],
|
|
|
|
js: [],
|
|
|
|
head: '',
|
|
|
|
body: '',
|
|
|
|
meteorManifest: JSON.stringify(manifest),
|
|
|
|
...baseDataExtension,
|
|
|
|
};
|
|
|
|
|
2018-01-27 16:12:06 +09:00
|
|
|
manifest.forEach(item => {
|
2017-07-29 15:30:22 +09:00
|
|
|
const urlPath = urlMapper(item.url);
|
|
|
|
const itemObj = { url: urlPath };
|
|
|
|
|
|
|
|
if (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));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.baseData = boilerplateBaseData;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Returns a template function that, when called, produces the boilerplate
|
|
|
|
// html as a string.
|
2018-06-10 10:37:33 +09:00
|
|
|
function getTemplate(arch) {
|
|
|
|
const prefix = arch.split(".", 2).join(".");
|
|
|
|
|
|
|
|
if (prefix === "web.browser") {
|
2017-07-29 15:30:22 +09:00
|
|
|
return WebBrowserTemplate;
|
2018-06-10 10:37:33 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
if (prefix === "web.cordova") {
|
2017-07-29 15:30:22 +09:00
|
|
|
return WebCordovaTemplate;
|
|
|
|
}
|
2018-06-10 10:37:33 +09:00
|
|
|
|
|
|
|
throw new Error("Unsupported arch: " + arch);
|
|
|
|
}
|