mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
308 lines
10 KiB
JavaScript
308 lines
10 KiB
JavaScript
var _ = require('./third/underscore.js');
|
|
var files = require('./files.js');
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
|
|
// Under the hood, packages in the library (/package/foo), and user
|
|
// applications, are both Packages -- they are just represented
|
|
// differently on disk.
|
|
//
|
|
// To create a package object from a package in the library:
|
|
// var pkg = new Package;
|
|
// pkg.init_from_library(name);
|
|
//
|
|
// To create a package object from an app directory:
|
|
// var pkg = new Package;
|
|
// pkg.init_from_app_dir(app_dir);
|
|
//
|
|
// Or from a collection (a directory whose subdirs are packages):
|
|
// var pkg = new Package;
|
|
// pkg.init_from_collection(collection_dir);
|
|
|
|
var next_package_id = 1;
|
|
var Package = function () {
|
|
var self = this;
|
|
|
|
// Fields set by init_*:
|
|
// name: package name, or null for an app pseudo-package or collection
|
|
// source_root: base directory for resolving source files, null for collection
|
|
// serve_root: base directory for serving files, null for collection
|
|
|
|
// A unique ID (guaranteed to not be reused in this process -- if
|
|
// the package is reloaded, it will get a different id the second
|
|
// time)
|
|
self.id = next_package_id++;
|
|
|
|
// package metadata, from describe()
|
|
self.metadata = {};
|
|
|
|
self.on_use = null;
|
|
self.on_test = null;
|
|
|
|
// registered source file handlers
|
|
self.extensions = {};
|
|
|
|
// functions that can be called when the package is scanned
|
|
self.api = {
|
|
// keys
|
|
// - summary: for 'meteor list'
|
|
// - internal: if true, hide in list
|
|
// - environments: optional
|
|
// (1) if present, if depended on in an environment not on this
|
|
// list, then throw an error
|
|
// (2) if present, these are also the environments that will be
|
|
// used when an application uses the package (since it can't
|
|
// specify environments.) if not present, apps will use
|
|
// [''], which is suitable for a package that doesn't care
|
|
// where it's loaded (like livedata.)
|
|
describe: function (metadata) {
|
|
_.extend(self.metadata, metadata);
|
|
},
|
|
|
|
on_use: function (f) {
|
|
if (self.on_use)
|
|
throw new Error("A package may have only one on_use handler");
|
|
self.on_use = f;
|
|
},
|
|
|
|
on_test: function (f) {
|
|
if (self.on_test)
|
|
throw new Error("A package may have only one on_test handler");
|
|
self.on_test = f;
|
|
},
|
|
|
|
register_extension: function (extension, callback) {
|
|
if (self.on_test)
|
|
throw new Error("This package has already registered a handler for " +
|
|
extension);
|
|
self.extensions[extension] = callback;
|
|
}
|
|
};
|
|
};
|
|
|
|
_.extend(Package.prototype, {
|
|
init_from_library: function (name) {
|
|
var self = this;
|
|
self.name = name;
|
|
self.source_root = path.join(__dirname, '../../packages', name);
|
|
self.serve_root = path.join('/packages', name);
|
|
|
|
var fullpath = path.join(files.get_package_dir(), name, 'package.js');
|
|
var code = fs.readFileSync(fullpath).toString();
|
|
// \n is necessary in case final line is a //-comment
|
|
var wrapped = "(function(Package,require){" + code + "\n})";
|
|
// XXX it'd be nice to runInNewContext so that the package
|
|
// setup code can't mess with our globals, but objects that
|
|
// come out of runInNewContext have bizarro antimatter
|
|
// prototype chains and break 'instanceof Array'. for now,
|
|
// steer clear
|
|
var func = require('vm').runInThisContext(wrapped, fullpath, true);
|
|
// XXX would be nice to eliminate require. packages like
|
|
// 'templating' use this to load other code to run at
|
|
// bundle-time. and to pull in, eg, 'fs' and 'path' to access
|
|
// the file system
|
|
func(self.api, require);
|
|
},
|
|
|
|
init_from_app_dir: function (app_dir, ignore_files) {
|
|
var self = this;
|
|
self.name = null;
|
|
self.source_root = app_dir;
|
|
self.serve_root = '/';
|
|
|
|
var sources_except = function (api, except, tests) {
|
|
return _(self._scan_for_sources(api, ignore_files || []))
|
|
.reject(function (source_path) {
|
|
return ('/' + source_path + '/').indexOf('/' + except + '/') !== -1;
|
|
})
|
|
.filter(function (source_path) {
|
|
var is_test = (('/' + source_path + '/').indexOf('/tests/') !== -1);
|
|
return is_test === (!!tests);
|
|
});
|
|
};
|
|
|
|
self.api.on_use(function (api) {
|
|
// -- Packages --
|
|
|
|
// standard client packages (for now), for the classic meteor
|
|
// stack -- has to come before user packages, because we don't
|
|
// (presently) require packages to declare dependencies on
|
|
// 'standard meteor stuff' like minimongo.
|
|
api.use(['deps', 'session', 'livedata', 'mongo-livedata', 'liveui',
|
|
'templating', 'startup', 'past']);
|
|
api.use(require('./project.js').get_packages(app_dir));
|
|
|
|
// -- Source files --
|
|
api.add_files(sources_except(api, "server"), "client");
|
|
api.add_files(sources_except(api, "client"), "server");
|
|
});
|
|
|
|
self.api.on_test(function (api) {
|
|
api.use(self);
|
|
api.add_files(sources_except(api, "server", true), "client");
|
|
api.add_files(sources_except(api, "client", true), "server");
|
|
});
|
|
},
|
|
|
|
// Find all files under this.source_root that have an extension we
|
|
// recognize, and return them as a list of paths relative to
|
|
// source_root. Ignore files that match a regexp in the ignore_files
|
|
// array, if given. As a special case (ugh), push all html files to
|
|
// the head of the list.
|
|
_scan_for_sources: function (api, ignore_files) {
|
|
var self = this;
|
|
|
|
// find everything in tree, sorted depth-first alphabetically.
|
|
var file_list = files.file_list_sync(self.source_root,
|
|
api.registered_extensions());
|
|
file_list = _.reject(file_list, function (file) {
|
|
return _.any(ignore_files || [], function (pattern) {
|
|
return file.match(pattern);
|
|
});
|
|
});
|
|
file_list.sort(files.sort);
|
|
|
|
// XXX HUGE HACK --
|
|
// push html (template) files ahead of everything else. this is
|
|
// important because the user wants to be able to say
|
|
// Template.foo.events = { ... }
|
|
//
|
|
// maybe all of the templates should go in one file? packages
|
|
// should probably have a way to request this treatment (load
|
|
// order depedency tags?) .. who knows.
|
|
var htmls = [];
|
|
_.each(file_list, function (filename) {
|
|
if (path.extname(filename) === '.html') {
|
|
htmls.push(filename);
|
|
file_list = _.reject(file_list, function (f) { return f === filename;});
|
|
}
|
|
});
|
|
file_list = htmls.concat(file_list);
|
|
|
|
// now make everything relative to source_root
|
|
var prefix = self.source_root;
|
|
if (prefix[prefix.length - 1] !== '/')
|
|
prefix += '/';
|
|
return file_list.map(function (abs) {
|
|
if (prefix.length >= abs.length ||
|
|
abs.substr(0, prefix.length) !== prefix)
|
|
// XXX audit to make sure it works in all possible symlink
|
|
// scenarios
|
|
throw new Error("internal error: source file outside of parent?");
|
|
return abs.substr(prefix.length);
|
|
});
|
|
},
|
|
|
|
init_from_collection: function (collection_dir) {
|
|
var self = this;
|
|
self.name = null;
|
|
self.source_root = null;
|
|
self.serve_root = null;
|
|
|
|
self.api.on_test(function (api) {
|
|
_.each(fs.readdirSync(collection_dir), function (name) {
|
|
// only take things that are actually packages
|
|
if (files.is_package_dir(path.join(collection_dir, name)))
|
|
api.include_tests(name);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// in the future, this could be an on-disk cache that tracks mtimes.
|
|
var package_cache = {};
|
|
|
|
var packages = module.exports = {
|
|
// get a package by name. also maps package objects to themselves.
|
|
get: function (name) {
|
|
if (name instanceof Package)
|
|
return name;
|
|
if (!(name in package_cache)) {
|
|
var pkg = new Package;
|
|
pkg.init_from_library(name);
|
|
package_cache[name] = pkg;
|
|
}
|
|
|
|
return package_cache[name];
|
|
},
|
|
|
|
// get a package that represents an app. (ignore_files is optional
|
|
// and if given, it should be an array of regexps for filenames to
|
|
// ignore when scanning for source files.)
|
|
get_for_app: function (app_dir, ignore_files) {
|
|
var pkg = new Package;
|
|
pkg.init_from_app_dir(app_dir, ignore_files || []);
|
|
return pkg;
|
|
},
|
|
|
|
get_for_collection: function (collection_dir) {
|
|
var pkg = new Package;
|
|
pkg.init_from_collection(collection_dir);
|
|
return pkg;
|
|
},
|
|
|
|
// get a package that represents a particular directory on disk,
|
|
// which might be an app, a package, or even a collection of
|
|
// packages.
|
|
get_for_dir: function (project_dir) {
|
|
if (files.is_app_dir(project_dir))
|
|
return packages.get_for_app(project_dir);
|
|
else if (files.is_package_dir(project_dir))
|
|
// this will need to change when packages are stored in more
|
|
// than one place
|
|
return packages.get(path.basename(project_dir));
|
|
else if (files.is_package_collection_dir(project_dir))
|
|
return packages.get_for_collection(project_dir);
|
|
else
|
|
throw new Error("Unknown project directory type");
|
|
},
|
|
|
|
// force reload of all packages
|
|
flush: function () {
|
|
package_cache = {};
|
|
},
|
|
|
|
// get all packages in the directory, in a map from package name to
|
|
// a package object.
|
|
list: function () {
|
|
var ret = {};
|
|
var dir = files.get_package_dir();
|
|
_.each(fs.readdirSync(dir), function (name) {
|
|
// skip .meteor directory
|
|
if (path.existsSync(path.join(dir, name, 'package.js')))
|
|
ret[name] = packages.get(name);
|
|
});
|
|
|
|
return ret;
|
|
},
|
|
|
|
// returns a pretty list suitable for showing to the user. input is
|
|
// a list of package objects, each of which must have a name (not be
|
|
// an application package.)
|
|
format_list: function (pkgs) {
|
|
var longest = '';
|
|
_.each(pkgs, function (pkg) {
|
|
if (pkg.name.length > longest.length)
|
|
longest = pkg.name;
|
|
});
|
|
var pad = longest.replace(/./g, ' ');
|
|
// it'd be nice to read the actual terminal width, but I tried
|
|
// several methods and none of them work (COLUMNS isn't set in
|
|
// node's environment; `tput cols` returns a constant 80.) maybe
|
|
// node is doing something weird with ptys.
|
|
var width = 80;
|
|
|
|
var out = '';
|
|
_.each(pkgs, function (pkg) {
|
|
if (pkg.metadata.internal)
|
|
return;
|
|
var name = pkg.name + pad.substr(pkg.name.length);
|
|
var summary = pkg.metadata.summary || 'No description';
|
|
out += (name + " " +
|
|
summary.substr(0, width - 2 - pad.length) + "\n");
|
|
});
|
|
|
|
return out;
|
|
}
|
|
}
|