diff --git a/History.md b/History.md index 9c3a62d71e..c96bc8f5b1 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,8 @@ ## vNEXT +## v0.6.5 + * linker! namespacing, exports, unipackages, weak and unordered dependencies, etc. sourcemaps (including for coffee). standard-app-packages. don't implicitly use all app packages. lots of stuff moved from server.js (now diff --git a/packages/json/json_native.js b/packages/json/json_native.js new file mode 100644 index 0000000000..bbb4e00f84 --- /dev/null +++ b/packages/json/json_native.js @@ -0,0 +1,3 @@ +// Do we already have a global JSON object? Export it as our JSON object. +if (window.JSON) + JSON = window.JSON; diff --git a/packages/json/package.js b/packages/json/package.js index 87602a5998..abb6a2ec71 100644 --- a/packages/json/package.js +++ b/packages/json/package.js @@ -3,10 +3,12 @@ Package.describe({ internal: true }); -// We need to figure out how to serve this file only to browsers that -// don't have JSON.stringify (eg, IE7 and earlier -- or is that IE8?) +// We need to figure out how to serve this file only to browsers that don't have +// JSON.stringify (eg, IE7 and earlier, and IE8 outside of "standards mode") Package.on_use(function (api) { // Node always has JSON; we only need this in some browsers. + api.export('JSON', 'client'); + api.add_files('json_native.js', 'client'); api.add_files('json2.js', 'client'); }); diff --git a/packages/less/plugin/compile-less.js b/packages/less/plugin/compile-less.js index a860ad2861..41980738a8 100644 --- a/packages/less/plugin/compile-less.js +++ b/packages/less/plugin/compile-less.js @@ -1,8 +1,18 @@ var fs = Npm.require('fs'); var path = Npm.require('path'); var less = Npm.require('less'); +var Future = Npm.require('fibers/future'); Plugin.registerSourceHandler("less", function (compileStep) { + // XXX annoying that this is replicated in .css, .less, and .styl + if (! compileStep.archMatches('browser')) { + // XXX in the future, might be better to emit some kind of a + // warning if a stylesheet is included on the server, rather than + // silently ignoring it. but that would mean you can't stick .css + // at the top level of your app, which is kind of silly. + return; + } + var source = compileStep.read().toString('utf8'); var options = { // Use fs.readFileSync to process @imports. This is the bundler, so @@ -13,24 +23,28 @@ Plugin.registerSourceHandler("less", function (compileStep) { paths: [path.dirname(compileStep._fullInputPath)] // for @import }; + var f = new Future; + var css; try { - less.render(source, options, function (err, css) { - if (err) { - // XXX better error handling, once the Plugin interface support it - throw new Error(err.message); - } - - compileStep.addStylesheet({ - path: compileStep.inputPath + ".css", - data: css - }); - }); + less.render(source, options, f.resolver()); + css = f.wait(); } catch (e) { // less.render() is supposed to report any errors via its // callback. But sometimes, it throws them instead. This is // probably a bug in less. Be prepared for either behavior. - throw new Error(compileStep.inputPath + ": Less compiler error: " + e.message); + compileStep.error({ + message: "Less compiler error: " + e.message, + sourcePath: e.filename || compileStep.inputPath, + line: e.line - 1, // dunno why, but it matches + column: e.column + 1 + }); + return; } + + compileStep.addStylesheet({ + path: compileStep.inputPath + ".css", + data: css + }); });; // Register lessimport files with the dependency watcher, without actually diff --git a/packages/livedata/livedata_connection_tests.js b/packages/livedata/livedata_connection_tests.js index 72d56d65cb..119a78792d 100644 --- a/packages/livedata/livedata_connection_tests.js +++ b/packages/livedata/livedata_connection_tests.js @@ -1287,18 +1287,17 @@ Tinytest.add("livedata connection - onReconnect prepends messages correctly with var getSelfConnectionUrl = function () { if (Meteor.isClient) { - return "/"; + return Meteor._relativeToSiteRootUrl("/"); } else { return Meteor.absoluteUrl(); } }; if (Meteor.isServer) { - var reversed = {}; Meteor.methods({ reverse: function (arg) { - reversed[arg] = true; - return arg.split("").reverse().join(""); + // Return something notably different from reverse.meteor.com. + return arg.split("").reverse().join("") + " LOCAL"; } }); } @@ -1325,22 +1324,10 @@ testAsyncMulti("livedata connection - reconnect to a different server", [ if (self.doTest) { self.conn.reconnect({url: getSelfConnectionUrl()}); self.conn.call("reverse", "bar", expect(function (err, res) { - test.equal(res, "rab"); - })); - } - }, - function (test, expect) { - var self = this; - var id = Random.id(); - if (self.doTest) { - self.conn.call("reverse", id, expect(function (err, res) { - if (Meteor.isServer) { - test.isTrue(reversed[id]); - } + test.equal(res, "rab LOCAL"); })); } } - ]); Tinytest.addAsync("livedata connection - version negotiation requires renegotiating", diff --git a/packages/livedata/livedata_tests.js b/packages/livedata/livedata_tests.js index 7ea0f656e8..3fd1cd0c87 100644 --- a/packages/livedata/livedata_tests.js +++ b/packages/livedata/livedata_tests.js @@ -600,6 +600,8 @@ if (Meteor.isClient) { ]); } +var selfUrl = Meteor.isServer + ? Meteor.absoluteUrl() : Meteor._relativeToSiteRootUrl('/'); if (Meteor.isServer) { Meteor.methods({ @@ -613,7 +615,7 @@ if (Meteor.isServer) { testAsyncMulti("livedata - connect works from both client and server", [ function (test, expect) { var self = this; - self.conn = DDP.connect(Meteor.absoluteUrl()); + self.conn = DDP.connect(selfUrl); pollUntil(expect, function () { return self.conn.status().connected; }, 10000); @@ -637,7 +639,7 @@ if (Meteor.isServer) { testAsyncMulti("livedata - method call on server blocks in a fiber way", [ function (test, expect) { var self = this; - self.conn = DDP.connect(Meteor.absoluteUrl()); + self.conn = DDP.connect(selfUrl); pollUntil(expect, function () { return self.conn.status().connected; }, 10000); diff --git a/packages/livedata/sockjs-0.3.4.js b/packages/livedata/sockjs-0.3.4.js index a19b0d29d1..a3a2f687eb 100644 --- a/packages/livedata/sockjs-0.3.4.js +++ b/packages/livedata/sockjs-0.3.4.js @@ -23,9 +23,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +// Commented out JSO implementation (use json package instead). // JSON2 by Douglas Crockford (minified). -var JSON;JSON||(JSON={}),function(){function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c // [*] Including lib/index.js // Public object diff --git a/packages/livedata/stream_client_sockjs.js b/packages/livedata/stream_client_sockjs.js index d8c4bfb810..8605e6275c 100644 --- a/packages/livedata/stream_client_sockjs.js +++ b/packages/livedata/stream_client_sockjs.js @@ -101,7 +101,7 @@ _.extend(LivedataTest.ClientStream.prototype, { self._clearConnectionAndHeartbeatTimers(); if (self.socket) { self.socket.onmessage = self.socket.onclose - = self.socket.onerror = function () {}; + = self.socket.onerror = self.socket.onheartbeat = function () {}; self.socket.close(); self.socket = null; } diff --git a/packages/logging/logging.js b/packages/logging/logging.js index e33361c11d..7c742f31e4 100644 --- a/packages/logging/logging.js +++ b/packages/logging/logging.js @@ -247,7 +247,7 @@ Log.format = function (obj, options) { '-', timeStamp, utcOffsetStr, - timeInexact ? '?' : ' ', + timeInexact ? '? ' : ' ', appInfo, sourceInfo, stderrIndicator].join(''); diff --git a/packages/logging/logging_test.js b/packages/logging/logging_test.js index 4305abf7e0..9fa50f0099 100644 --- a/packages/logging/logging_test.js +++ b/packages/logging/logging_test.js @@ -51,7 +51,7 @@ Tinytest.add("logging - log", function (test) { [0, "0", "falsy - 0"], [null, "null", "falsy - null"], [undefined, "undefined", "falsy - undefined"], - ["2013-06-13T01:15:16.000Z", new Date("2013-06-13T01:15:16.000Z"), "date"], + [new Date("2013-06-13T01:15:16.000Z"), new Date("2013-06-13T01:15:16.000Z"), "date"], [/[^regexp]{0,1}/g, "/[^regexp]{0,1}/g", "regexp"], [true, "true", "boolean - true"], [false, "false", "boolean - false"], @@ -72,7 +72,11 @@ Tinytest.add("logging - log", function (test) { var recieved = intercepted[index]; var obj = EJSON.parse(recieved); - if (_.isDate(expected)) + // IE8 doesn't support this date format. Skip it. + if (expected && expected.toString && expected.toString() === "NaN") + return; + + if (_.isDate(testcase[0])) obj.message = new Date(obj.message); test.equal(obj.message, expected, 'Logging ' + testName); }); @@ -137,7 +141,7 @@ Tinytest.add("logging - format", function (test) { test.equal( Log.format({message: "message", time: time, timeInexact: true, level: level}), - level.charAt(0).toUpperCase() + "20120908-07:06:05.004" + utcOffsetStr + "?message"); + level.charAt(0).toUpperCase() + "20120908-07:06:05.004" + utcOffsetStr + "? message"); test.equal( Log.format({foo1: "bar1", foo2: "bar2", time: time, level: level}), diff --git a/packages/meteor/plugin/basic-file-types.js b/packages/meteor/plugin/basic-file-types.js index 14141699a3..3c87328198 100644 --- a/packages/meteor/plugin/basic-file-types.js +++ b/packages/meteor/plugin/basic-file-types.js @@ -3,11 +3,12 @@ source file. */ Plugin.registerSourceHandler("css", function (compileStep) { - // XXX use archinfo rather than rolling our own - if (! compileStep.arch.match(/^browser(\.|$)/)) { + // XXX annoying that this is replicated in .css, .less, and .styl + if (! compileStep.archMatches('browser')) { // XXX in the future, might be better to emit some kind of a // warning if a stylesheet is included on the server, rather than - // silently ignoring it + // silently ignoring it. but that would mean you can't stick .css + // at the top level of your app, which is kind of silly. return; } diff --git a/packages/stylus/package.js b/packages/stylus/package.js index 95e74bac9f..850d7cd49a 100644 --- a/packages/stylus/package.js +++ b/packages/stylus/package.js @@ -12,7 +12,7 @@ Package._transitional_registerBuildPlugin({ }); Package.on_test(function (api) { - api.use(['tinytest', 'stylus', 'test-helpers']) + api.use(['tinytest', 'stylus', 'test-helpers']); api.use('spark'); api.add_files(['stylus_tests.styl', 'stylus_tests.js'], 'client'); }); diff --git a/packages/stylus/plugin/compile-stylus.js b/packages/stylus/plugin/compile-stylus.js index 74b786d81d..7ddc33dcf5 100644 --- a/packages/stylus/plugin/compile-stylus.js +++ b/packages/stylus/plugin/compile-stylus.js @@ -1,21 +1,34 @@ var fs = Npm.require('fs'); var stylus = Npm.require('stylus'); var nib = Npm.require('nib'); +var Future = Npm.require('fibers/future'); Plugin.registerSourceHandler("styl", function (compileStep) { + // XXX annoying that this is replicated in .css, .less, and .styl + if (! compileStep.archMatches('browser')) { + // XXX in the future, might be better to emit some kind of a + // warning if a stylesheet is included on the server, rather than + // silently ignoring it. but that would mean you can't stick .css + // at the top level of your app, which is kind of silly. + return; + } + + var f = new Future; stylus(compileStep.read().toString('utf8')) .use(nib()) .set('filename', compileStep.inputPath) - .render(function(err, output) { - if (err) { - // XXX better error handling, once the Plugin interface support it - throw new Error('Stylus compiler error: ' + err.message); - } + .render(f.resolver()); - compileStep.addStylesheet({ - path: compileStep.inputPath + ".css", - data: output - }); + try { + var css = f.wait(); + } catch (e) { + compileStep.error({ + message: "Stylus compiler error: " + e.message }); + return; } -); + compileStep.addStylesheet({ + path: compileStep.inputPath + ".css", + data: css + }); +}); diff --git a/tools/library.js b/tools/library.js index fc9c039c55..6d174ba215 100644 --- a/tools/library.js +++ b/tools/library.js @@ -110,9 +110,25 @@ _.extend(Library.prototype, { for (var i = 0; i < self.localPackageDirs.length; ++i) { var packageDir = path.join(self.localPackageDirs[i], name); - // XXX or unipackage.json? see also watchLocalPackageDirs - if (fs.existsSync(path.join(packageDir, 'package.js'))) + // A directory is a package if it either contains 'package.js' (a package + // source tree) or 'unipackage.json' (a compiled unipackage). (Actually, + // for now, unipackages contain a dummy package.js too.) + // + // XXX support for putting unipackages in a local package dir is + // incomplete! They will be properly loaded, but other packages that + // depend on them have no way of knowing when they change! unipackages + // that are the .build of a source tree work fine (they have a + // buildinfo.json and can be rebuilt), and warehouse unipackages work fine + // too (users are not supposed to edit them (they are read-only on disk), + // and their pathname specifies a version). But if you, eg, have a + // unipackage of coffeescript in a local package directory, build another + // package dependending on it, and substitute another version of the + // unipackage in the same location, nothing will ever rebuild your + // package! + if (fs.existsSync(path.join(packageDir, 'package.js')) || + fs.existsSync(path.join(packageDir, 'unipackage.json'))) { return packageDir; + } } // Try the Meteor distribution, if we have one. @@ -120,6 +136,8 @@ _.extend(Library.prototype, { if (version) { packageDir = path.join(warehouse.getWarehouseDir(), 'packages', name, version); + // The warehouse is theoretically constructed carefully enough that the + // directory really should not exist unless it is complete. if (! fs.existsSync(packageDir)) throw new Error("Package missing from warehouse: " + name + " version " + version); @@ -224,8 +242,15 @@ _.extend(Library.prototype, { if (! buildmessage.jobHasMessages() && // ensure no errors! pkg.canBeSavedAsUnipackage()) { // Save it, for a fast load next time - files.add_to_gitignore(packageDir, '.build*'); - pkg.saveAsUnipackage(buildDir, { buildOfPath: packageDir }); + try { + files.add_to_gitignore(packageDir, '.build*'); + pkg.saveAsUnipackage(buildDir, { buildOfPath: packageDir }); + } catch (e) { + // If we can't write to this directory, we don't get to cache our + // output, but otherwise life is good. + if (!(e && (e.code === 'EACCES' || e.code === 'EPERM'))) + throw e; + } } }); } @@ -272,7 +297,7 @@ _.extend(Library.prototype, { // Register local package directories with a watchSet. We want to know if a // package is created or deleted, which includes both its top-level source - // directory or its package.js file. + // directory and its main package metadata file. watchLocalPackageDirs: function (watchSet) { var self = this; _.each(self.localPackageDirs, function (packageDir) { @@ -283,7 +308,8 @@ _.extend(Library.prototype, { _.each(packages, function (p) { watch.readAndWatchFile(watchSet, path.join(packageDir, p, 'package.js')); - // XXX unipackage.json too? + watch.readAndWatchFile(watchSet, + path.join(packageDir, p, 'unipackage.json')); }); }); }, diff --git a/tools/packages.js b/tools/packages.js index 7ee8dc248a..707efade99 100644 --- a/tools/packages.js +++ b/tools/packages.js @@ -231,6 +231,12 @@ _.extend(Slice.prototype, { var handler = !fileOptions.isAsset && self._getSourceHandler(ext); var contents = watch.readAndWatchFile(self.watchSet, absPath); + if (contents === null) { + buildmessage.error("File not found: " + source.relPath); + // recover by ignoring + return; + } + if (! handler) { // If we don't have an extension handler, serve this file as a // static resource on the client, or ignore it on the server. @@ -359,6 +365,9 @@ _.extend(Slice.prototype, { packageName: self.pkg.name, rootOutputPath: self.pkg.serveRoot, arch: self.arch, + archMatches: function (pattern) { + return archinfo.matches(self.arch, pattern); + }, fileOptions: fileOptions, declaredExports: _.pluck(self.declaredExports, 'name'), read: function (n) { @@ -1792,6 +1801,28 @@ _.extend(Package.prototype, { var otherSliceRegExp = (sliceName === "server" ? /^client\/$/ : /^server\/$/); + // The paths that we've called checkForInfiniteRecursion on. + var seenPaths = {}; + // Used internally by fs.realpathSync as an optimization. + var realpathCache = {}; + var checkForInfiniteRecursion = function (relDir) { + var absPath = path.join(self.sourceRoot, relDir); + try { + var realpath = fs.realpathSync(absPath, realpathCache); + } catch (e) { + if (!e || e.code !== 'ELOOP') + throw e; + // else leave realpath undefined + } + if (realpath === undefined || _.has(seenPaths, realpath)) { + buildmessage.error("Symlink cycle detected at " + relDir); + // recover by returning no files + return true; + } + seenPaths[realpath] = true; + return false; + }; + // Read top-level subdirectories. Ignore subdirectories that have // special handling. var sourceDirectories = readAndWatchDirectory('', { @@ -1800,13 +1831,17 @@ _.extend(Package.prototype, { /^public\/$/, /^private\/$/, otherSliceRegExp].concat(sourceExclude) }); + checkForInfiniteRecursion(''); - // XXX avoid infinite recursion with bad symlinks while (!_.isEmpty(sourceDirectories)) { var dir = sourceDirectories.shift(); + // remove trailing slash dir = dir.substr(0, dir.length - 1); + if (checkForInfiniteRecursion(dir)) + return []; // pretend we found no files + // Find source files in this directory. Array.prototype.push.apply(sources, readAndWatchDirectory(dir, { include: sourceInclude, @@ -1845,7 +1880,6 @@ _.extend(Package.prototype, { include: [new RegExp('^' + assetDir + '/$')] }); - // XXX avoid infinite recursion with bad symlinks if (!_.isEmpty(assetDirs)) { if (!_.isEqual(assetDirs, [assetDir + '/'])) throw new Error("Surprising assetDirs: " + JSON.stringify(assetDirs)); @@ -1855,6 +1889,9 @@ _.extend(Package.prototype, { // remove trailing slash dir = dir.substr(0, dir.length - 1); + if (checkForInfiniteRecursion(dir)) + return []; // pretend we found no files + // Find asset files in this directory. var assetsAndSubdirs = readAndWatchDirectory(dir, { include: [/.?/], diff --git a/tools/run.js b/tools/run.js index 6982cb9f2a..832d68c226 100644 --- a/tools/run.js +++ b/tools/run.js @@ -561,9 +561,10 @@ exports.run = function (context, options) { Status.running = true; - var rootUrl = process.env.ROOT_URL || ('http://localhost:' + outerPort); + var rootUrl = process.env.ROOT_URL || + ('http://localhost:' + outerPort + '/'); if (firstRun) { - process.stdout.write("=> Meteor server running on: " + rootUrl + "/\n"); + process.stdout.write("=> Meteor server running on: " + rootUrl + "\n"); firstRun = false; lastThingThatPrintedWasRestartMessage = false; } else { diff --git a/tools/watch.js b/tools/watch.js index d4b05bdb37..ea685773cb 100644 --- a/tools/watch.js +++ b/tools/watch.js @@ -228,9 +228,12 @@ var readDirectory = function (options) { // XXX Does the treatment of symlinks make sense? var stats = fs.statSync(path.join(options.absPath, entry)); } catch (e) { - // Disappeared after the readdirSync (or a dangling symlink)? Eh, pretend - // it was never there in the first place. - return; + if (e && (e.code === 'ENOENT')) { + // Disappeared after the readdirSync (or a dangling symlink)? Eh, + // pretend it was never there in the first place. + return; + } + throw e; } // XXX if we're on windows, I guess it's possible for files to end with '/'. if (stats.isDirectory())