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
|
||||
// browser.
|
||||
var autoupdateVersion = __meteor_runtime_config__.autoupdateVersion || "unknown";
|
||||
|
||||
var autoupdateVersionRefreshable =
|
||||
__meteor_runtime_config__.autoupdateVersionRefreshable || "unknown";
|
||||
|
||||
// The collection of acceptable client versions.
|
||||
var ClientVersions = new Meteor.Collection("meteor_autoupdate_clientVersions");
|
||||
|
||||
ClientVersions = new Meteor.Collection("meteor_autoupdate_clientVersions");
|
||||
|
||||
Autoupdate = {};
|
||||
|
||||
@@ -42,7 +42,7 @@ Autoupdate.newClientAvailable = function () {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
var knownToSupportCssOnLoad = false;
|
||||
|
||||
var retry = new Retry({
|
||||
// Unlike the stream reconnect use of Retry, which we want to be instant
|
||||
@@ -76,15 +76,72 @@ Autoupdate._retrySubscription = function () {
|
||||
},
|
||||
onReady: function () {
|
||||
if (Package.reload) {
|
||||
Deps.autorun(function (computation) {
|
||||
if (ClientVersions.findOne({current: true}) &&
|
||||
(! ClientVersions.findOne({_id: autoupdateVersion}))) {
|
||||
computation.stop();
|
||||
Package.reload.Reload._reload();
|
||||
var handle = ClientVersions.find().observeChanges({
|
||||
added: function (id, fields) {
|
||||
var self = this;
|
||||
if (fields.refreshable && id !== autoupdateVersionRefreshable) {
|
||||
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();
|
||||
|
||||
@@ -37,45 +37,90 @@
|
||||
|
||||
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
|
||||
// all packages have loaded and have had a chance to populate the
|
||||
// runtime config before using the client hash as our default auto
|
||||
// update version id.
|
||||
|
||||
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 () {
|
||||
// Allow people to override Autoupdate.autoupdateVersion before
|
||||
// startup. Tests do this.
|
||||
if (Autoupdate.autoupdateVersion === null)
|
||||
Autoupdate.autoupdateVersion =
|
||||
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;
|
||||
// Allow people to override Autoupdate.autoupdateVersion before startup.
|
||||
// Tests do this.
|
||||
startupVersion = Autoupdate.autoupdateVersion;
|
||||
WebApp.onListening(updateVersions);
|
||||
});
|
||||
|
||||
|
||||
Meteor.publish(
|
||||
"meteor_autoupdate_clientVersions",
|
||||
function () {
|
||||
var self = this;
|
||||
// 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"));
|
||||
}
|
||||
return ClientVersions.find();
|
||||
},
|
||||
{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}}>
|
||||
<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}}
|
||||
<script type='text/javascript'>__meteor_runtime_config__ = {{meteorRuntimeConfig}};</script>
|
||||
|
||||
@@ -11,6 +11,8 @@ var connect = Npm.require('connect');
|
||||
var useragent = Npm.require('useragent');
|
||||
var send = Npm.require('send');
|
||||
|
||||
var Future = Npm.require('fibers/future');
|
||||
|
||||
var SHORT_SOCKET_TIMEOUT = 5*1000;
|
||||
var LONG_SOCKET_TIMEOUT = 120*1000;
|
||||
|
||||
@@ -62,6 +64,10 @@ var sha1 = function (contents) {
|
||||
return hash.digest('hex');
|
||||
};
|
||||
|
||||
var readUtf8FileSync = function (filename) {
|
||||
return Future.wrap(fs.readFile)(filename, 'utf8').wait();
|
||||
};
|
||||
|
||||
// #BrowserIdentification
|
||||
//
|
||||
// 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
|
||||
// requirement).
|
||||
|
||||
var calculateClientHash = function () {
|
||||
var calculateClientHash = function (includeFilter) {
|
||||
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) {
|
||||
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.hash);
|
||||
}
|
||||
@@ -211,6 +221,16 @@ var calculateClientHash = function () {
|
||||
|
||||
Meteor.startup(function () {
|
||||
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 shuttingDown = false;
|
||||
// read the control for the client we'll be serving up
|
||||
var clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
|
||||
__meteor_bootstrap__.configJson.client);
|
||||
var clientDir = path.dirname(clientJsonPath);
|
||||
var clientJson = JSON.parse(fs.readFileSync(clientJsonPath, 'utf8'));
|
||||
var syncQueue = new Meteor._SynchronousQueue();
|
||||
|
||||
if (clientJson.format !== "browser-program-pre1")
|
||||
throw new Error("Unsupported format for client assets: " +
|
||||
JSON.stringify(clientJson.format));
|
||||
var getItemPathname = function (itemUrl) {
|
||||
return decodeURIComponent(url.parse(itemUrl).pathname);
|
||||
};
|
||||
|
||||
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
|
||||
var app = connect();
|
||||
@@ -285,36 +359,6 @@ var runWebAppServer = function () {
|
||||
// generally pretty handy..
|
||||
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.
|
||||
// This is inspired by the 'static' middleware.
|
||||
app.use(function (req, res, next) {
|
||||
@@ -480,10 +524,9 @@ var runWebAppServer = function () {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var htmlAttributes = getHtmlAttributes(request);
|
||||
|
||||
// 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.
|
||||
var htmlAttributes = getHtmlAttributes(request);
|
||||
var attributeKey = JSON.stringify(htmlAttributes);
|
||||
if (!_.has(boilerplateByAttributes, attributeKey)) {
|
||||
try {
|
||||
@@ -568,12 +611,6 @@ var runWebAppServer = function () {
|
||||
connectHandlers: packageAndAppHandlers,
|
||||
rawConnectHandlers: rawConnectHandlers,
|
||||
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.
|
||||
suppressConnectErrors: function () {
|
||||
suppressConnectErrors = true;
|
||||
@@ -602,48 +639,60 @@ var runWebAppServer = function () {
|
||||
// '--keepalive' is a use of the option.
|
||||
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 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);
|
||||
// Exported to allow client-side only changes to rebuild the boilerplate
|
||||
// without requiring a full server restart.
|
||||
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({
|
||||
kind: "MainPage",
|
||||
render: boilerplateRender
|
||||
});
|
||||
_.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 =
|
||||
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.
|
||||
var localPort = parseInt(process.env.PORT) || 0;
|
||||
|
||||
@@ -1578,17 +1578,23 @@ var writeSiteArchive = function (targets, outputPath, options) {
|
||||
builder.writeJson('star.json', json);
|
||||
|
||||
// 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));
|
||||
_.each(dependencySources, function (s) {
|
||||
watchSet.merge(s.getWatchSet());
|
||||
if (s instanceof ClientTarget) {
|
||||
clientWatchSet.merge(s.getWatchSet());
|
||||
} else {
|
||||
serverWatchSet.merge(s.getWatchSet());
|
||||
}
|
||||
});
|
||||
|
||||
// We did it!
|
||||
builder.complete();
|
||||
|
||||
return {
|
||||
watchSet: watchSet,
|
||||
clientWatchSet: clientWatchSet,
|
||||
serverWatchSet: serverWatchSet,
|
||||
starManifest: json
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -1669,12 +1675,14 @@ exports.bundle = function (options) {
|
||||
" " + release.current.name : "");
|
||||
|
||||
var success = false;
|
||||
var watchSet = new watch.WatchSet();
|
||||
var serverWatchSet = new watch.WatchSet();
|
||||
var clientWatchSet = new watch.WatchSet();
|
||||
var starResult = null;
|
||||
var targets = {};
|
||||
|
||||
var messages = buildmessage.capture({
|
||||
title: "building the application"
|
||||
}, function () {
|
||||
var targets = {};
|
||||
var controlProgram = null;
|
||||
|
||||
var makeClientTarget = function (app) {
|
||||
@@ -1730,7 +1738,7 @@ exports.bundle = function (options) {
|
||||
// case.)
|
||||
|
||||
var includeDefaultTargets = watch.readAndWatchFile(
|
||||
watchSet, path.join(appDir, 'no-default-targets')) === null;
|
||||
serverWatchSet, path.join(appDir, 'no-default-targets')) === null;
|
||||
|
||||
if (includeDefaultTargets) {
|
||||
// Create a Unipackage object that represents the app
|
||||
@@ -1742,7 +1750,8 @@ exports.bundle = function (options) {
|
||||
targets.client = client;
|
||||
|
||||
// Server
|
||||
var server = makeServerTarget(app, client);
|
||||
var server = options.cachedServerTarget || makeServerTarget(app, client);
|
||||
server.clientTarget = client;
|
||||
targets.server = server;
|
||||
}
|
||||
|
||||
@@ -1752,7 +1761,7 @@ exports.bundle = function (options) {
|
||||
var programs = [];
|
||||
var programsDir = project.project.getProgramsDirectory();
|
||||
var programsSubdirs = project.project.getProgramsSubdirs({
|
||||
watchSet: watchSet
|
||||
watchSet: serverWatchSet
|
||||
});
|
||||
|
||||
_.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
|
||||
// added or changed).
|
||||
if (watch.readAndWatchFile(
|
||||
watchSet, path.join(programsDir, item, 'package.js')) === null) {
|
||||
serverWatchSet, path.join(programsDir, item, 'package.js')) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1780,7 +1789,7 @@ exports.bundle = function (options) {
|
||||
var attrsJsonAbsPath = path.join(programsDir, item, 'attributes.json');
|
||||
var attrsJsonRelPath = path.join('programs', item, 'attributes.json');
|
||||
var attrsJsonContents = watch.readAndWatchFile(
|
||||
watchSet, attrsJsonAbsPath);
|
||||
serverWatchSet, attrsJsonAbsPath);
|
||||
|
||||
var attrsJson = {};
|
||||
if (attrsJsonContents !== null) {
|
||||
@@ -1900,7 +1909,8 @@ exports.bundle = function (options) {
|
||||
controlProgram: controlProgram,
|
||||
releaseName: releaseName
|
||||
});
|
||||
watchSet.merge(starResult.watchSet);
|
||||
serverWatchSet.merge(starResult.serverWatchSet);
|
||||
clientWatchSet.merge(starResult.clientWatchSet);
|
||||
|
||||
success = true;
|
||||
});
|
||||
@@ -1910,8 +1920,10 @@ exports.bundle = function (options) {
|
||||
|
||||
return {
|
||||
errors: success ? false : messages,
|
||||
watchSet: watchSet,
|
||||
starManifest: starResult && starResult.starManifest
|
||||
serverWatchSet: serverWatchSet,
|
||||
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 absPath = path.resolve(inputSourceArch.pkg.sourceRoot, 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;
|
||||
|
||||
// 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);
|
||||
|
||||
if (contents === null) {
|
||||
@@ -563,6 +570,7 @@ var compileUnibuild = function (unipackage, inputSourceArch, packageLoader,
|
||||
throw new Error("'section' must be 'head' or 'body'");
|
||||
if (typeof options.data !== "string")
|
||||
throw new Error("'data' option to appendDocument must be a string");
|
||||
sourceIsWatched = true;
|
||||
resources.push({
|
||||
type: options.section,
|
||||
data: new Buffer(options.data, 'utf8')
|
||||
@@ -574,8 +582,10 @@ var compileUnibuild = function (unipackage, inputSourceArch, packageLoader,
|
||||
"browser targets");
|
||||
if (typeof options.data !== "string")
|
||||
throw new Error("'data' option to addStylesheet must be a string");
|
||||
sourceIsWatched = true;
|
||||
resources.push({
|
||||
type: "css",
|
||||
refreshable: true,
|
||||
data: new Buffer(options.data, 'utf8'),
|
||||
servePath: path.join(inputSourceArch.pkg.serveRoot, options.path),
|
||||
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.");
|
||||
if (options.bare && ! archinfo.matches(inputSourceArch.arch, "browser"))
|
||||
throw new Error("'bare' option may only be used for browser targets");
|
||||
sourceIsWatched = true;
|
||||
js.push({
|
||||
source: options.data,
|
||||
sourcePath: options.sourcePath,
|
||||
@@ -599,6 +610,7 @@ var compileUnibuild = function (unipackage, inputSourceArch, packageLoader,
|
||||
addAsset: function (options) {
|
||||
if (! (options.data instanceof Buffer))
|
||||
throw new Error("'data' option to addAsset must be a Buffer");
|
||||
sourceIsWatched = true;
|
||||
addAsset(options.data, options.path);
|
||||
},
|
||||
error: function (options) {
|
||||
@@ -620,6 +632,10 @@ var compileUnibuild = function (unipackage, inputSourceArch, packageLoader,
|
||||
// Recover by ignoring this source file (as best we can -- the
|
||||
// handler might already have emitted resources)
|
||||
}
|
||||
|
||||
if (sourceIsWatched) {
|
||||
watchSet.merge(sourceWatchSet);
|
||||
}
|
||||
});
|
||||
|
||||
// *** Run Phase 1 link
|
||||
|
||||
@@ -286,6 +286,7 @@ Options:
|
||||
Run tests of the 'meteor' tool.
|
||||
Usage: meteor self-test [pattern] [--changed] [--slow]
|
||||
[--force-online] [--history n]
|
||||
[--browserstack]
|
||||
|
||||
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
|
||||
// 'wrong-release', 'conflicting-versions' and possibly 'stopped'), the return
|
||||
// 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 self = this;
|
||||
|
||||
@@ -410,19 +411,38 @@ _.extend(AppRunner.prototype, {
|
||||
}
|
||||
|
||||
// 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');
|
||||
if (self.recordPackageUsage)
|
||||
stats.recordPackages(self.appDir);
|
||||
|
||||
var bundleResult = bundler.bundle({
|
||||
outputPath: bundlePath,
|
||||
includeNodeModulesSymlink: true,
|
||||
buildOptions: self.buildOptions
|
||||
});
|
||||
var watchSet = bundleResult.watchSet;
|
||||
// Cache the server target because the server will not change inside
|
||||
// a single invocation of _runOnce().
|
||||
var cachedServerTarget = null;
|
||||
var bundleApp = function () {
|
||||
if (! self.firstRun)
|
||||
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
|
||||
var settings = null;
|
||||
@@ -438,7 +458,7 @@ _.extend(AppRunner.prototype, {
|
||||
// HACK: merge the watchset and messages from reading the settings
|
||||
// file into those from the build. This works fine but it sort of
|
||||
// messy. Maybe clean it up sometime.
|
||||
watchSet.merge(settingsWatchSet);
|
||||
serverWatchSet.merge(settingsWatchSet);
|
||||
if (settingsMessages.hasMessages()) {
|
||||
if (! bundleResult.errors)
|
||||
bundleResult.errors = settingsMessages;
|
||||
@@ -448,15 +468,7 @@ _.extend(AppRunner.prototype, {
|
||||
|
||||
// HACK: Also make sure we notice when somebody adds a package to
|
||||
// the app packages dir that may override a catalog package.
|
||||
catalog.complete.watchLocalPackageDirs(watchSet);
|
||||
|
||||
// Were there errors?
|
||||
if (bundleResult.errors) {
|
||||
return {
|
||||
outcome: 'bundle-fail',
|
||||
bundleResult: bundleResult
|
||||
};
|
||||
}
|
||||
catalog.complete.watchLocalPackageDirs(serverWatchSet);
|
||||
|
||||
// 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.
|
||||
@@ -464,7 +476,7 @@ _.extend(AppRunner.prototype, {
|
||||
return { outcome: 'stopped', bundleResult: bundleResult };
|
||||
if (self.runFuture)
|
||||
throw new Error("already have future?");
|
||||
var runFuture = self.runFuture = new Future;
|
||||
self.runFuture = new Future;
|
||||
|
||||
// Run the program
|
||||
var appProcess = new AppProcess({
|
||||
@@ -495,13 +507,15 @@ _.extend(AppRunner.prototype, {
|
||||
appProcess.start();
|
||||
|
||||
// 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
|
||||
// hashes and lists of matching files in each directory.
|
||||
var watcher;
|
||||
var serverWatcher;
|
||||
var clientWatcher;
|
||||
|
||||
if (self.watchForChanges) {
|
||||
watcher = new watch.Watcher({
|
||||
watchSet: watchSet,
|
||||
serverWatcher = new watch.Watcher({
|
||||
watchSet: serverWatchSet,
|
||||
onChange: function () {
|
||||
self._runFutureReturn({
|
||||
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
|
||||
// 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.proxy.setMode("hold");
|
||||
appProcess.stop();
|
||||
if (watcher)
|
||||
watcher.stop();
|
||||
|
||||
serverWatcher && serverWatcher.stop();
|
||||
clientWatcher && clientWatcher.stop();
|
||||
|
||||
return ret;
|
||||
},
|
||||
@@ -614,8 +670,12 @@ _.extend(AppRunner.prototype, {
|
||||
|
||||
if (self.watchForChanges) {
|
||||
self.watchFuture = new Future;
|
||||
|
||||
var watchSet = new watch.WatchSet();
|
||||
watchSet.merge(runResult.bundleResult.serverWatchSet);
|
||||
watchSet.merge(runResult.bundleResult.clientWatchSet);
|
||||
var watcher = new watch.Watcher({
|
||||
watchSet: runResult.bundleResult.watchSet,
|
||||
watchSet: watchSet,
|
||||
onChange: function () {
|
||||
self._watchFutureReturn();
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ var RunLog = function () {
|
||||
// message, and the value will be the number of consecutive such
|
||||
// messages that have been logged with no other intervening messages
|
||||
self.consecutiveRestartMessages = null;
|
||||
self.consecutiveClientRestartMessages = null;
|
||||
|
||||
// If non-null, the last thing that was logged was a temporary
|
||||
// message (with a carriage return but no newline), and this is its
|
||||
@@ -66,6 +67,11 @@ _.extend(RunLog.prototype, {
|
||||
process.stdout.write("\n");
|
||||
}
|
||||
|
||||
if (self.consecutiveClientRestartMessages) {
|
||||
self.consecutiveClientRestartMessages = null;
|
||||
process.stdout.write("\n");
|
||||
}
|
||||
|
||||
if (self.temporaryMessageLength) {
|
||||
var spaces = new Array(self.temporaryMessageLength + 1).join(' ');
|
||||
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 () {
|
||||
var self = this;
|
||||
|
||||
@@ -176,8 +209,8 @@ _.extend(RunLog.prototype, {
|
||||
// object you get with require('./run-log.js').
|
||||
var runLogInstance = new RunLog;
|
||||
_.each(
|
||||
['log', 'logTemporary', 'logRestart', 'logAppOutput', 'setRawLogs',
|
||||
'finish', 'clearLog', 'getLog'],
|
||||
['log', 'logTemporary', 'logRestart', 'logClientRestart', 'logAppOutput',
|
||||
'setRawLogs', 'finish', 'clearLog', 'getLog'],
|
||||
function (method) {
|
||||
exports[method] = _.bind(runLogInstance[method], runLogInstance);
|
||||
});
|
||||
|
||||
@@ -408,6 +408,7 @@ _.extend(Sandbox.prototype, {
|
||||
" to run against clients." );
|
||||
}
|
||||
_.each(self.clients, function (client) {
|
||||
console.log("testing with " + client.name + "...");
|
||||
f(new Run(self.execPath, {
|
||||
sandbox: self,
|
||||
args: [],
|
||||
@@ -662,6 +663,7 @@ var PhantomClient = function (options) {
|
||||
var self = this;
|
||||
Client.apply(this, arguments);
|
||||
|
||||
self.name = "phantomjs";
|
||||
self.process = null;
|
||||
};
|
||||
|
||||
@@ -677,11 +679,7 @@ _.extend(PhantomClient.prototype, {
|
||||
'/bin/bash',
|
||||
['-c',
|
||||
("exec " + phantomPath + " --load-images=no /dev/stdin <<'END'\n" +
|
||||
phantomScript + "END\n")], function (err, stdout, stderr) {
|
||||
if (stderr.match(/not found/)) {
|
||||
console.log("ERROR: phantomjs not installed properly.");
|
||||
}
|
||||
});
|
||||
phantomScript + "END\n")]);
|
||||
},
|
||||
stop: function() {
|
||||
var self = this;
|
||||
@@ -697,6 +695,7 @@ var BrowserStackClient = function (options) {
|
||||
var self = this;
|
||||
Client.apply(this, arguments);
|
||||
|
||||
self.name = "BrowserStack";
|
||||
self.tunnelProcess = null;
|
||||
self.driver = null;
|
||||
};
|
||||
|
||||
@@ -9,17 +9,29 @@ if (Meteor.isClient) {
|
||||
}).join('\n'));
|
||||
};
|
||||
|
||||
Meteor.call("clientLoad");
|
||||
var numCssChanges = 0;
|
||||
var oldCss = allCss();
|
||||
Meteor.call("newStylesheet", numCssChanges, oldCss);
|
||||
setInterval(function () {
|
||||
var newCss = allCss();
|
||||
if (oldCss !== newCss) {
|
||||
oldCss = newCss;
|
||||
Meteor.call("newStylesheet", ++numCssChanges, newCss);
|
||||
}
|
||||
}, 500);
|
||||
Meteor.startup(function () {
|
||||
Meteor.call("clientLoad");
|
||||
var numCssChanges = 0;
|
||||
var oldCss = allCss();
|
||||
Meteor.call("newStylesheet", numCssChanges, oldCss);
|
||||
var callingServer = false;
|
||||
Meteor.setInterval(function () {
|
||||
if (callingServer)
|
||||
return;
|
||||
|
||||
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) {
|
||||
@@ -30,7 +42,7 @@ if (Meteor.isServer) {
|
||||
|
||||
newStylesheet: function (numCssChanges, cssText) {
|
||||
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.cd("myapp");
|
||||
|
||||
s.testWithAllClients(function (run) {
|
||||
s.set("METEOR_TEST_TMP", files.mkdtemp());
|
||||
run.baseTimeout = 20;
|
||||
run.match("myapp");
|
||||
run.match("proxy");
|
||||
run.match("MongoDB");
|
||||
run.waitSecs(20);
|
||||
run.match("running at");
|
||||
run.match("localhost");
|
||||
|
||||
run.connectClient();
|
||||
|
||||
run.waitSecs(60);
|
||||
run.match("client connected");
|
||||
|
||||
// Initially there is no CSS file.
|
||||
run.waitSecs(20);
|
||||
run.match("numCssChanges: 0");
|
||||
run.match("new css:");
|
||||
run.match("client connected");
|
||||
|
||||
// 'numCssChanges' variable is set to 0 on a client refresh.
|
||||
// Since CSS changes should not trigger a client refresh, numCssChanges
|
||||
// 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; }");
|
||||
run.waitSecs(20);
|
||||
run.match("numCssChanges: 0");
|
||||
run.match("new css: body { background-color: red; }");
|
||||
s.write("test.css", "body { background-color: blue; }");
|
||||
run.waitSecs(20);
|
||||
run.match("numCssChanges: 0");
|
||||
run.match("new css: body { background-color: blue; }");
|
||||
run.match("server restarted");
|
||||
run.match("numCssChanges: 1");
|
||||
run.match("css: body { background-color: red; }");
|
||||
|
||||
s.write("test.css", "body { background-color: orange; }");
|
||||
run.match("refreshing");
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user