mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Client CSS and template injection.
This commit is contained in:
@@ -25,11 +25,11 @@
|
|||||||
// The client version of the client code currently running in the
|
// The client version of the client code currently running in the
|
||||||
// browser.
|
// browser.
|
||||||
var autoupdateVersion = __meteor_runtime_config__.autoupdateVersion || "unknown";
|
var autoupdateVersion = __meteor_runtime_config__.autoupdateVersion || "unknown";
|
||||||
|
var autoupdateVersionRefreshable =
|
||||||
|
__meteor_runtime_config__.autoupdateVersionRefreshable || "unknown";
|
||||||
|
|
||||||
// The collection of acceptable client versions.
|
// The collection of acceptable client versions.
|
||||||
var ClientVersions = new Meteor.Collection("meteor_autoupdate_clientVersions");
|
ClientVersions = new Meteor.Collection("meteor_autoupdate_clientVersions");
|
||||||
|
|
||||||
|
|
||||||
Autoupdate = {};
|
Autoupdate = {};
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ Autoupdate.newClientAvailable = function () {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var knownToSupportCssOnLoad = false;
|
||||||
|
|
||||||
var retry = new Retry({
|
var retry = new Retry({
|
||||||
// Unlike the stream reconnect use of Retry, which we want to be instant
|
// Unlike the stream reconnect use of Retry, which we want to be instant
|
||||||
@@ -76,15 +76,72 @@ Autoupdate._retrySubscription = function () {
|
|||||||
},
|
},
|
||||||
onReady: function () {
|
onReady: function () {
|
||||||
if (Package.reload) {
|
if (Package.reload) {
|
||||||
Deps.autorun(function (computation) {
|
var handle = ClientVersions.find().observeChanges({
|
||||||
if (ClientVersions.findOne({current: true}) &&
|
added: function (id, fields) {
|
||||||
(! ClientVersions.findOne({_id: autoupdateVersion}))) {
|
var self = this;
|
||||||
computation.stop();
|
if (fields.refreshable && id !== autoupdateVersionRefreshable) {
|
||||||
Package.reload.Reload._reload();
|
autoupdateVersionRefreshable = id;
|
||||||
|
|
||||||
|
// Switch out old css links for the new css links. Inspired by:
|
||||||
|
// https://github.com/guard/guard-livereload/blob/master/js/livereload.js#L710
|
||||||
|
var newCss = fields.assets.allCss;
|
||||||
|
var oldLinks = [];
|
||||||
|
_.each(document.getElementsByTagName('link'), function (link) {
|
||||||
|
if (link.className === '__meteor-css__') {
|
||||||
|
oldLinks.push(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var waitUntilCssLoads = function (link, callback) {
|
||||||
|
var executeCallback = _.once(callback);
|
||||||
|
link.onload = function () {
|
||||||
|
knownToSupportCssOnLoad = true;
|
||||||
|
executeCallback();
|
||||||
|
};
|
||||||
|
if (! knownToSupportCssOnLoad) {
|
||||||
|
var id = Meteor.setInterval(function () {
|
||||||
|
if (link.sheet) {
|
||||||
|
executeCallback();
|
||||||
|
Meteor.clearInterval(id);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var attachStylesheetLink = function (newLink) {
|
||||||
|
var removeOldLinks = _.after(newCss.length, function () {
|
||||||
|
_.each(oldLinks, function (oldLink) {
|
||||||
|
oldLink.parentNode.removeChild(oldLink);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementsByTagName("head").
|
||||||
|
item(0).
|
||||||
|
insertBefore(newLink);
|
||||||
|
|
||||||
|
waitUntilCssLoads(newLink, function () {
|
||||||
|
Meteor.setTimeout(removeOldLinks, 200);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
_.each(newCss, function (css) {
|
||||||
|
var newLink = document.createElement("link");
|
||||||
|
newLink.setAttribute("rel", "stylesheet");
|
||||||
|
newLink.setAttribute("type", "text/css");
|
||||||
|
newLink.setAttribute("class", "__meteor-css__");
|
||||||
|
newLink.setAttribute("href", css.url);
|
||||||
|
attachStylesheetLink(newLink);
|
||||||
|
});
|
||||||
|
} else if (! fields.refreshable && id !== autoupdateVersion) {
|
||||||
|
if (handle) {
|
||||||
|
handle.stop();
|
||||||
|
Package.reload.Reload._reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
Autoupdate._retrySubscription();
|
Autoupdate._retrySubscription();
|
||||||
|
|||||||
@@ -37,45 +37,90 @@
|
|||||||
|
|
||||||
Autoupdate = {};
|
Autoupdate = {};
|
||||||
|
|
||||||
|
// The collection of acceptable client versions.
|
||||||
|
ClientVersions = new Meteor.Collection("meteor_autoupdate_clientVersions",
|
||||||
|
{ connection: null });
|
||||||
|
|
||||||
// The client hash includes __meteor_runtime_config__, so wait until
|
// The client hash includes __meteor_runtime_config__, so wait until
|
||||||
// all packages have loaded and have had a chance to populate the
|
// all packages have loaded and have had a chance to populate the
|
||||||
// runtime config before using the client hash as our default auto
|
// runtime config before using the client hash as our default auto
|
||||||
// update version id.
|
// update version id.
|
||||||
|
|
||||||
Autoupdate.autoupdateVersion = null;
|
Autoupdate.autoupdateVersion = null;
|
||||||
|
Autoupdate.autoupdateVersionRefreshable = null;
|
||||||
|
|
||||||
|
var syncQueue = new Meteor._SynchronousQueue();
|
||||||
|
var startupVersion = null;
|
||||||
|
|
||||||
|
// updateVersions can only be called after the server has fully loaded.
|
||||||
|
var updateVersions = function () {
|
||||||
|
syncQueue.runTask(function () {
|
||||||
|
var oldVersion = Autoupdate.autoupdateVersion;
|
||||||
|
var oldVersionRefreshable = Autoupdate.autoupdateVersionRefreshable;
|
||||||
|
|
||||||
|
// Step 1: load the current client program on the server and update the
|
||||||
|
// hash values in __meteor_runtime_config__.
|
||||||
|
WebAppInternals.reloadClientProgram();
|
||||||
|
|
||||||
|
if (startupVersion === null) {
|
||||||
|
Autoupdate.autoupdateVersion =
|
||||||
|
__meteor_runtime_config__.autoupdateVersion =
|
||||||
|
process.env.AUTOUPDATE_VERSION ||
|
||||||
|
process.env.SERVER_ID || // XXX COMPAT 0.6.6
|
||||||
|
WebApp.calculateClientHashNonRefreshable();
|
||||||
|
}
|
||||||
|
|
||||||
|
Autoupdate.autoupdateVersionRefreshable =
|
||||||
|
__meteor_runtime_config__.autoupdateVersionRefreshable =
|
||||||
|
process.env.AUTOUPDATE_VERSION ||
|
||||||
|
process.env.SERVER_ID || // XXX COMPAT 0.6.6
|
||||||
|
WebApp.calculateClientHashRefreshable();
|
||||||
|
|
||||||
|
// Step 2: form the new client boilerplate which contains the updated
|
||||||
|
// assets and __meteor_runtime_config__.
|
||||||
|
WebAppInternals.generateBoilerplate();
|
||||||
|
|
||||||
|
if (Autoupdate.autoupdateVersion !== oldVersion) {
|
||||||
|
if (oldVersion) {
|
||||||
|
ClientVersions.remove(oldVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientVersions.insert({
|
||||||
|
_id: Autoupdate.autoupdateVersion,
|
||||||
|
refreshable: false,
|
||||||
|
current: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Autoupdate.autoupdateVersionRefreshable !== oldVersionRefreshable) {
|
||||||
|
if (oldVersionRefreshable) {
|
||||||
|
ClientVersions.remove(oldVersionRefreshable);
|
||||||
|
}
|
||||||
|
ClientVersions.insert({
|
||||||
|
_id: Autoupdate.autoupdateVersionRefreshable,
|
||||||
|
refreshable: true,
|
||||||
|
assets: WebAppInternals.refreshableAssets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
Meteor.startup(function () {
|
Meteor.startup(function () {
|
||||||
// Allow people to override Autoupdate.autoupdateVersion before
|
// Allow people to override Autoupdate.autoupdateVersion before startup.
|
||||||
// startup. Tests do this.
|
// Tests do this.
|
||||||
if (Autoupdate.autoupdateVersion === null)
|
startupVersion = Autoupdate.autoupdateVersion;
|
||||||
Autoupdate.autoupdateVersion =
|
WebApp.onListening(updateVersions);
|
||||||
process.env.AUTOUPDATE_VERSION ||
|
|
||||||
process.env.SERVER_ID || // XXX COMPAT 0.6.6
|
|
||||||
WebApp.clientHash;
|
|
||||||
|
|
||||||
// Make autoupdateVersion available on the client.
|
|
||||||
__meteor_runtime_config__.autoupdateVersion = Autoupdate.autoupdateVersion;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
Meteor.publish(
|
Meteor.publish(
|
||||||
"meteor_autoupdate_clientVersions",
|
"meteor_autoupdate_clientVersions",
|
||||||
function () {
|
function () {
|
||||||
var self = this;
|
return ClientVersions.find();
|
||||||
// Using `autoupdateVersion` here is safe because we can't get a
|
|
||||||
// subscription before webapp starts listening, and it doesn't do
|
|
||||||
// that until the startup hooks have run.
|
|
||||||
if (Autoupdate.autoupdateVersion) {
|
|
||||||
self.added(
|
|
||||||
"meteor_autoupdate_clientVersions",
|
|
||||||
Autoupdate.autoupdateVersion,
|
|
||||||
{current: true}
|
|
||||||
);
|
|
||||||
self.ready();
|
|
||||||
} else {
|
|
||||||
// huh? shouldn't happen. Just error the sub.
|
|
||||||
self.error(new Meteor.Error(500, "Autoupdate.autoupdateVersion not set"));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{is_auto: true}
|
{is_auto: true}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Listen for SIGUSR2, which signals that a client asset has changed.
|
||||||
|
process.on('SIGUSR2', Meteor.bindEnvironment(function () {
|
||||||
|
updateVersions();
|
||||||
|
}));
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<html {{htmlAttributes}}>
|
<html {{htmlAttributes}}>
|
||||||
<head>
|
<head>
|
||||||
{{#each css}} <link rel="stylesheet" href="{{../bundledJsCssPrefix}}{{url}}">{{/each}}
|
{{#each css}} <link rel="stylesheet" type="text/css" class="__meteor-css__" href="{{../bundledJsCssPrefix}}{{url}}">{{/each}}
|
||||||
|
|
||||||
{{#if inlineScriptsAllowed}}
|
{{#if inlineScriptsAllowed}}
|
||||||
<script type='text/javascript'>__meteor_runtime_config__ = {{meteorRuntimeConfig}};</script>
|
<script type='text/javascript'>__meteor_runtime_config__ = {{meteorRuntimeConfig}};</script>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ var connect = Npm.require('connect');
|
|||||||
var useragent = Npm.require('useragent');
|
var useragent = Npm.require('useragent');
|
||||||
var send = Npm.require('send');
|
var send = Npm.require('send');
|
||||||
|
|
||||||
|
var Future = Npm.require('fibers/future');
|
||||||
|
|
||||||
var SHORT_SOCKET_TIMEOUT = 5*1000;
|
var SHORT_SOCKET_TIMEOUT = 5*1000;
|
||||||
var LONG_SOCKET_TIMEOUT = 120*1000;
|
var LONG_SOCKET_TIMEOUT = 120*1000;
|
||||||
|
|
||||||
@@ -62,6 +64,10 @@ var sha1 = function (contents) {
|
|||||||
return hash.digest('hex');
|
return hash.digest('hex');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var readUtf8FileSync = function (filename) {
|
||||||
|
return Future.wrap(fs.readFile)(filename, 'utf8').wait();
|
||||||
|
};
|
||||||
|
|
||||||
// #BrowserIdentification
|
// #BrowserIdentification
|
||||||
//
|
//
|
||||||
// We have multiple places that want to identify the browser: the
|
// We have multiple places that want to identify the browser: the
|
||||||
@@ -179,11 +185,15 @@ var appUrl = function (url) {
|
|||||||
// (but the second is a performance enhancement, not a hard
|
// (but the second is a performance enhancement, not a hard
|
||||||
// requirement).
|
// requirement).
|
||||||
|
|
||||||
var calculateClientHash = function () {
|
var calculateClientHash = function (includeFilter) {
|
||||||
var hash = crypto.createHash('sha1');
|
var hash = crypto.createHash('sha1');
|
||||||
hash.update(JSON.stringify(__meteor_runtime_config__), 'utf8');
|
// Omit the old hashed client values in the new hash. These may be
|
||||||
|
// modified in the new boilerplate.
|
||||||
|
hash.update(JSON.stringify(_.omit(__meteor_runtime_config__,
|
||||||
|
['autoupdateVersion', 'autoupdateVersionRefreshable']), 'utf8'));
|
||||||
_.each(WebApp.clientProgram.manifest, function (resource) {
|
_.each(WebApp.clientProgram.manifest, function (resource) {
|
||||||
if (resource.where === 'client' || resource.where === 'internal') {
|
if ((! includeFilter || includeFilter(resource.type)) &&
|
||||||
|
(resource.where === 'client' || resource.where === 'internal')) {
|
||||||
hash.update(resource.path);
|
hash.update(resource.path);
|
||||||
hash.update(resource.hash);
|
hash.update(resource.hash);
|
||||||
}
|
}
|
||||||
@@ -211,6 +221,16 @@ var calculateClientHash = function () {
|
|||||||
|
|
||||||
Meteor.startup(function () {
|
Meteor.startup(function () {
|
||||||
WebApp.clientHash = calculateClientHash();
|
WebApp.clientHash = calculateClientHash();
|
||||||
|
WebApp.calculateClientHashRefreshable = function () {
|
||||||
|
return calculateClientHash(function (name) {
|
||||||
|
return name === "css";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
WebApp.calculateClientHashNonRefreshable = function () {
|
||||||
|
return calculateClientHash(function (name) {
|
||||||
|
return name !== "css";
|
||||||
|
});
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -237,15 +257,69 @@ WebApp._timeoutAdjustmentRequestCallback = function (req, res) {
|
|||||||
|
|
||||||
var runWebAppServer = function () {
|
var runWebAppServer = function () {
|
||||||
var shuttingDown = false;
|
var shuttingDown = false;
|
||||||
// read the control for the client we'll be serving up
|
var syncQueue = new Meteor._SynchronousQueue();
|
||||||
var clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
|
|
||||||
__meteor_bootstrap__.configJson.client);
|
|
||||||
var clientDir = path.dirname(clientJsonPath);
|
|
||||||
var clientJson = JSON.parse(fs.readFileSync(clientJsonPath, 'utf8'));
|
|
||||||
|
|
||||||
if (clientJson.format !== "browser-program-pre1")
|
var getItemPathname = function (itemUrl) {
|
||||||
throw new Error("Unsupported format for client assets: " +
|
return decodeURIComponent(url.parse(itemUrl).pathname);
|
||||||
JSON.stringify(clientJson.format));
|
};
|
||||||
|
|
||||||
|
var staticFiles;
|
||||||
|
|
||||||
|
var clientJsonPath;
|
||||||
|
var clientDir;
|
||||||
|
var clientJson;
|
||||||
|
|
||||||
|
WebAppInternals.reloadClientProgram = function () {
|
||||||
|
syncQueue.runTask(function() {
|
||||||
|
try {
|
||||||
|
// read the control for the client we'll be serving up
|
||||||
|
clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
|
||||||
|
__meteor_bootstrap__.configJson.client);
|
||||||
|
clientDir = path.dirname(clientJsonPath);
|
||||||
|
clientJson = JSON.parse(readUtf8FileSync(clientJsonPath));
|
||||||
|
if (clientJson.format !== "browser-program-pre1")
|
||||||
|
throw new Error("Unsupported format for client assets: " +
|
||||||
|
JSON.stringify(clientJson.format));
|
||||||
|
|
||||||
|
staticFiles = {};
|
||||||
|
_.each(clientJson.manifest, function (item) {
|
||||||
|
if (item.url && item.where === "client") {
|
||||||
|
staticFiles[getItemPathname(item.url)] = {
|
||||||
|
path: item.path,
|
||||||
|
cacheable: item.cacheable,
|
||||||
|
// Link from source to its map
|
||||||
|
sourceMapUrl: item.sourceMapUrl,
|
||||||
|
type: item.type
|
||||||
|
};
|
||||||
|
|
||||||
|
if (item.sourceMap) {
|
||||||
|
// Serve the source map too, under the specified URL. We assume all
|
||||||
|
// source maps are cacheable.
|
||||||
|
staticFiles[getItemPathname(item.sourceMapUrl)] = {
|
||||||
|
path: item.sourceMap,
|
||||||
|
cacheable: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
WebApp.clientProgram = {
|
||||||
|
manifest: clientJson.manifest
|
||||||
|
// XXX do we need a "root: clientDir" field here? it used to be here but
|
||||||
|
// was unused.
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exported for tests.
|
||||||
|
WebAppInternals.staticFiles = staticFiles;
|
||||||
|
} catch (e) {
|
||||||
|
Log.error("Error reloading the client program: " + e.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
WebAppInternals.reloadClientProgram();
|
||||||
|
|
||||||
|
if (! clientJsonPath || ! clientDir || ! clientJson)
|
||||||
|
throw new Error("Client config file not parsed.");
|
||||||
|
|
||||||
// webserver
|
// webserver
|
||||||
var app = connect();
|
var app = connect();
|
||||||
@@ -285,36 +359,6 @@ var runWebAppServer = function () {
|
|||||||
// generally pretty handy..
|
// generally pretty handy..
|
||||||
app.use(connect.query());
|
app.use(connect.query());
|
||||||
|
|
||||||
var getItemPathname = function (itemUrl) {
|
|
||||||
return decodeURIComponent(url.parse(itemUrl).pathname);
|
|
||||||
};
|
|
||||||
|
|
||||||
var staticFiles = {};
|
|
||||||
_.each(clientJson.manifest, function (item) {
|
|
||||||
if (item.url && item.where === "client") {
|
|
||||||
staticFiles[getItemPathname(item.url)] = {
|
|
||||||
path: item.path,
|
|
||||||
cacheable: item.cacheable,
|
|
||||||
// Link from source to its map
|
|
||||||
sourceMapUrl: item.sourceMapUrl,
|
|
||||||
type: item.type
|
|
||||||
};
|
|
||||||
|
|
||||||
if (item.sourceMap) {
|
|
||||||
// Serve the source map too, under the specified URL. We assume all
|
|
||||||
// source maps are cacheable.
|
|
||||||
staticFiles[getItemPathname(item.sourceMapUrl)] = {
|
|
||||||
path: item.sourceMap,
|
|
||||||
cacheable: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Exported for tests.
|
|
||||||
WebAppInternals.staticFiles = staticFiles;
|
|
||||||
|
|
||||||
|
|
||||||
// Serve static files from the manifest.
|
// Serve static files from the manifest.
|
||||||
// This is inspired by the 'static' middleware.
|
// This is inspired by the 'static' middleware.
|
||||||
app.use(function (req, res, next) {
|
app.use(function (req, res, next) {
|
||||||
@@ -480,10 +524,9 @@ var runWebAppServer = function () {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
var htmlAttributes = getHtmlAttributes(request);
|
|
||||||
|
|
||||||
// The only thing that changes from request to request (for now) are the
|
// The only thing that changes from request to request (for now) are the
|
||||||
// HTML attributes (used by, eg, appcache), so we can memoize based on that.
|
// HTML attributes (used by, eg, appcache), so we can memoize based on that.
|
||||||
|
var htmlAttributes = getHtmlAttributes(request);
|
||||||
var attributeKey = JSON.stringify(htmlAttributes);
|
var attributeKey = JSON.stringify(htmlAttributes);
|
||||||
if (!_.has(boilerplateByAttributes, attributeKey)) {
|
if (!_.has(boilerplateByAttributes, attributeKey)) {
|
||||||
try {
|
try {
|
||||||
@@ -568,12 +611,6 @@ var runWebAppServer = function () {
|
|||||||
connectHandlers: packageAndAppHandlers,
|
connectHandlers: packageAndAppHandlers,
|
||||||
rawConnectHandlers: rawConnectHandlers,
|
rawConnectHandlers: rawConnectHandlers,
|
||||||
httpServer: httpServer,
|
httpServer: httpServer,
|
||||||
// metadata about the client program that we serve
|
|
||||||
clientProgram: {
|
|
||||||
manifest: clientJson.manifest
|
|
||||||
// XXX do we need a "root: clientDir" field here? it used to be here but
|
|
||||||
// was unused.
|
|
||||||
},
|
|
||||||
// For testing.
|
// For testing.
|
||||||
suppressConnectErrors: function () {
|
suppressConnectErrors: function () {
|
||||||
suppressConnectErrors = true;
|
suppressConnectErrors = true;
|
||||||
@@ -602,48 +639,60 @@ var runWebAppServer = function () {
|
|||||||
// '--keepalive' is a use of the option.
|
// '--keepalive' is a use of the option.
|
||||||
var expectKeepalives = _.contains(argv, '--keepalive');
|
var expectKeepalives = _.contains(argv, '--keepalive');
|
||||||
|
|
||||||
boilerplateBaseData = {
|
|
||||||
css: [],
|
|
||||||
js: [],
|
|
||||||
head: '',
|
|
||||||
body: '',
|
|
||||||
inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed(),
|
|
||||||
meteorRuntimeConfig: JSON.stringify(__meteor_runtime_config__),
|
|
||||||
reloadSafetyBelt: RELOAD_SAFETYBELT,
|
|
||||||
rootUrlPathPrefix: __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '',
|
|
||||||
bundledJsCssPrefix: bundledJsCssPrefix ||
|
|
||||||
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''
|
|
||||||
};
|
|
||||||
|
|
||||||
_.each(WebApp.clientProgram.manifest, function (item) {
|
|
||||||
if (item.type === 'css' && item.where === 'client') {
|
|
||||||
boilerplateBaseData.css.push({url: item.url});
|
|
||||||
}
|
|
||||||
if (item.type === 'js' && item.where === 'client') {
|
|
||||||
boilerplateBaseData.js.push({url: item.url});
|
|
||||||
}
|
|
||||||
if (item.type === 'head') {
|
|
||||||
boilerplateBaseData.head = fs.readFileSync(
|
|
||||||
path.join(clientDir, item.path), 'utf8');
|
|
||||||
}
|
|
||||||
if (item.type === 'body') {
|
|
||||||
boilerplateBaseData.body = fs.readFileSync(
|
|
||||||
path.join(clientDir, item.path), 'utf8');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var boilerplateTemplateSource = Assets.getText("boilerplate.html");
|
var boilerplateTemplateSource = Assets.getText("boilerplate.html");
|
||||||
var boilerplateRenderCode = Spacebars.compile(
|
|
||||||
boilerplateTemplateSource, { isBody: true });
|
|
||||||
|
|
||||||
// Note that we are actually depending on eval's local environment capture
|
// Exported to allow client-side only changes to rebuild the boilerplate
|
||||||
// so that UI and HTML are visible to the eval'd code.
|
// without requiring a full server restart.
|
||||||
var boilerplateRender = eval(boilerplateRenderCode);
|
WebAppInternals.generateBoilerplate = function () {
|
||||||
|
syncQueue.runTask(function() {
|
||||||
|
boilerplateBaseData = {
|
||||||
|
css: [],
|
||||||
|
js: [],
|
||||||
|
head: '',
|
||||||
|
body: '',
|
||||||
|
inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed(),
|
||||||
|
meteorRuntimeConfig: JSON.stringify(__meteor_runtime_config__),
|
||||||
|
reloadSafetyBelt: RELOAD_SAFETYBELT,
|
||||||
|
rootUrlPathPrefix: __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '',
|
||||||
|
bundledJsCssPrefix: bundledJsCssPrefix ||
|
||||||
|
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''
|
||||||
|
};
|
||||||
|
|
||||||
boilerplateTemplate = UI.Component.extend({
|
_.each(WebApp.clientProgram.manifest, function (item) {
|
||||||
kind: "MainPage",
|
if (item.type === 'css' && item.where === 'client') {
|
||||||
render: boilerplateRender
|
boilerplateBaseData.css.push({url: item.url});
|
||||||
});
|
}
|
||||||
|
if (item.type === 'js' && item.where === 'client') {
|
||||||
|
boilerplateBaseData.js.push({url: item.url});
|
||||||
|
}
|
||||||
|
if (item.type === 'head') {
|
||||||
|
boilerplateBaseData.head =
|
||||||
|
readUtf8FileSync(path.join(clientDir, item.path));
|
||||||
|
}
|
||||||
|
if (item.type === 'body') {
|
||||||
|
boilerplateBaseData.body =
|
||||||
|
readUtf8FileSync(path.join(clientDir, item.path));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var boilerplateRenderCode = Spacebars.compile(
|
||||||
|
boilerplateTemplateSource, { 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.
|
||||||
|
var boilerplateRender = eval(boilerplateRenderCode);
|
||||||
|
boilerplateTemplate = UI.Component.extend({
|
||||||
|
kind: "MainPage",
|
||||||
|
render: boilerplateRender
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the memoized boilerplate cache.
|
||||||
|
boilerplateByAttributes = {};
|
||||||
|
|
||||||
|
WebAppInternals.refreshableAssets = { allCss: boilerplateBaseData.css };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
WebAppInternals.generateBoilerplate();
|
||||||
|
|
||||||
// only start listening after all the startup code has run.
|
// only start listening after all the startup code has run.
|
||||||
var localPort = parseInt(process.env.PORT) || 0;
|
var localPort = parseInt(process.env.PORT) || 0;
|
||||||
|
|||||||
@@ -1578,17 +1578,23 @@ var writeSiteArchive = function (targets, outputPath, options) {
|
|||||||
builder.writeJson('star.json', json);
|
builder.writeJson('star.json', json);
|
||||||
|
|
||||||
// Merge the WatchSet of everything that went into the bundle.
|
// Merge the WatchSet of everything that went into the bundle.
|
||||||
var watchSet = new watch.WatchSet();
|
var clientWatchSet = new watch.WatchSet();
|
||||||
|
var serverWatchSet = new watch.WatchSet();
|
||||||
var dependencySources = [builder].concat(_.values(targets));
|
var dependencySources = [builder].concat(_.values(targets));
|
||||||
_.each(dependencySources, function (s) {
|
_.each(dependencySources, function (s) {
|
||||||
watchSet.merge(s.getWatchSet());
|
if (s instanceof ClientTarget) {
|
||||||
|
clientWatchSet.merge(s.getWatchSet());
|
||||||
|
} else {
|
||||||
|
serverWatchSet.merge(s.getWatchSet());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// We did it!
|
// We did it!
|
||||||
builder.complete();
|
builder.complete();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
watchSet: watchSet,
|
clientWatchSet: clientWatchSet,
|
||||||
|
serverWatchSet: serverWatchSet,
|
||||||
starManifest: json
|
starManifest: json
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1669,12 +1675,14 @@ exports.bundle = function (options) {
|
|||||||
" " + release.current.name : "");
|
" " + release.current.name : "");
|
||||||
|
|
||||||
var success = false;
|
var success = false;
|
||||||
var watchSet = new watch.WatchSet();
|
var serverWatchSet = new watch.WatchSet();
|
||||||
|
var clientWatchSet = new watch.WatchSet();
|
||||||
var starResult = null;
|
var starResult = null;
|
||||||
|
var targets = {};
|
||||||
|
|
||||||
var messages = buildmessage.capture({
|
var messages = buildmessage.capture({
|
||||||
title: "building the application"
|
title: "building the application"
|
||||||
}, function () {
|
}, function () {
|
||||||
var targets = {};
|
|
||||||
var controlProgram = null;
|
var controlProgram = null;
|
||||||
|
|
||||||
var makeClientTarget = function (app) {
|
var makeClientTarget = function (app) {
|
||||||
@@ -1730,7 +1738,7 @@ exports.bundle = function (options) {
|
|||||||
// case.)
|
// case.)
|
||||||
|
|
||||||
var includeDefaultTargets = watch.readAndWatchFile(
|
var includeDefaultTargets = watch.readAndWatchFile(
|
||||||
watchSet, path.join(appDir, 'no-default-targets')) === null;
|
serverWatchSet, path.join(appDir, 'no-default-targets')) === null;
|
||||||
|
|
||||||
if (includeDefaultTargets) {
|
if (includeDefaultTargets) {
|
||||||
// Create a Unipackage object that represents the app
|
// Create a Unipackage object that represents the app
|
||||||
@@ -1742,7 +1750,8 @@ exports.bundle = function (options) {
|
|||||||
targets.client = client;
|
targets.client = client;
|
||||||
|
|
||||||
// Server
|
// Server
|
||||||
var server = makeServerTarget(app, client);
|
var server = options.cachedServerTarget || makeServerTarget(app, client);
|
||||||
|
server.clientTarget = client;
|
||||||
targets.server = server;
|
targets.server = server;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1752,7 +1761,7 @@ exports.bundle = function (options) {
|
|||||||
var programs = [];
|
var programs = [];
|
||||||
var programsDir = project.project.getProgramsDirectory();
|
var programsDir = project.project.getProgramsDirectory();
|
||||||
var programsSubdirs = project.project.getProgramsSubdirs({
|
var programsSubdirs = project.project.getProgramsSubdirs({
|
||||||
watchSet: watchSet
|
watchSet: serverWatchSet
|
||||||
});
|
});
|
||||||
|
|
||||||
_.each(programsSubdirs, function (item) {
|
_.each(programsSubdirs, function (item) {
|
||||||
@@ -1770,7 +1779,7 @@ exports.bundle = function (options) {
|
|||||||
// the package.js file here, though (but we do restart if it is later
|
// the package.js file here, though (but we do restart if it is later
|
||||||
// added or changed).
|
// added or changed).
|
||||||
if (watch.readAndWatchFile(
|
if (watch.readAndWatchFile(
|
||||||
watchSet, path.join(programsDir, item, 'package.js')) === null) {
|
serverWatchSet, path.join(programsDir, item, 'package.js')) === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1780,7 +1789,7 @@ exports.bundle = function (options) {
|
|||||||
var attrsJsonAbsPath = path.join(programsDir, item, 'attributes.json');
|
var attrsJsonAbsPath = path.join(programsDir, item, 'attributes.json');
|
||||||
var attrsJsonRelPath = path.join('programs', item, 'attributes.json');
|
var attrsJsonRelPath = path.join('programs', item, 'attributes.json');
|
||||||
var attrsJsonContents = watch.readAndWatchFile(
|
var attrsJsonContents = watch.readAndWatchFile(
|
||||||
watchSet, attrsJsonAbsPath);
|
serverWatchSet, attrsJsonAbsPath);
|
||||||
|
|
||||||
var attrsJson = {};
|
var attrsJson = {};
|
||||||
if (attrsJsonContents !== null) {
|
if (attrsJsonContents !== null) {
|
||||||
@@ -1900,7 +1909,8 @@ exports.bundle = function (options) {
|
|||||||
controlProgram: controlProgram,
|
controlProgram: controlProgram,
|
||||||
releaseName: releaseName
|
releaseName: releaseName
|
||||||
});
|
});
|
||||||
watchSet.merge(starResult.watchSet);
|
serverWatchSet.merge(starResult.serverWatchSet);
|
||||||
|
clientWatchSet.merge(starResult.clientWatchSet);
|
||||||
|
|
||||||
success = true;
|
success = true;
|
||||||
});
|
});
|
||||||
@@ -1910,8 +1920,10 @@ exports.bundle = function (options) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
errors: success ? false : messages,
|
errors: success ? false : messages,
|
||||||
watchSet: watchSet,
|
serverWatchSet: serverWatchSet,
|
||||||
starManifest: starResult && starResult.starManifest
|
clientWatchSet: clientWatchSet,
|
||||||
|
starManifest: starResult && starResult.starManifest,
|
||||||
|
serverTarget: targets.server
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -390,9 +390,16 @@ var compileUnibuild = function (unipackage, inputSourceArch, packageLoader,
|
|||||||
var fileOptions = _.clone(source.fileOptions) || {};
|
var fileOptions = _.clone(source.fileOptions) || {};
|
||||||
var absPath = path.resolve(inputSourceArch.pkg.sourceRoot, relPath);
|
var absPath = path.resolve(inputSourceArch.pkg.sourceRoot, relPath);
|
||||||
var filename = path.basename(relPath);
|
var filename = path.basename(relPath);
|
||||||
var file = watch.readAndWatchFileWithHash(watchSet, absPath);
|
var sourceWatchSet = new watch.WatchSet();
|
||||||
|
var file = watch.readAndWatchFileWithHash(sourceWatchSet, absPath);
|
||||||
var contents = file.contents;
|
var contents = file.contents;
|
||||||
|
|
||||||
|
// Only add the source file to the WatchSet if it's actually added to
|
||||||
|
// the build. This is a hacky workaround because plugins do not register
|
||||||
|
// themselves as "client" or "server", so we need to detect whether a file
|
||||||
|
// is actually added to the client/server program.
|
||||||
|
var sourceIsWatched = false;
|
||||||
|
|
||||||
sources.push(relPath);
|
sources.push(relPath);
|
||||||
|
|
||||||
if (contents === null) {
|
if (contents === null) {
|
||||||
@@ -563,6 +570,7 @@ var compileUnibuild = function (unipackage, inputSourceArch, packageLoader,
|
|||||||
throw new Error("'section' must be 'head' or 'body'");
|
throw new Error("'section' must be 'head' or 'body'");
|
||||||
if (typeof options.data !== "string")
|
if (typeof options.data !== "string")
|
||||||
throw new Error("'data' option to appendDocument must be a string");
|
throw new Error("'data' option to appendDocument must be a string");
|
||||||
|
sourceIsWatched = true;
|
||||||
resources.push({
|
resources.push({
|
||||||
type: options.section,
|
type: options.section,
|
||||||
data: new Buffer(options.data, 'utf8')
|
data: new Buffer(options.data, 'utf8')
|
||||||
@@ -574,8 +582,10 @@ var compileUnibuild = function (unipackage, inputSourceArch, packageLoader,
|
|||||||
"browser targets");
|
"browser targets");
|
||||||
if (typeof options.data !== "string")
|
if (typeof options.data !== "string")
|
||||||
throw new Error("'data' option to addStylesheet must be a string");
|
throw new Error("'data' option to addStylesheet must be a string");
|
||||||
|
sourceIsWatched = true;
|
||||||
resources.push({
|
resources.push({
|
||||||
type: "css",
|
type: "css",
|
||||||
|
refreshable: true,
|
||||||
data: new Buffer(options.data, 'utf8'),
|
data: new Buffer(options.data, 'utf8'),
|
||||||
servePath: path.join(inputSourceArch.pkg.serveRoot, options.path),
|
servePath: path.join(inputSourceArch.pkg.serveRoot, options.path),
|
||||||
sourceMap: options.sourceMap
|
sourceMap: options.sourceMap
|
||||||
@@ -588,6 +598,7 @@ var compileUnibuild = function (unipackage, inputSourceArch, packageLoader,
|
|||||||
throw new Error("'sourcePath' option must be supplied to addJavaScript. Consider passing inputPath.");
|
throw new Error("'sourcePath' option must be supplied to addJavaScript. Consider passing inputPath.");
|
||||||
if (options.bare && ! archinfo.matches(inputSourceArch.arch, "browser"))
|
if (options.bare && ! archinfo.matches(inputSourceArch.arch, "browser"))
|
||||||
throw new Error("'bare' option may only be used for browser targets");
|
throw new Error("'bare' option may only be used for browser targets");
|
||||||
|
sourceIsWatched = true;
|
||||||
js.push({
|
js.push({
|
||||||
source: options.data,
|
source: options.data,
|
||||||
sourcePath: options.sourcePath,
|
sourcePath: options.sourcePath,
|
||||||
@@ -599,6 +610,7 @@ var compileUnibuild = function (unipackage, inputSourceArch, packageLoader,
|
|||||||
addAsset: function (options) {
|
addAsset: function (options) {
|
||||||
if (! (options.data instanceof Buffer))
|
if (! (options.data instanceof Buffer))
|
||||||
throw new Error("'data' option to addAsset must be a Buffer");
|
throw new Error("'data' option to addAsset must be a Buffer");
|
||||||
|
sourceIsWatched = true;
|
||||||
addAsset(options.data, options.path);
|
addAsset(options.data, options.path);
|
||||||
},
|
},
|
||||||
error: function (options) {
|
error: function (options) {
|
||||||
@@ -620,6 +632,10 @@ var compileUnibuild = function (unipackage, inputSourceArch, packageLoader,
|
|||||||
// Recover by ignoring this source file (as best we can -- the
|
// Recover by ignoring this source file (as best we can -- the
|
||||||
// handler might already have emitted resources)
|
// handler might already have emitted resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sourceIsWatched) {
|
||||||
|
watchSet.merge(sourceWatchSet);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// *** Run Phase 1 link
|
// *** Run Phase 1 link
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ Options:
|
|||||||
Run tests of the 'meteor' tool.
|
Run tests of the 'meteor' tool.
|
||||||
Usage: meteor self-test [pattern] [--changed] [--slow]
|
Usage: meteor self-test [pattern] [--changed] [--slow]
|
||||||
[--force-online] [--history n]
|
[--force-online] [--history n]
|
||||||
|
[--browserstack]
|
||||||
|
|
||||||
Runs internal tests. Exits with status 0 on success.
|
Runs internal tests. Exits with status 0 on success.
|
||||||
|
|
||||||
|
|||||||
118
tools/run-app.js
118
tools/run-app.js
@@ -314,7 +314,8 @@ _.extend(AppProcess.prototype, {
|
|||||||
// - bundleResult: for runs in which bundling happened (all except
|
// - bundleResult: for runs in which bundling happened (all except
|
||||||
// 'wrong-release', 'conflicting-versions' and possibly 'stopped'), the return
|
// 'wrong-release', 'conflicting-versions' and possibly 'stopped'), the return
|
||||||
// value from bundler.bundle(), which contains such interesting things as the
|
// value from bundler.bundle(), which contains such interesting things as the
|
||||||
// build errors and a watchset describing the source files of the app.
|
// build errors and a watchset describing the server source files and client
|
||||||
|
// source files of the app.
|
||||||
var AppRunner = function (appDir, options) {
|
var AppRunner = function (appDir, options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@@ -410,19 +411,38 @@ _.extend(AppRunner.prototype, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bundle up the app
|
// Bundle up the app
|
||||||
if (! self.firstRun)
|
|
||||||
packageCache.packageCache.refresh(true); // pick up changes to packages
|
|
||||||
|
|
||||||
var bundlePath = path.join(self.appDir, '.meteor', 'local', 'build');
|
var bundlePath = path.join(self.appDir, '.meteor', 'local', 'build');
|
||||||
if (self.recordPackageUsage)
|
if (self.recordPackageUsage)
|
||||||
stats.recordPackages(self.appDir);
|
stats.recordPackages(self.appDir);
|
||||||
|
|
||||||
var bundleResult = bundler.bundle({
|
// Cache the server target because the server will not change inside
|
||||||
outputPath: bundlePath,
|
// a single invocation of _runOnce().
|
||||||
includeNodeModulesSymlink: true,
|
var cachedServerTarget = null;
|
||||||
buildOptions: self.buildOptions
|
var bundleApp = function () {
|
||||||
});
|
if (! self.firstRun)
|
||||||
var watchSet = bundleResult.watchSet;
|
packageCache.packageCache.refresh(true); // pick up changes to packages
|
||||||
|
|
||||||
|
var bundle = bundler.bundle({
|
||||||
|
outputPath: bundlePath,
|
||||||
|
includeNodeModulesSymlink: true,
|
||||||
|
buildOptions: self.buildOptions,
|
||||||
|
cachedServerTarget: cachedServerTarget
|
||||||
|
});
|
||||||
|
|
||||||
|
cachedServerTarget = bundle.serverTarget;
|
||||||
|
return bundle;
|
||||||
|
};
|
||||||
|
|
||||||
|
var bundleResult = bundleApp();
|
||||||
|
|
||||||
|
if (bundleResult.errors) {
|
||||||
|
return {
|
||||||
|
outcome: 'bundle-fail',
|
||||||
|
bundleResult: bundleResult
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverWatchSet = bundleResult.serverWatchSet;
|
||||||
|
|
||||||
// Read the settings file, if any
|
// Read the settings file, if any
|
||||||
var settings = null;
|
var settings = null;
|
||||||
@@ -438,7 +458,7 @@ _.extend(AppRunner.prototype, {
|
|||||||
// HACK: merge the watchset and messages from reading the settings
|
// HACK: merge the watchset and messages from reading the settings
|
||||||
// file into those from the build. This works fine but it sort of
|
// file into those from the build. This works fine but it sort of
|
||||||
// messy. Maybe clean it up sometime.
|
// messy. Maybe clean it up sometime.
|
||||||
watchSet.merge(settingsWatchSet);
|
serverWatchSet.merge(settingsWatchSet);
|
||||||
if (settingsMessages.hasMessages()) {
|
if (settingsMessages.hasMessages()) {
|
||||||
if (! bundleResult.errors)
|
if (! bundleResult.errors)
|
||||||
bundleResult.errors = settingsMessages;
|
bundleResult.errors = settingsMessages;
|
||||||
@@ -448,15 +468,7 @@ _.extend(AppRunner.prototype, {
|
|||||||
|
|
||||||
// HACK: Also make sure we notice when somebody adds a package to
|
// HACK: Also make sure we notice when somebody adds a package to
|
||||||
// the app packages dir that may override a catalog package.
|
// the app packages dir that may override a catalog package.
|
||||||
catalog.complete.watchLocalPackageDirs(watchSet);
|
catalog.complete.watchLocalPackageDirs(serverWatchSet);
|
||||||
|
|
||||||
// Were there errors?
|
|
||||||
if (bundleResult.errors) {
|
|
||||||
return {
|
|
||||||
outcome: 'bundle-fail',
|
|
||||||
bundleResult: bundleResult
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomically (1) see if we've been stop()'d, (2) if not, create a
|
// Atomically (1) see if we've been stop()'d, (2) if not, create a
|
||||||
// future that can be used to stop() us once we start running.
|
// future that can be used to stop() us once we start running.
|
||||||
@@ -464,7 +476,7 @@ _.extend(AppRunner.prototype, {
|
|||||||
return { outcome: 'stopped', bundleResult: bundleResult };
|
return { outcome: 'stopped', bundleResult: bundleResult };
|
||||||
if (self.runFuture)
|
if (self.runFuture)
|
||||||
throw new Error("already have future?");
|
throw new Error("already have future?");
|
||||||
var runFuture = self.runFuture = new Future;
|
self.runFuture = new Future;
|
||||||
|
|
||||||
// Run the program
|
// Run the program
|
||||||
var appProcess = new AppProcess({
|
var appProcess = new AppProcess({
|
||||||
@@ -495,13 +507,15 @@ _.extend(AppRunner.prototype, {
|
|||||||
appProcess.start();
|
appProcess.start();
|
||||||
|
|
||||||
// Start watching for changes for files if requested. There's no
|
// Start watching for changes for files if requested. There's no
|
||||||
// hurry to do this, since watchSet contains a snapshot of the
|
// hurry to do this, since clientWatchSet contains a snapshot of the
|
||||||
// state of the world at the time of bundling, in the form of
|
// state of the world at the time of bundling, in the form of
|
||||||
// hashes and lists of matching files in each directory.
|
// hashes and lists of matching files in each directory.
|
||||||
var watcher;
|
var serverWatcher;
|
||||||
|
var clientWatcher;
|
||||||
|
|
||||||
if (self.watchForChanges) {
|
if (self.watchForChanges) {
|
||||||
watcher = new watch.Watcher({
|
serverWatcher = new watch.Watcher({
|
||||||
watchSet: watchSet,
|
watchSet: serverWatchSet,
|
||||||
onChange: function () {
|
onChange: function () {
|
||||||
self._runFutureReturn({
|
self._runFutureReturn({
|
||||||
outcome: 'changed',
|
outcome: 'changed',
|
||||||
@@ -511,15 +525,57 @@ _.extend(AppRunner.prototype, {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var setupClientWatcher = function () {
|
||||||
|
clientWatcher && clientWatcher.stop();
|
||||||
|
clientWatcher = new watch.Watcher({
|
||||||
|
watchSet: bundleResult.clientWatchSet,
|
||||||
|
onChange: function () {
|
||||||
|
var outcome = watch.isUpToDate(serverWatchSet)
|
||||||
|
? 'changed-refreshable' // only a client asset has changed
|
||||||
|
: 'changed'; // both a client and server asset changed
|
||||||
|
self._runFutureReturn({
|
||||||
|
outcome: outcome,
|
||||||
|
bundleResult: bundleResult
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (self.watchForChanges) {
|
||||||
|
setupClientWatcher();
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for either the process to exit, or (if watchForChanges) a
|
// Wait for either the process to exit, or (if watchForChanges) a
|
||||||
// source file to change. Or, for stop() to be called.
|
// source file to change. Or, for stop() to be called.
|
||||||
var ret = runFuture.wait();
|
var ret = self.runFuture.wait();
|
||||||
|
|
||||||
|
while (ret.outcome === 'changed-refreshable') {
|
||||||
|
// We stay in this loop as long as only refreshable assets have changed.
|
||||||
|
// When ret.refreshable becomes false, we restart the server.
|
||||||
|
bundleResult = bundleApp();
|
||||||
|
if (bundleResult.errors) {
|
||||||
|
return {
|
||||||
|
outcome: 'bundle-fail',
|
||||||
|
bundleResult: bundleResult
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establish a watcher on the new files.
|
||||||
|
setupClientWatcher();
|
||||||
|
|
||||||
|
// Notify the server that new client assets have been added to the build.
|
||||||
|
process.kill(appProcess.proc.pid, 'SIGUSR2');
|
||||||
|
runLog.logClientRestart();
|
||||||
|
|
||||||
|
self.runFuture = new Future;
|
||||||
|
ret = self.runFuture.wait();
|
||||||
|
}
|
||||||
self.runFuture = null;
|
self.runFuture = null;
|
||||||
|
|
||||||
self.proxy.setMode("hold");
|
self.proxy.setMode("hold");
|
||||||
appProcess.stop();
|
appProcess.stop();
|
||||||
if (watcher)
|
|
||||||
watcher.stop();
|
serverWatcher && serverWatcher.stop();
|
||||||
|
clientWatcher && clientWatcher.stop();
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
@@ -614,8 +670,12 @@ _.extend(AppRunner.prototype, {
|
|||||||
|
|
||||||
if (self.watchForChanges) {
|
if (self.watchForChanges) {
|
||||||
self.watchFuture = new Future;
|
self.watchFuture = new Future;
|
||||||
|
|
||||||
|
var watchSet = new watch.WatchSet();
|
||||||
|
watchSet.merge(runResult.bundleResult.serverWatchSet);
|
||||||
|
watchSet.merge(runResult.bundleResult.clientWatchSet);
|
||||||
var watcher = new watch.Watcher({
|
var watcher = new watch.Watcher({
|
||||||
watchSet: runResult.bundleResult.watchSet,
|
watchSet: watchSet,
|
||||||
onChange: function () {
|
onChange: function () {
|
||||||
self._watchFutureReturn();
|
self._watchFutureReturn();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ var RunLog = function () {
|
|||||||
// message, and the value will be the number of consecutive such
|
// message, and the value will be the number of consecutive such
|
||||||
// messages that have been logged with no other intervening messages
|
// messages that have been logged with no other intervening messages
|
||||||
self.consecutiveRestartMessages = null;
|
self.consecutiveRestartMessages = null;
|
||||||
|
self.consecutiveClientRestartMessages = null;
|
||||||
|
|
||||||
// If non-null, the last thing that was logged was a temporary
|
// If non-null, the last thing that was logged was a temporary
|
||||||
// message (with a carriage return but no newline), and this is its
|
// message (with a carriage return but no newline), and this is its
|
||||||
@@ -66,6 +67,11 @@ _.extend(RunLog.prototype, {
|
|||||||
process.stdout.write("\n");
|
process.stdout.write("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.consecutiveClientRestartMessages) {
|
||||||
|
self.consecutiveClientRestartMessages = null;
|
||||||
|
process.stdout.write("\n");
|
||||||
|
}
|
||||||
|
|
||||||
if (self.temporaryMessageLength) {
|
if (self.temporaryMessageLength) {
|
||||||
var spaces = new Array(self.temporaryMessageLength + 1).join(' ');
|
var spaces = new Array(self.temporaryMessageLength + 1).join(' ');
|
||||||
process.stdout.write(spaces + '\r');
|
process.stdout.write(spaces + '\r');
|
||||||
@@ -155,6 +161,33 @@ _.extend(RunLog.prototype, {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
logClientRestart: function () {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (self.consecutiveClientRestartMessages) {
|
||||||
|
// replace old message in place. this assumes that the new restart message
|
||||||
|
// is not shorter than the old one.
|
||||||
|
process.stdout.write("\r");
|
||||||
|
self.messages.pop();
|
||||||
|
self.consecutiveClientRestartMessages ++;
|
||||||
|
} else {
|
||||||
|
self._clearSpecial();
|
||||||
|
self.consecutiveClientRestartMessages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = "=> Client modified -- refreshing";
|
||||||
|
if (self.consecutiveClientRestartMessages > 1)
|
||||||
|
message += " (x" + self.consecutiveClientRestartMessages + ")";
|
||||||
|
// no newline, so that we can overwrite it if we get another
|
||||||
|
// restart message right after this one
|
||||||
|
process.stdout.write(message);
|
||||||
|
|
||||||
|
self._record({
|
||||||
|
time: new Date,
|
||||||
|
message: message
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
finish: function () {
|
finish: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@@ -176,8 +209,8 @@ _.extend(RunLog.prototype, {
|
|||||||
// object you get with require('./run-log.js').
|
// object you get with require('./run-log.js').
|
||||||
var runLogInstance = new RunLog;
|
var runLogInstance = new RunLog;
|
||||||
_.each(
|
_.each(
|
||||||
['log', 'logTemporary', 'logRestart', 'logAppOutput', 'setRawLogs',
|
['log', 'logTemporary', 'logRestart', 'logClientRestart', 'logAppOutput',
|
||||||
'finish', 'clearLog', 'getLog'],
|
'setRawLogs', 'finish', 'clearLog', 'getLog'],
|
||||||
function (method) {
|
function (method) {
|
||||||
exports[method] = _.bind(runLogInstance[method], runLogInstance);
|
exports[method] = _.bind(runLogInstance[method], runLogInstance);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -408,6 +408,7 @@ _.extend(Sandbox.prototype, {
|
|||||||
" to run against clients." );
|
" to run against clients." );
|
||||||
}
|
}
|
||||||
_.each(self.clients, function (client) {
|
_.each(self.clients, function (client) {
|
||||||
|
console.log("testing with " + client.name + "...");
|
||||||
f(new Run(self.execPath, {
|
f(new Run(self.execPath, {
|
||||||
sandbox: self,
|
sandbox: self,
|
||||||
args: [],
|
args: [],
|
||||||
@@ -662,6 +663,7 @@ var PhantomClient = function (options) {
|
|||||||
var self = this;
|
var self = this;
|
||||||
Client.apply(this, arguments);
|
Client.apply(this, arguments);
|
||||||
|
|
||||||
|
self.name = "phantomjs";
|
||||||
self.process = null;
|
self.process = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -677,11 +679,7 @@ _.extend(PhantomClient.prototype, {
|
|||||||
'/bin/bash',
|
'/bin/bash',
|
||||||
['-c',
|
['-c',
|
||||||
("exec " + phantomPath + " --load-images=no /dev/stdin <<'END'\n" +
|
("exec " + phantomPath + " --load-images=no /dev/stdin <<'END'\n" +
|
||||||
phantomScript + "END\n")], function (err, stdout, stderr) {
|
phantomScript + "END\n")]);
|
||||||
if (stderr.match(/not found/)) {
|
|
||||||
console.log("ERROR: phantomjs not installed properly.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
stop: function() {
|
stop: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -697,6 +695,7 @@ var BrowserStackClient = function (options) {
|
|||||||
var self = this;
|
var self = this;
|
||||||
Client.apply(this, arguments);
|
Client.apply(this, arguments);
|
||||||
|
|
||||||
|
self.name = "BrowserStack";
|
||||||
self.tunnelProcess = null;
|
self.tunnelProcess = null;
|
||||||
self.driver = null;
|
self.driver = null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,17 +9,29 @@ if (Meteor.isClient) {
|
|||||||
}).join('\n'));
|
}).join('\n'));
|
||||||
};
|
};
|
||||||
|
|
||||||
Meteor.call("clientLoad");
|
Meteor.startup(function () {
|
||||||
var numCssChanges = 0;
|
Meteor.call("clientLoad");
|
||||||
var oldCss = allCss();
|
var numCssChanges = 0;
|
||||||
Meteor.call("newStylesheet", numCssChanges, oldCss);
|
var oldCss = allCss();
|
||||||
setInterval(function () {
|
Meteor.call("newStylesheet", numCssChanges, oldCss);
|
||||||
var newCss = allCss();
|
var callingServer = false;
|
||||||
if (oldCss !== newCss) {
|
Meteor.setInterval(function () {
|
||||||
oldCss = newCss;
|
if (callingServer)
|
||||||
Meteor.call("newStylesheet", ++numCssChanges, newCss);
|
return;
|
||||||
}
|
|
||||||
}, 500);
|
var newCss = allCss();
|
||||||
|
if (oldCss !== newCss) {
|
||||||
|
callingServer = true;
|
||||||
|
// give the client some time to load the new css
|
||||||
|
Meteor.setTimeout(function () {
|
||||||
|
var newCss = allCss();
|
||||||
|
oldCss = newCss;
|
||||||
|
Meteor.call("newStylesheet", ++numCssChanges, newCss);
|
||||||
|
callingServer = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Meteor.isServer) {
|
if (Meteor.isServer) {
|
||||||
@@ -30,7 +42,7 @@ if (Meteor.isServer) {
|
|||||||
|
|
||||||
newStylesheet: function (numCssChanges, cssText) {
|
newStylesheet: function (numCssChanges, cssText) {
|
||||||
console.log("numCssChanges: " + numCssChanges);
|
console.log("numCssChanges: " + numCssChanges);
|
||||||
console.log("new css: " + cssText);
|
console.log("css: " + cssText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
1
tools/tests/apps/hot-code-push-test/.meteor/.gitignore
vendored
Normal file
1
tools/tests/apps/hot-code-push-test/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
local
|
||||||
1
tools/tests/apps/hot-code-push-test/.meteor/identifier
Normal file
1
tools/tests/apps/hot-code-push-test/.meteor/identifier
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1da9lx3m24vwv1kt1w0a
|
||||||
6
tools/tests/apps/hot-code-push-test/.meteor/packages
Normal file
6
tools/tests/apps/hot-code-push-test/.meteor/packages
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Meteor packages used by this project, one per line.
|
||||||
|
#
|
||||||
|
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||||
|
# but you can also edit it by hand.
|
||||||
|
|
||||||
|
standard-app-packages
|
||||||
1
tools/tests/apps/hot-code-push-test/.meteor/release
Normal file
1
tools/tests/apps/hot-code-push-test/.meteor/release
Normal file
@@ -0,0 +1 @@
|
|||||||
|
none
|
||||||
40
tools/tests/apps/hot-code-push-test/.meteor/versions
Normal file
40
tools/tests/apps/hot-code-push-test/.meteor/versions
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
application-configuration@1.0.0
|
||||||
|
autopublish@1.0.0
|
||||||
|
autoupdate@1.0.0
|
||||||
|
binary-heap@1.0.0
|
||||||
|
callback-hook@1.0.0
|
||||||
|
check@1.0.0
|
||||||
|
ctl-helper@1.0.0
|
||||||
|
ctl@1.0.0
|
||||||
|
deps@1.0.0
|
||||||
|
ejson@1.0.0
|
||||||
|
follower-livedata@1.0.0
|
||||||
|
geojson-utils@1.0.0
|
||||||
|
html-tools@1.0.0
|
||||||
|
htmljs@1.0.0
|
||||||
|
id-map@1.0.0
|
||||||
|
insecure@1.0.0
|
||||||
|
jquery@1.0.0
|
||||||
|
json@1.0.0
|
||||||
|
livedata@1.0.0
|
||||||
|
logging@1.0.0
|
||||||
|
meteor@1.0.0
|
||||||
|
minifiers@1.0.0
|
||||||
|
minimongo@1.0.0
|
||||||
|
mongo-livedata@1.0.0
|
||||||
|
observe-sequence@1.0.0
|
||||||
|
ordered-dict@1.0.0
|
||||||
|
random@1.0.0
|
||||||
|
reactive-dict@1.0.0
|
||||||
|
reload@1.0.0
|
||||||
|
retry@1.0.0
|
||||||
|
routepolicy@1.0.0
|
||||||
|
session@1.0.0
|
||||||
|
spacebars-common@1.0.0
|
||||||
|
spacebars-compiler@1.0.0
|
||||||
|
spacebars@1.0.0
|
||||||
|
standard-app-packages@1.0.0
|
||||||
|
templating@1.0.0
|
||||||
|
ui@1.0.0
|
||||||
|
underscore@1.0.0
|
||||||
|
webapp@1.0.0
|
||||||
18
tools/tests/apps/hot-code-push-test/hot-code-push-test.js
Normal file
18
tools/tests/apps/hot-code-push-test/hot-code-push-test.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
if (Meteor.isClient) {
|
||||||
|
Meteor.startup(function () {
|
||||||
|
Meteor.call("clientLoad", typeof jsVar === 'undefined' ? 'undefined' : jsVar);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Meteor.isServer) {
|
||||||
|
var clientConnections;
|
||||||
|
Meteor.startup(function () {
|
||||||
|
clientConnections = 0;
|
||||||
|
});
|
||||||
|
Meteor.methods({
|
||||||
|
clientLoad: function (jsVar) {
|
||||||
|
console.log("client connected: " + clientConnections++);
|
||||||
|
console.log("jsVar: " + jsVar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -13,39 +13,130 @@ selftest.define("css injection", function (options) {
|
|||||||
|
|
||||||
s.createApp("myapp", "css-injection-test");
|
s.createApp("myapp", "css-injection-test");
|
||||||
s.cd("myapp");
|
s.cd("myapp");
|
||||||
|
|
||||||
s.testWithAllClients(function (run) {
|
s.testWithAllClients(function (run) {
|
||||||
s.set("METEOR_TEST_TMP", files.mkdtemp());
|
run.baseTimeout = 20;
|
||||||
run.match("myapp");
|
run.match("myapp");
|
||||||
run.match("proxy");
|
run.match("proxy");
|
||||||
run.match("MongoDB");
|
run.match("MongoDB");
|
||||||
run.waitSecs(20);
|
|
||||||
run.match("running at");
|
run.match("running at");
|
||||||
run.match("localhost");
|
run.match("localhost");
|
||||||
|
|
||||||
run.connectClient();
|
run.connectClient();
|
||||||
|
|
||||||
run.waitSecs(60);
|
|
||||||
run.match("client connected");
|
|
||||||
|
|
||||||
// Initially there is no CSS file.
|
|
||||||
run.waitSecs(20);
|
run.waitSecs(20);
|
||||||
run.match("numCssChanges: 0");
|
run.match("client connected");
|
||||||
run.match("new css:");
|
|
||||||
|
|
||||||
// 'numCssChanges' variable is set to 0 on a client refresh.
|
// 'numCssChanges' variable is set to 0 on a client refresh.
|
||||||
// Since CSS changes should not trigger a client refresh, numCssChanges
|
// Since CSS changes should not trigger a client refresh, numCssChanges
|
||||||
// should never reset.
|
// should never reset.
|
||||||
|
|
||||||
// XXX change test expectations when CSS injection patch lands.
|
// The css file is initially empty.
|
||||||
|
run.match("numCssChanges: 0");
|
||||||
|
run.match("css: \n");
|
||||||
|
|
||||||
|
// The server restarts if a new css file is added.
|
||||||
s.write("test.css", "body { background-color: red; }");
|
s.write("test.css", "body { background-color: red; }");
|
||||||
run.waitSecs(20);
|
run.match("server restarted");
|
||||||
run.match("numCssChanges: 0");
|
run.match("numCssChanges: 1");
|
||||||
run.match("new css: body { background-color: red; }");
|
run.match("css: body { background-color: red; }");
|
||||||
s.write("test.css", "body { background-color: blue; }");
|
|
||||||
run.waitSecs(20);
|
s.write("test.css", "body { background-color: orange; }");
|
||||||
run.match("numCssChanges: 0");
|
run.match("refreshing");
|
||||||
run.match("new css: body { background-color: blue; }");
|
run.match("numCssChanges: 2");
|
||||||
|
run.match("css: body { background-color: orange; }");
|
||||||
|
|
||||||
|
// The server restarts if a css file is removed.
|
||||||
|
s.unlink("test.css");
|
||||||
|
run.match("server restarted");
|
||||||
|
run.match("numCssChanges: 3");
|
||||||
|
run.match("css: \n");
|
||||||
|
run.stop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
selftest.define("javascript hot code push", function (options) {
|
||||||
|
var s = new Sandbox({
|
||||||
|
clients: options.clients,
|
||||||
|
});
|
||||||
|
|
||||||
|
s.createApp("myapp", "hot-code-push-test");
|
||||||
|
s.cd("myapp");
|
||||||
|
s.testWithAllClients(function (run) {
|
||||||
|
run.baseTimeout = 20;
|
||||||
|
run.match("myapp");
|
||||||
|
run.match("proxy");
|
||||||
|
run.match("MongoDB");
|
||||||
|
run.match("running at");
|
||||||
|
run.match("localhost");
|
||||||
|
|
||||||
|
run.connectClient();
|
||||||
|
run.waitSecs(20);
|
||||||
|
|
||||||
|
// There is initially no JavaScript file.
|
||||||
|
run.match("client connected: 0");
|
||||||
|
run.match("jsVar: undefined");
|
||||||
|
|
||||||
|
// The server and client both restart if a shared js file is added
|
||||||
|
// or removed.
|
||||||
|
s.write("test.js", "jsVar = 'foo'");
|
||||||
|
run.match("server restarted");
|
||||||
|
run.match("client connected: 0");
|
||||||
|
run.match("jsVar: foo");
|
||||||
|
|
||||||
|
s.unlink("test.js");
|
||||||
|
run.match("server restarted");
|
||||||
|
run.match("client connected: 0");
|
||||||
|
run.match("jsVar: undefined");
|
||||||
|
|
||||||
|
// Only the client should refresh if a client js file is added. Thus,
|
||||||
|
// "client connected" variable will be incremented.
|
||||||
|
s.write("client/test.js", "jsVar = 'bar'");
|
||||||
|
run.match("client connected: 1");
|
||||||
|
run.match("jsVar: bar");
|
||||||
|
|
||||||
|
s.unlink("client/test.js");
|
||||||
|
run.match("client connected: 2");
|
||||||
|
run.match("jsVar: undefined");
|
||||||
|
|
||||||
|
// When we change a server file the client should not refresh. We observe
|
||||||
|
// this by changing a server file and then a client file and verifying
|
||||||
|
// that the client has only connected once.
|
||||||
|
s.write("server/test.js", "jsVar = 'bar'");
|
||||||
|
run.match("server restarted");
|
||||||
|
s.write("client/empty.js", "");
|
||||||
|
run.match("client connected: 0");
|
||||||
|
run.match("jsVar: undefined"); // cannot access a server variable from the client.
|
||||||
|
|
||||||
|
s.unlink("server/test.js");
|
||||||
|
run.match("server restarted");
|
||||||
|
s.unlink("client/empty.js");
|
||||||
|
run.match("client connected: 0");
|
||||||
|
run.match("jsVar: undefined");
|
||||||
|
|
||||||
|
// Add appcache and ensure that the browser still reloads.
|
||||||
|
s.write(".meteor/packages", "standard-app-packages \n appcache");
|
||||||
|
run.match("added appcache");
|
||||||
|
run.match("server restarted");
|
||||||
|
run.match("client connected: 0");
|
||||||
|
run.match("jsVar: undefined");
|
||||||
|
|
||||||
|
s.write("client/test.js", "jsVar = 'bar'");
|
||||||
|
run.match("client connected: 1");
|
||||||
|
run.match("jsVar: bar");
|
||||||
|
|
||||||
|
// Remove appcache and ensure that the browser still reloads.
|
||||||
|
s.write(".meteor/packages", "standard-app-packages");
|
||||||
|
run.match("removed");
|
||||||
|
run.match("appcache");
|
||||||
|
run.match("server restarted");
|
||||||
|
run.match("client connected: 0");
|
||||||
|
run.match("jsVar: bar");
|
||||||
|
|
||||||
|
s.write("client/test.js", "jsVar = 'baz'");
|
||||||
|
run.match("client connected: 1");
|
||||||
|
run.match("jsVar: baz");
|
||||||
|
s.unlink("client/test.js");
|
||||||
|
|
||||||
run.stop();
|
run.stop();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user