mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
858 lines
30 KiB
JavaScript
858 lines
30 KiB
JavaScript
// Bundle contents:
|
|
// main.js [run to start the server]
|
|
// /static [served by node for now]
|
|
// /static_cacheable [cache-forever files, served by node for now]
|
|
// /server [XXX split out into a package]
|
|
// server.js, .... [contents of tools/server]
|
|
// node_modules [for now, contents of (dev_bundle)/lib/node_modules]
|
|
// /app.html
|
|
// /app [user code]
|
|
// /app.json: [data for server.js]
|
|
// - load [list of files to load, relative to root, presumably under /app]
|
|
// - manifest [list of resources in load order, each consists of an object]:
|
|
// {
|
|
// "path": relative path of file in the bundle, normalized to use forward slashes
|
|
// "where": "client", "internal" [could also be "server" in future]
|
|
// "type": "js", "css", or "static"
|
|
// "cacheable": (client) boolean, is it safe to ask the browser to cache this file
|
|
// "url": (client) relative url to download the resource, includes cache
|
|
// busting param if used
|
|
// "size": size in bytes
|
|
// "hash": sha1 hash of the contents
|
|
// }
|
|
// /dependencies.json: files to monitor for changes in development mode
|
|
// - extensions [list of extensions registered for user code, with dots]
|
|
// - packages [map from package name to list of paths relative to the package]
|
|
// - core [paths relative to 'app' in meteor tree]
|
|
// - app [paths relative to top of app tree]
|
|
// - exclude [list of regexps for files to ignore (everywhere)]
|
|
// (for 'core' and 'apps', if a directory is given, you should
|
|
// monitor everything in the subtree under it minus the stuff that
|
|
// matches exclude, and if it doesn't exist yet, you should watch for
|
|
// it to appear)
|
|
//
|
|
// The application launcher is expected to execute /main.js with node, setting
|
|
// various environment variables (such as PORT and MONGO_URL). The enclosed node
|
|
// application is expected to do the rest, including serving /static.
|
|
|
|
var path = require('path');
|
|
var files = require(path.join(__dirname, 'files.js'));
|
|
var packages = require(path.join(__dirname, 'packages.js'));
|
|
var warehouse = require(path.join(__dirname, 'warehouse.js'));
|
|
var crypto = require('crypto');
|
|
var fs = require('fs');
|
|
var uglify = require('uglify-js');
|
|
var cleanCSS = require('clean-css');
|
|
var _ = require('underscore');
|
|
var project = require(path.join(__dirname, 'project.js'));
|
|
|
|
// files to ignore when bundling. node has no globs, so use regexps
|
|
var ignore_files = [
|
|
/~$/, /^\.#/, /^#.*#$/,
|
|
/^\.DS_Store$/, /^ehthumbs\.db$/, /^Icon.$/, /^Thumbs\.db$/,
|
|
/^\.meteor$/, /* avoids scanning N^2 files when bundling all packages */
|
|
/^\.git$/ /* often has too many files to watch */
|
|
];
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// PackageBundlingInfo
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Represents the occurrence of a package in a bundle. Includes data
|
|
// relevant to the process of bundling this package, distinct from the
|
|
// package data itself.
|
|
var PackageBundlingInfo = function (pkg, bundle) {
|
|
var self = this;
|
|
self.pkg = pkg;
|
|
self.bundle = bundle;
|
|
|
|
// list of places we've already been used. map from a 'canonicalized
|
|
// where' to true. 'canonicalized where' is the JSONification of a
|
|
// sorted array with zero or more elements drawn from the set
|
|
// 'client', 'server', with each element unique
|
|
// XXX this is a mess, refactor
|
|
self.where = {};
|
|
|
|
// other packages we've used (with any 'where') -- map from id to package
|
|
self.using = {};
|
|
|
|
// map from where (client, server) to a source file name (relative
|
|
// to the package) to true
|
|
self.files = {client: {}, server: {}};
|
|
|
|
// files we depend on -- map from rel_path to true
|
|
self.dependencies = {};
|
|
if (pkg.name)
|
|
self.dependencies['package.js'] = true;
|
|
|
|
// Set if we've installed NPM modules on this package during this
|
|
// bundling. Used to ensure that we only refresh NPM modules once per package
|
|
// per bundling run.
|
|
self.installedNpmModules = false;
|
|
|
|
// the API available from on_use / on_test handlers
|
|
self.api = {
|
|
// Called when this package wants to make another package be
|
|
// used. Can also take literal package objects, if you have
|
|
// anonymous packages you want to use (eg, app packages)
|
|
use: function (names, where) {
|
|
if (!(names instanceof Array))
|
|
names = names ? [names] : [];
|
|
|
|
_.each(names, function (name) {
|
|
var pkg = packages.get(name, self.bundle.packageSearchOptions);
|
|
if (!pkg)
|
|
throw new Error("Package not found: " + name);
|
|
self.bundle.use(pkg, where, self);
|
|
});
|
|
},
|
|
|
|
add_files: function (paths, where) {
|
|
if (!(paths instanceof Array))
|
|
paths = paths ? [paths] : [];
|
|
if (!(where instanceof Array))
|
|
where = where ? [where] : [];
|
|
|
|
_.each(where, function (w) {
|
|
_.each(paths, function (rel_path) {
|
|
self.add_file(rel_path, w);
|
|
});
|
|
});
|
|
},
|
|
|
|
// Return a list of all of the extension that indicate source files
|
|
// inside this package, INCLUDING leading dots.
|
|
registered_extensions: function () {
|
|
var ret = _.keys(self.pkg.extensions);
|
|
|
|
for (var id in self.using) {
|
|
var other_inst = self.using[id];
|
|
ret = _.union(ret, _.keys(other_inst.pkg.extensions));
|
|
}
|
|
|
|
return _.map(ret, function (x) {return "." + x;});
|
|
},
|
|
|
|
// Report an error. It should be a single human-readable
|
|
// string. If any errors are reported, the bundling is considered
|
|
// to have failed.
|
|
error: function (message) {
|
|
self.bundle.errors.push(message);
|
|
}
|
|
};
|
|
|
|
if (pkg.name !== "meteor")
|
|
self.api.use("meteor");
|
|
};
|
|
|
|
_.extend(PackageBundlingInfo.prototype, {
|
|
// Find the function that should be used to handle a source file
|
|
// found in this package. We'll use handlers that are defined in
|
|
// this package and in its immediate dependencies. ('extension'
|
|
// should be the extension of the file without a leading dot.)
|
|
get_source_handler: function (extension) {
|
|
var self = this;
|
|
var candidates = [];
|
|
|
|
if (extension in self.pkg.extensions)
|
|
candidates.push(self.pkg.extensions[extension]);
|
|
|
|
for (var id in self.using) {
|
|
var other_inst = self.using[id];
|
|
var other_pkg = other_inst.pkg;
|
|
if (extension in other_pkg.extensions)
|
|
candidates.push(other_pkg.extensions[extension]);
|
|
}
|
|
|
|
// XXX do something more graceful than printing a stack trace and
|
|
// exiting!! we have higher standards than that!
|
|
|
|
if (!candidates.length)
|
|
return null;
|
|
|
|
if (candidates.length > 1)
|
|
// XXX improve error message (eg, name the packages involved)
|
|
// and make it clear that it's not a global conflict, but just
|
|
// among this package's dependencies
|
|
throw new Error("Conflict: two packages are both trying " +
|
|
"to handle ." + extension);
|
|
|
|
return candidates[0];
|
|
},
|
|
|
|
add_file: function (rel_path, where) {
|
|
var self = this;
|
|
|
|
if (self.files[where][rel_path])
|
|
return;
|
|
self.files[where][rel_path] = true;
|
|
|
|
var ext = files.findExtension(self.api.registered_extensions(), rel_path);
|
|
// substr to remove the dot to translate between the with-dot world
|
|
// of registered_extensions and the without dot world of
|
|
// get_source_handler. This could use some API beautification.
|
|
var handler = ext && self.get_source_handler(ext.substr(1));
|
|
if (handler) {
|
|
handler(self.bundle.api,
|
|
path.join(self.pkg.source_root, rel_path),
|
|
path.join(self.pkg.serve_root, rel_path),
|
|
where);
|
|
} else {
|
|
// If we don't have an extension handler, serve this file
|
|
// as a static resource.
|
|
self.bundle.api.add_resource({
|
|
type: "static",
|
|
path: path.join(self.pkg.serve_root, rel_path),
|
|
data: fs.readFileSync(path.join(self.pkg.source_root, rel_path)),
|
|
where: where
|
|
});
|
|
}
|
|
|
|
// Reload runner when this file changes.
|
|
self.dependencies[rel_path] = true;
|
|
}
|
|
});
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Bundle
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
var Bundle = function () {
|
|
var self = this;
|
|
|
|
// Packages being used. Map from a package id to a PackageBundlingInfo.
|
|
self.packageBundlingInfo = {};
|
|
|
|
// Packages that have had tests included. Map from package id to instance
|
|
self.tests_included = {};
|
|
|
|
// meteor release stamp
|
|
self.releaseStamp = null;
|
|
|
|
// see packages.js
|
|
self.packageSearchOptions = {};
|
|
|
|
// map from environment, to list of filenames
|
|
self.js = {client: [], server: []};
|
|
|
|
// list of filenames
|
|
self.css = [];
|
|
|
|
// images and other static files added from packages
|
|
// map from environment, to list of filenames
|
|
self.static = {client: [], server: []};
|
|
|
|
// Map from environment, to path name (server relative), to contents
|
|
// of file as buffer.
|
|
self.files = {client: {}, client_cacheable: {}, server: {}};
|
|
|
|
// See description of the manifest at the top.
|
|
// Note that in contrast to self.js etc., the manifest only includes
|
|
// files which are in the final bundler output: for example, if code
|
|
// is minified, the manifest includes the minify output file but not
|
|
// the individual input files that were combined.
|
|
self.manifest = [];
|
|
|
|
// these directories are copied (cp -r) or symlinked into the
|
|
// bundle. maps target path (server relative) to source directory on
|
|
// disk
|
|
self.nodeModulesDirs = {};
|
|
|
|
// list of segments of additional HTML for <head>/<body>
|
|
self.head = [];
|
|
self.body = [];
|
|
|
|
// list of errors encountered while bundling. array of string.
|
|
self.errors = [];
|
|
|
|
// the API available from register_extension handlers
|
|
self.api = {
|
|
/**
|
|
* This is the ultimate low-level API to add data to the bundle.
|
|
*
|
|
* type: "js", "css", "head", "body", "static"
|
|
*
|
|
* where: an environment, or a list of one or more environments
|
|
* ("client", "server", "tests") -- for non-JS resources, the only
|
|
* legal environment is "client"
|
|
*
|
|
* path: the (absolute) path at which the file will be
|
|
* served. ignored in the case of "head" and "body".
|
|
*
|
|
* source_file: the absolute path to read the data from. if path
|
|
* is set, will default based on that. overridden by data.
|
|
*
|
|
* data: the data to send. overrides source_file if present. you
|
|
* must still set path (except for "head" and "body".)
|
|
*/
|
|
add_resource: function (options) {
|
|
var source_file = options.source_file || options.path;
|
|
|
|
var data;
|
|
if (options.data) {
|
|
data = options.data;
|
|
if (!(data instanceof Buffer)) {
|
|
if (!(typeof data === "string"))
|
|
throw new Error("Bad type for data");
|
|
data = new Buffer(data, 'utf8');
|
|
}
|
|
} else {
|
|
if (!source_file)
|
|
throw new Error("Need either source_file or data");
|
|
data = fs.readFileSync(source_file);
|
|
}
|
|
|
|
var where = options.where;
|
|
if (typeof where === "string")
|
|
where = [where];
|
|
if (!where)
|
|
throw new Error("Must specify where");
|
|
|
|
_.each(where, function (w) {
|
|
if (options.type === "js") {
|
|
if (!options.path)
|
|
throw new Error("Must specify path");
|
|
|
|
if (w === "client" || w === "server") {
|
|
var wrapped = data;
|
|
// On the client, wrap each file in a closure, to give it a separate
|
|
// scope (eg, file-level vars are file-scoped). On the server, this
|
|
// is done in server/server.js to inject the Npm symbol.
|
|
//
|
|
// The ".call(this)" allows you to do a top-level "this.foo = " to
|
|
// define global variables; this is the only way to do it in
|
|
// CoffeeScript.
|
|
if (w === "client") {
|
|
wrapped = Buffer.concat([
|
|
new Buffer("(function(){"),
|
|
data,
|
|
new Buffer("\n}).call(this);\n")]);
|
|
}
|
|
self.files[w][options.path] = wrapped;
|
|
self.js[w].push(options.path);
|
|
} else {
|
|
throw new Error("Invalid environment");
|
|
}
|
|
} else if (options.type === "css") {
|
|
if (w !== "client")
|
|
// XXX might be nice to throw an error here, but then we'd
|
|
// have to make it so that packages.js ignores css files
|
|
// that appear in the server directories in an app tree
|
|
return;
|
|
if (!options.path)
|
|
throw new Error("Must specify path");
|
|
self.files.client[options.path] = data;
|
|
self.css.push(options.path);
|
|
} else if (options.type === "head" || options.type === "body") {
|
|
if (w !== "client")
|
|
throw new Error("HTML segments can only go to the client");
|
|
self[options.type].push(data);
|
|
} else if (options.type === "static") {
|
|
self.files[w][options.path] = data;
|
|
self.static[w].push(options.path);
|
|
} else {
|
|
throw new Error("Unknown type " + options.type);
|
|
}
|
|
});
|
|
},
|
|
|
|
// Report an error. It should be a single human-readable
|
|
// string. If any errors are reported, the bundling is considered
|
|
// to have failed.
|
|
error: function (message) {
|
|
self.errors.push(message);
|
|
}
|
|
};
|
|
};
|
|
|
|
_.extend(Bundle.prototype, {
|
|
_get_bundling_info_for_package: function (pkg) {
|
|
var self = this;
|
|
|
|
var bundlingInfo = self.packageBundlingInfo[pkg.id];
|
|
if (!bundlingInfo) {
|
|
bundlingInfo = new PackageBundlingInfo(pkg, self);
|
|
self.packageBundlingInfo[pkg.id] = bundlingInfo;
|
|
}
|
|
|
|
return bundlingInfo;
|
|
},
|
|
|
|
_hash: function (contents) {
|
|
var hash = crypto.createHash('sha1');
|
|
hash.update(contents);
|
|
return hash.digest('hex');
|
|
},
|
|
|
|
_maybeUpdateNpmDependencies: function (pkg, inst) {
|
|
var self = this;
|
|
if (pkg.npmDependencies) {
|
|
// If the package isn't in the warehouse, maybe update the NPM
|
|
// dependencies. (Warehouse packages shouldn't change after they're
|
|
// installed, so we skip this slow step.) Also, we only do this once per
|
|
// package per bundling run.
|
|
if (!pkg.inWarehouse && !inst.installedNpmModules) {
|
|
pkg.installNpmDependencies();
|
|
inst.installedNpmModules = true;
|
|
}
|
|
self.bundleNodeModules(pkg);
|
|
}
|
|
},
|
|
|
|
// Call to add a package to this bundle
|
|
// if 'where' is given, it's an array of "client" and/or "server"
|
|
// if 'from' is given, it's the PackageBundlingInfo that's doing the
|
|
// using, or it can be undefined for top level
|
|
use: function (pkg, where, from) {
|
|
var self = this;
|
|
var inst = self._get_bundling_info_for_package(pkg);
|
|
|
|
if (from)
|
|
from.using[pkg.id] = inst;
|
|
|
|
// get 'canonicalized where'
|
|
var canon_where = where;
|
|
if (!canon_where)
|
|
canon_where = [];
|
|
if (!(canon_where instanceof Array))
|
|
canon_where = [canon_where];
|
|
else
|
|
canon_where = _.clone(canon_where);
|
|
canon_where.sort();
|
|
canon_where = JSON.stringify(canon_where);
|
|
|
|
if (inst.where[canon_where])
|
|
return; // already used in this environment
|
|
inst.where[canon_where] = true;
|
|
|
|
// XXX detect circular dependencies and print an error. (not sure
|
|
// what the current code will do)
|
|
|
|
self._maybeUpdateNpmDependencies(pkg, inst);
|
|
|
|
if (pkg.on_use_handler)
|
|
pkg.on_use_handler(inst.api, where);
|
|
},
|
|
|
|
includeTests: function (packageOrPackageName) {
|
|
var self = this;
|
|
// 'packages.get' is a noop if 'packageOrPackageName' is a Package object.
|
|
var pkg = packages.get(packageOrPackageName, self.packageSearchOptions);
|
|
if (!pkg) {
|
|
console.error("Can't find package " + packageOrPackageName);
|
|
process.exit(1);
|
|
}
|
|
if (self.tests_included[pkg.id])
|
|
return;
|
|
self.tests_included[pkg.id] = true;
|
|
|
|
var inst = self._get_bundling_info_for_package(pkg);
|
|
|
|
// XXX we might want to support npm modules that are only used in
|
|
// tests. one example is stream-buffers as used in the email
|
|
// package
|
|
self._maybeUpdateNpmDependencies(pkg, inst);
|
|
|
|
if (inst.pkg.on_test_handler)
|
|
inst.pkg.on_test_handler(inst.api);
|
|
},
|
|
|
|
// map a package's generated node_modules directory to the package
|
|
// directory within the bundle
|
|
bundleNodeModules: function (pkg) {
|
|
var nodeModulesPath = path.join(pkg.npmDir(), 'node_modules');
|
|
// use '/' rather than path.join since this is part of a url
|
|
var relNodeModulesPath = ['packages', pkg.name, 'node_modules'].join('/');
|
|
this.nodeModulesDirs[relNodeModulesPath] = nodeModulesPath;
|
|
},
|
|
|
|
// Minify the bundle
|
|
minify: function () {
|
|
var self = this;
|
|
|
|
var addFile = function (type, finalCode) {
|
|
var contents = new Buffer(finalCode);
|
|
var hash = self._hash(contents);
|
|
var name = '/' + hash + '.' + type;
|
|
self.files.client_cacheable[name] = contents;
|
|
self.manifest.push({
|
|
path: 'static_cacheable' + name,
|
|
where: 'client',
|
|
type: type,
|
|
cacheable: true,
|
|
url: name,
|
|
size: contents.length,
|
|
hash: hash
|
|
});
|
|
};
|
|
|
|
/// Javascript
|
|
var codeParts = [];
|
|
_.each(self.js.client, function (js_path) {
|
|
codeParts.push(self.files.client[js_path].toString('utf8'));
|
|
|
|
delete self.files.client[js_path];
|
|
});
|
|
self.js.client = [];
|
|
|
|
var combinedCode = codeParts.join('\n;\n');
|
|
var finalCode = uglify.minify(
|
|
combinedCode, {fromString: true, compress: {drop_debugger: false}}).code;
|
|
|
|
addFile('js', finalCode);
|
|
|
|
/// CSS
|
|
var css_concat = "";
|
|
_.each(self.css, function (css_path) {
|
|
var css_data = self.files.client[css_path];
|
|
css_concat = css_concat + "\n" + css_data.toString('utf8');
|
|
|
|
delete self.files.client[css_path];
|
|
});
|
|
self.css = [];
|
|
|
|
var final_css = cleanCSS.process(css_concat);
|
|
|
|
addFile('css', final_css);
|
|
},
|
|
|
|
_clientUrlsFor: function (type) {
|
|
var self = this;
|
|
return _.pluck(
|
|
_.filter(self.manifest, function (resource) {
|
|
return resource.where === 'client' && resource.type === type;
|
|
}),
|
|
'url'
|
|
);
|
|
},
|
|
|
|
_generate_app_html: function () {
|
|
var self = this;
|
|
|
|
var template = fs.readFileSync(path.join(__dirname, "app.html.in"));
|
|
var f = require('handlebars').compile(template.toString());
|
|
return f({
|
|
scripts: self._clientUrlsFor('js'),
|
|
head_extra: self.head.join('\n'),
|
|
body_extra: self.body.join('\n'),
|
|
stylesheets: self._clientUrlsFor('css')
|
|
});
|
|
},
|
|
|
|
// The extensions registered by the application package, if
|
|
// any. Kind of a hack.
|
|
_app_extensions: function () {
|
|
var self = this;
|
|
var exts = {};
|
|
|
|
for (var id in self.packageBundlingInfo) {
|
|
var inst = self.packageBundlingInfo[id];
|
|
if (!inst.name)
|
|
_.each(inst.api.registered_extensions(), function (ext) {
|
|
exts[ext] = true;
|
|
});
|
|
}
|
|
|
|
return _.keys(exts);
|
|
},
|
|
|
|
// nodeModulesMode should be "skip", "symlink", or "copy"
|
|
write_to_directory: function (output_path, project_dir, nodeModulesMode) {
|
|
var self = this;
|
|
var app_json = {};
|
|
var dependencies_json = {core: [], app: [], packages: {}};
|
|
var is_app = files.is_app_dir(project_dir);
|
|
|
|
if (is_app) {
|
|
dependencies_json.app.push(path.join('.meteor', 'packages'));
|
|
dependencies_json.app.push(path.join('.meteor', 'release'));
|
|
}
|
|
|
|
// --- Set up build area ---
|
|
|
|
// foo/bar => foo/.build.bar
|
|
var build_path = path.join(path.dirname(output_path),
|
|
'.build.' + path.basename(output_path));
|
|
|
|
// XXX cleaner error handling. don't make the humans read an
|
|
// exception (and, make suitable for use in automated systems)
|
|
files.rm_recursive(build_path);
|
|
files.mkdir_p(build_path, 0755);
|
|
|
|
// --- Core runner code ---
|
|
|
|
files.cp_r(path.join(__dirname, 'server'),
|
|
path.join(build_path, 'server'), {ignore: ignore_files});
|
|
dependencies_json.core.push('server');
|
|
|
|
// --- Third party dependencies ---
|
|
|
|
if (nodeModulesMode === "symlink")
|
|
fs.symlinkSync(path.join(files.get_dev_bundle(), 'lib', 'node_modules'),
|
|
path.join(build_path, 'server', 'node_modules'));
|
|
else if (nodeModulesMode === "copy")
|
|
files.cp_r(path.join(files.get_dev_bundle(), 'lib', 'node_modules'),
|
|
path.join(build_path, 'server', 'node_modules'),
|
|
{ignore: ignore_files});
|
|
else
|
|
/* nodeModulesMode === "skip" */;
|
|
|
|
fs.writeFileSync(
|
|
path.join(build_path, 'server', '.bundle_version.txt'),
|
|
fs.readFileSync(
|
|
path.join(files.get_dev_bundle(), '.bundle_version.txt')));
|
|
|
|
// --- Static assets ---
|
|
|
|
var addClientFileToManifest = function (filepath, contents, type, cacheable, url) {
|
|
if (! contents instanceof Buffer)
|
|
throw new Error('contents must be a Buffer');
|
|
var normalized = filepath.split(path.sep).join('/');
|
|
if (normalized.charAt(0) === '/')
|
|
normalized = normalized.substr(1);
|
|
self.manifest.push({
|
|
// path is normalized to use forward slashes
|
|
path: (cacheable ? 'static_cacheable' : 'static') + '/' + normalized,
|
|
where: 'client',
|
|
type: type,
|
|
cacheable: cacheable,
|
|
url: url || '/' + normalized,
|
|
// contents is a Buffer and so correctly gives us the size in bytes
|
|
size: contents.length,
|
|
hash: self._hash(contents)
|
|
});
|
|
};
|
|
|
|
if (is_app) {
|
|
if (fs.existsSync(path.join(project_dir, 'public'))) {
|
|
var copied =
|
|
files.cp_r(path.join(project_dir, 'public'),
|
|
path.join(build_path, 'static'), {ignore: ignore_files});
|
|
|
|
_.each(copied, function (fs_relative_path) {
|
|
var filepath = path.join(build_path, 'static', fs_relative_path);
|
|
var contents = fs.readFileSync(filepath);
|
|
addClientFileToManifest(fs_relative_path, contents, 'static', false);
|
|
});
|
|
}
|
|
dependencies_json.app.push('public');
|
|
}
|
|
|
|
// Add cache busting query param if needed, and
|
|
// add to manifest.
|
|
var processClientCode = function (type, file) {
|
|
var contents, url;
|
|
if (file in self.files.client_cacheable) {
|
|
contents = self.files.client_cacheable[file];
|
|
url = file;
|
|
}
|
|
else if (file in self.files.client) {
|
|
// Client css and js becomes cacheable with the addition of the
|
|
// cache busting query parameter.
|
|
contents = self.files.client[file];
|
|
delete self.files.client[file];
|
|
self.files.client_cacheable[file] = contents;
|
|
url = file + '?' + self._hash(contents)
|
|
}
|
|
else
|
|
throw new Error('unable to find file: ' + file);
|
|
|
|
addClientFileToManifest(file, contents, type, true, url);
|
|
};
|
|
|
|
_.each(self.js.client, function (file) { processClientCode('js', file); });
|
|
_.each(self.css, function (file) { processClientCode('css', file); });
|
|
|
|
// -- Client code --
|
|
for (var rel_path in self.files.client) {
|
|
var full_path = path.join(build_path, 'static', rel_path);
|
|
files.mkdir_p(path.dirname(full_path), 0755);
|
|
fs.writeFileSync(full_path, self.files.client[rel_path]);
|
|
addClientFileToManifest(rel_path, self.files.client[rel_path], 'static', false);
|
|
}
|
|
|
|
// -- Client cache forever code --
|
|
for (var rel_path in self.files.client_cacheable) {
|
|
var full_path = path.join(build_path, 'static_cacheable', rel_path);
|
|
files.mkdir_p(path.dirname(full_path), 0755);
|
|
fs.writeFileSync(full_path, self.files.client_cacheable[rel_path]);
|
|
}
|
|
|
|
app_json.load = [];
|
|
files.mkdir_p(path.join(build_path, 'app'), 0755);
|
|
for (var rel_path in self.files.server) {
|
|
var path_in_bundle = path.join('app', rel_path);
|
|
var full_path = path.join(build_path, path_in_bundle);
|
|
files.mkdir_p(path.dirname(full_path), 0755);
|
|
fs.writeFileSync(full_path, self.files.server[rel_path]);
|
|
app_json.load.push(path_in_bundle);
|
|
}
|
|
|
|
// `node_modules` directories for packages
|
|
for (var rel_path in self.nodeModulesDirs) {
|
|
var path_in_bundle = path.join('app', rel_path);
|
|
var full_path = path.join(build_path, path_in_bundle);
|
|
|
|
// XXX it's bizarre that we would be trying to install npm
|
|
// modules into a non-existant path, but this happens when we
|
|
// have an npm dependency only used during bundle time (such as
|
|
// the less package). we should consider supporting bundle
|
|
// time-only npm dependencies.
|
|
if (fs.existsSync(path.dirname(full_path))) {
|
|
if (nodeModulesMode === 'symlink') {
|
|
// if we symlink the dev_bundle, also symlink individual package
|
|
// node_modules.
|
|
fs.symlinkSync(self.nodeModulesDirs[rel_path], full_path);
|
|
} else {
|
|
// otherwise, copy them. if we're skipping the dev_bundle
|
|
// modules (eg for deploy) we still need the per-package
|
|
// modules.
|
|
files.cp_r(self.nodeModulesDirs[rel_path], full_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
var app_html = self._generate_app_html();
|
|
fs.writeFileSync(path.join(build_path, 'app.html'), app_html);
|
|
self.manifest.push({
|
|
path: 'app.html',
|
|
where: 'internal',
|
|
hash: self._hash(app_html)
|
|
});
|
|
dependencies_json.core.push(path.join('tools', 'app.html.in'));
|
|
|
|
// --- Documentation, and running from the command line ---
|
|
|
|
fs.writeFileSync(path.join(build_path, 'main.js'),
|
|
"require('./server/server.js');\n");
|
|
|
|
fs.writeFileSync(path.join(build_path, 'README'),
|
|
"This is a Meteor application bundle. It has only one dependency,\n" +
|
|
"node.js (with the 'fibers' package). To run the application:\n" +
|
|
"\n" +
|
|
" $ npm install fibers@1.0.0\n" +
|
|
" $ export MONGO_URL='mongodb://user:password@host:port/databasename'\n" +
|
|
" $ export ROOT_URL='http://example.com'\n" +
|
|
" $ export MAIL_URL='smtp://user:password@mailhost:port/'\n" +
|
|
" $ node main.js\n" +
|
|
"\n" +
|
|
"Use the PORT environment variable to set the port where the\n" +
|
|
"application will listen. The default is 80, but that will require\n" +
|
|
"root on most systems.\n" +
|
|
"\n" +
|
|
"Find out more about Meteor at meteor.com.\n");
|
|
|
|
// --- Metadata ---
|
|
|
|
app_json.manifest = self.manifest;
|
|
|
|
dependencies_json.extensions = self._app_extensions();
|
|
dependencies_json.exclude = _.pluck(ignore_files, 'source');
|
|
dependencies_json.packages = {};
|
|
for (var id in self.packageBundlingInfo) {
|
|
var packageBundlingInfo = self.packageBundlingInfo[id];
|
|
if (packageBundlingInfo.pkg.name) {
|
|
dependencies_json.packages[packageBundlingInfo.pkg.name] =
|
|
_.keys(packageBundlingInfo.dependencies);
|
|
}
|
|
}
|
|
|
|
if (self.releaseStamp && self.releaseStamp !== 'none')
|
|
app_json.release = self.releaseStamp;
|
|
|
|
fs.writeFileSync(path.join(build_path, 'app.json'),
|
|
JSON.stringify(app_json, null, 2));
|
|
fs.writeFileSync(path.join(build_path, 'dependencies.json'),
|
|
JSON.stringify(dependencies_json));
|
|
|
|
// --- Move into place ---
|
|
|
|
// XXX cleaner error handling (no exceptions)
|
|
files.rm_recursive(output_path);
|
|
fs.renameSync(build_path, output_path);
|
|
}
|
|
|
|
});
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Main
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Take the Meteor app in project_dir, and compile it into a bundle at
|
|
* output_path. output_path will be created if it doesn't exist (it
|
|
* will be a directory), and removed if it does exist. The release
|
|
* version is *not* read from the app's .meteor/release file. Instead,
|
|
* it must be passed in as an option.
|
|
*
|
|
* Returns undefined on success. On failure, returns an array of
|
|
* strings, the error messages. On failure, a bundle will still be
|
|
* written to output_path. It is probably broken, but it is supposed
|
|
* to contain correct dependency information, so you can tell when to
|
|
* try bundling again.
|
|
*
|
|
* options include:
|
|
* - minify : minify the CSS and JS assets
|
|
*
|
|
* - nodeModulesMode : decide on how to create the bundle's
|
|
* node_modules directory. one of:
|
|
* 'skip' : don't create node_modules. used by `meteor deploy`, since
|
|
* our production servers already have all of the node modules
|
|
* 'copy' : copy from a prebuilt local installation. used by
|
|
* `meteor bundle`
|
|
* 'symlink' : symlink from a prebuild local installation. used
|
|
* by `meteor run`
|
|
*
|
|
* - testPackages : array of package objects or package names whose
|
|
* tests should be included in this bundle
|
|
*
|
|
* - releaseStamp : The Meteor release version to use. This is *ONLY*
|
|
* used as a stamp (eg Meteor.release). The package
|
|
* search path is configured with packageSearchOptions.
|
|
*
|
|
* - packageSearchOptions: see packages.js. NOTE: if there's an appDir here,
|
|
* it's used for package searching but it is NOT the appDir that we bundle!
|
|
* So for "meteor test-packages" in an app, appDir is the test-runner-app but
|
|
* packageSearchOptions.appDir is the app the user is in.
|
|
*/
|
|
exports.bundle = function (app_dir, output_path, options) {
|
|
if (!options)
|
|
throw new Error("Must pass options");
|
|
if (!options.nodeModulesMode)
|
|
throw new Error("Must pass options.nodeModulesMode");
|
|
if (!options.releaseStamp)
|
|
throw new Error("Must pass options.releaseStamp or 'none'.");
|
|
|
|
try {
|
|
// Create a bundle, add the project
|
|
packages.flush();
|
|
|
|
var bundle = new Bundle;
|
|
bundle.releaseStamp = options.releaseStamp;
|
|
bundle.packageSearchOptions = options.packageSearchOptions || {};
|
|
|
|
// our release manifest is set, let's now load the app
|
|
var app = packages.get_for_app(app_dir, ignore_files);
|
|
bundle.use(app);
|
|
|
|
// Include tests if requested
|
|
if (options.testPackages) {
|
|
_.each(options.testPackages, function(packageOrPackageName) {
|
|
bundle.includeTests(packageOrPackageName);
|
|
});
|
|
}
|
|
|
|
// Minify, if requested
|
|
if (options.minify)
|
|
bundle.minify();
|
|
|
|
// Write to disk
|
|
bundle.write_to_directory(output_path, app_dir, options.nodeModulesMode);
|
|
|
|
if (bundle.errors.length)
|
|
return bundle.errors;
|
|
} catch (err) {
|
|
return ["Exception while bundling application:\n" + (err.stack || err)];
|
|
}
|
|
};
|