Client CSS and template injection.

This commit is contained in:
Matthew Arbesfeld
2014-06-10 16:52:33 -07:00
parent 8ed9d3459c
commit f230eba62b
18 changed files with 646 additions and 204 deletions

View File

@@ -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();

View File

@@ -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();
}));

View File

@@ -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>

View File

@@ -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;

View File

@@ -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
};
};

View File

@@ -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

View File

@@ -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.

View File

@@ -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();
}

View File

@@ -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);
});

View File

@@ -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;
};

View File

@@ -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);
}
});
}

View File

@@ -0,0 +1 @@
local

View File

@@ -0,0 +1 @@
1da9lx3m24vwv1kt1w0a

View 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

View File

@@ -0,0 +1 @@
none

View 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

View 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);
}
});
}

View File

@@ -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();
});
});