Files
meteor/tools/packages.js
David Glasser 94c7833c82 Delete unused JSAnalyze.findGlobalDottedRefs.
Because it is currently unused and was the only direct use of the 'estraverse'
module, it seems better to minimize the number of dependencies shipped with
Meteor. Admittedly, estraverse is still a dependency of escope, but if we
upgrade escope to a version that has a more specific version requirement for
estraverse that conflicts with our Npm.depends, we'll be shipping two copies for
no reason.
2013-10-04 09:46:48 -07:00

2272 lines
88 KiB
JavaScript

var path = require('path');
var os = require('os');
var _ = require('underscore');
var files = require('./files.js');
var watch = require('./watch.js');
var bundler = require('./bundler.js');
var Builder = require('./builder.js');
var project = require('./project.js');
var buildmessage = require('./buildmessage.js');
var meteorNpm = require('./meteor_npm.js');
var archinfo = require(path.join(__dirname, 'archinfo.js'));
var linker = require(path.join(__dirname, 'linker.js'));
var unipackage = require('./unipackage.js');
var fs = require('fs');
var sourcemap = require('source-map');
// Whenever you change anything about the code that generates unipackages, bump
// this version number. The idea is that the "format" field of the unipackage
// JSON file only changes when the actual specified structure of the
// unipackage/slice changes, but this version (which is build-tool-specific) can
// change when the the contents (not structure) of the built output changes. So
// eg, if we improve the linker's static analysis, this should be bumped.
//
// You should also update this whenever you update any of the packages used
// directly by the unipackage creation process (eg js-analyze) since they do not
// end up as watched dependencies. (At least for now, packages only used in
// target creation (eg minifiers and dev-bundle-fetcher) don't require you to
// update BUILT_BY, though you will need to quit and rerun "meteor run".)
exports.BUILT_BY = 'meteor/10';
// Like Perl's quotemeta: quotes all regexp metacharacters. See
// https://github.com/substack/quotemeta/blob/master/index.js
var quotemeta = function (str) {
return String(str).replace(/(\W)/g, '\\$1');
};
var rejectBadPath = function (p) {
if (p.match(/\.\./))
throw new Error("bad path: " + p);
};
var parseSpec = function (spec) {
var parts = spec.split(':');
if (parts.length > 2 || parts.length === 0)
throw new Error("Bad package spec: " + spec);
var ret = {package: parts[0]};
if (parts.length === 2)
ret.slice = parts[1];
return ret;
};
///////////////////////////////////////////////////////////////////////////////
// Slice
///////////////////////////////////////////////////////////////////////////////
// Options:
// - name [required]
// - arch [required]
// - uses
// - implies
// - getSourcesFunc
// - exports
// - watchSet
// - nodeModulesPath
//
// Do not include the source files in watchSet. They will be
// added at compile time when the sources are actually read.
var Slice = function (pkg, options) {
var self = this;
options = options || {};
self.pkg = pkg;
// Name for this slice. For example, the "client" in "ddp.client"
// (which, NB, we might load on server arches.)
self.sliceName = options.name;
// The architecture (fully or partially qualified) that can use this
// slice.
self.arch = options.arch;
// Unique ID for this slice. Unique across all slices of all
// packages, but constant across reloads of this slice.
self.id = pkg.id + "." + options.name + "@" + self.arch;
// Packages used. The ordering is significant only for determining
// import symbol priority (it doesn't affect load order), and a
// given package could appear more than once in the list, so code
// that consumes this value will need to guard appropriately. Each
// element in the array has keys:
// - package: the package name
// - slice: the slice name (optional)
// - unordered: If true, we don't want the package's imports and we
// don't want to force the package to load before us. We just want
// to ensure that it loads if we load.
// - weak: If true, we don't *need* to load the other package, but
// if the other package ends up loaded in the target, it must
// be forced to load before us. We will not get its imports
// or plugins.
// It is an error for both unordered and weak to be true, because
// such a dependency would have no effect.
//
// In most places, you want to use slice.eachUsedSlice() instead of
// slice.uses, which also takes into account implied packages.
self.uses = options.uses;
// Packages which are "implied" by using this package. If a slice X uses this
// slice Y, and Y implies Z, then X will effectively use Z as well (and get
// its imports and plugins). An array of objects of the same type as the
// elements of self.uses (although for now unordered and weak are not
// allowed).
self.implies = options.implies || [];
// A function that returns the source files for this slice. Array of objects
// with keys "relPath" and "fileOptions". Null if loaded from unipackage.
//
// fileOptions is optional and represents arbitrary options passed to
// "api.add_files"; they are made available on to the plugin as
// compileStep.fileOptions.
//
// This is a function rather than a literal array because for an
// app, we need to know the file extensions registered by the
// plugins in order to compute the sources list, so we have to wait
// until build time (after we have loaded any plugins, including
// local plugins in this package) to compute this.
self.getSourcesFunc = options.getSourcesFunc || null;
// True if this slice is not permitted to have any exports, and in fact should
// not even define `Package.name` (ie, test slices).
self.noExports = options.noExports || false;
// Symbols that this slice should export. List of symbols (as strings). Null
// on built packages (see packageVariables instead), or in packages where
// noExports is set.
self.declaredExports = options.declaredExports || null;
// Files and directories that we want to monitor for changes in
// development mode, such as source files and package.js, as a watch.WatchSet.
self.watchSet = options.watchSet || new watch.WatchSet();
// Has this slice been compiled?
self.isBuilt = false;
// Prelink output.
//
// 'prelinkFiles' is the partially linked JavaScript code (an
// array of objects with keys 'source' and 'servePath', both strings -- see
// prelink() in linker.js)
//
// 'packageVariables' are are variables that are syntactically globals in our
// input files and which we capture with a package-scope closure. A list of
// objects with keys 'name' (required) and 'export' (true, 'tests', or falsy).
//
// Both of these are saved into slices on disk, and are inputs into the final
// link phase, which inserts the final JavaScript resources into
// 'resources'. Set only when isBuilt is true.
self.prelinkFiles = null;
self.packageVariables = null;
// All of the data provided for eventual inclusion in the bundle,
// other than JavaScript that still needs to be fed through the
// final link stage. A list of objects with these keys:
//
// type: "js", "css", "head", "body", "asset"
//
// data: The contents of this resource, as a Buffer. For example,
// for "head", the data to insert in <head>; for "js", the
// JavaScript source code (which may be subject to further
// processing such as minification); for "asset", the contents of a
// static resource such as an image.
//
// servePath: The (absolute) path at which the resource would prefer
// to be served. Interpretation varies by type. For example, always
// honored for "asset", ignored for "head" and "body", sometimes
// honored for CSS but ignored if we are concatenating.
//
// sourceMap: Allowed only for "js". If present, a string.
//
// Set only when isBuilt is true.
self.resources = null;
// Absolute path to the node_modules directory to use at runtime to
// resolve Npm.require() calls in this slice. null if this slice
// does not have a node_modules.
self.nodeModulesPath = options.nodeModulesPath;
};
_.extend(Slice.prototype, {
// Move the slice to the 'built' state. Process all source files
// through the appropriate handlers and run the prelink phase on any
// resulting JavaScript. Also add all provided source files to the
// package dependencies. Sets fields such as dependencies, exports,
// prelinkFiles, packageVariables, and resources.
build: function () {
var self = this;
var isApp = ! self.pkg.name;
if (self.isBuilt)
throw new Error("slice built twice?");
var resources = [];
var js = [];
// Preemptively check to make sure that each of the packages we
// reference actually exist. If we find a package that doesn't
// exist, emit an error and remove it from the package list. That
// way we get one error about it instead of a new error at each
// stage in the build process in which we try to retrieve the
// package.
_.each(['uses', 'implies'], function (field) {
var scrubbed = [];
_.each(self[field], function (u) {
var pkg = self.pkg.library.get(u.package, /* throwOnError */ false);
if (! pkg) {
buildmessage.error("no such package: '" + u.package + "'");
// recover by omitting this package from the field
} else
scrubbed.push(u);
});
self[field] = scrubbed;
});
var addAsset = function (contents, relPath) {
// XXX hack
if (!self.pkg.name)
relPath = relPath.replace(/^(private|public)\//, '');
resources.push({
type: "asset",
data: contents,
path: relPath,
servePath: path.join(self.pkg.serveRoot, relPath)
});
};
_.each(self.getSourcesFunc(), function (source) {
var relPath = source.relPath;
var fileOptions = _.clone(source.fileOptions) || {};
var absPath = path.resolve(self.pkg.sourceRoot, relPath);
var filename = path.basename(relPath);
var handler = !fileOptions.isAsset && self._getSourceHandler(filename);
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.
//
// XXX This is pretty confusing, especially if you've
// accidentally forgotten a plugin -- revisit?
addAsset(contents, relPath);
return;
}
// This object is called a #CompileStep and it's the interface
// to plugins that define new source file handlers (eg,
// Coffeescript.)
//
// Fields on CompileStep:
//
// - arch: the architecture for which we are building
// - inputSize: total number of bytes in the input file
// - inputPath: the filename and (relative) path of the input
// file, eg, "foo.js". We don't provide a way to get the full
// path because you're not supposed to read the file directly
// off of disk. Instead you should call read(). That way we
// can ensure that the version of the file that you use is
// exactly the one that is recorded in the dependency
// information.
// - pathForSourceMap: If this file is to be included in a source map,
// this is the name you should use for it in the map.
// - rootOutputPath: on browser targets, for resources such as
// stylesheet and static assets, this is the root URL that
// will get prepended to the paths you pick for your output
// files so that you get your own namespace, for example
// '/packages/foo'. null on non-browser targets
// - fileOptions: any options passed to "api.add_files"; for
// use by the plugin. The built-in "js" plugin uses the "bare"
// option for files that shouldn't be wrapped in a closure.
// - declaredExports: An array of symbols exported by this slice, or null
// if it may not export any symbols (eg, test slices). This is used by
// CoffeeScript to ensure that it doesn't close over those symbols, eg.
// - read(n): read from the input file. If n is given it should
// be an integer, and you will receive the next n bytes of the
// file as a Buffer. If n is omitted you get the rest of the
// file.
// - appendDocument({ section: "head", data: "my markup" })
// Browser targets only. Add markup to the "head" or "body"
// section of the document.
// - addStylesheet({ path: "my/stylesheet.css", data: "my css" })
// Browser targets only. Add a stylesheet to the
// document. 'path' is a requested URL for the stylesheet that
// may or may not ultimately be honored. (Meteor will add
// appropriate tags to cause the stylesheet to be loaded. It
// will be subject to any stylesheet processing stages in
// effect, such as minification.)
// - addJavaScript({ path: "my/program.js", data: "my code",
// sourcePath: "src/my/program.js",
// bare: true })
// Add JavaScript code, which will be namespaced into this
// package's environment (eg, it will see only the exports of
// this package's imports), and which will be subject to
// minification and so forth. Again, 'path' is merely a hint
// that may or may not be honored. 'sourcePath' is the path
// that will be used in any error messages generated (eg,
// "foo.js:4:1: syntax error"). It must be present and should
// be relative to the project root. Typically 'inputPath' will
// do handsomely. "bare" means to not wrap the file in
// a closure, so that its vars are shared with other files
// in the module.
// - addAsset({ path: "my/image.png", data: Buffer })
// Add a file to serve as-is over HTTP (browser targets) or
// to include as-is in the bundle (os targets).
// This time `data` is a Buffer rather than a string. For
// browser targets, it will be served at the exact path you
// request (concatenated with rootOutputPath). For server
// targets, the file can be retrieved by passing path to
// Assets.getText or Assets.getBinary.
// - error({ message: "There's a problem in your source file",
// sourcePath: "src/my/program.ext", line: 12,
// column: 20, func: "doStuff" })
// Flag an error -- at a particular location in a source
// file, if you like (you can even indicate a function name
// to show in the error, like in stack traces.) sourcePath,
// line, column, and func are all optional.
//
// XXX for now, these handlers must only generate portable code
// (code that isn't dependent on the arch, other than 'browser'
// vs 'os') -- they can look at the arch that is provided
// but they can't rely on the running on that particular arch
// (in the end, an arch-specific slice will be emitted only if
// there are native node modules.) Obviously this should
// change. A first step would be a setOutputArch() function
// analogous to what we do with native node modules, but maybe
// what we want is the ability to ask the plugin ahead of time
// how specific it would like to force builds to be.
//
// XXX we handle encodings in a rather cavalier way and I
// suspect we effectively end up assuming utf8. We can do better
// than that!
//
// XXX addAsset probably wants to be able to set MIME type and
// also control any manifest field we deem relevant (if any)
//
// XXX Some handlers process languages that have the concept of
// include files. These are problematic because we need to
// somehow instrument them to get the names and hashs of all of
// the files that they read for dependency tracking purposes. We
// don't have an API for that yet, so for now we provide a
// workaround, which is that _fullInputPath contains the full
// absolute path to the input files, which allows such a plugin
// to set up its include search path. It's then on its own for
// registering dependencies (for now..)
//
// XXX in the future we should give plugins an easy and clean
// way to return errors (that could go in an overall list of
// errors experienced across all files)
var readOffset = 0;
var compileStep = {
inputSize: contents.length,
inputPath: relPath,
_fullInputPath: absPath, // avoid, see above..
// XXX duplicates _pathForSourceMap() in linker
pathForSourceMap: (
self.pkg.name
? self.pkg.name + "/" + relPath
: path.basename(relPath)),
// null if this is an app. intended to be used for the sources
// dictionary for source maps.
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) {
if (n === undefined || readOffset + n > contents.length)
n = contents.length - readOffset;
var ret = contents.slice(readOffset, readOffset + n);
readOffset += n;
return ret;
},
appendDocument: function (options) {
if (! archinfo.matches(self.arch, "browser"))
throw new Error("Document sections can only be emitted to " +
"browser targets");
if (options.section !== "head" && options.section !== "body")
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");
resources.push({
type: options.section,
data: new Buffer(options.data, 'utf8')
});
},
addStylesheet: function (options) {
if (! archinfo.matches(self.arch, "browser"))
throw new Error("Stylesheets can only be emitted to " +
"browser targets");
if (typeof options.data !== "string")
throw new Error("'data' option to addStylesheet must be a string");
resources.push({
type: "css",
data: new Buffer(options.data, 'utf8'),
servePath: path.join(self.pkg.serveRoot, options.path)
});
},
addJavaScript: function (options) {
if (typeof options.data !== "string")
throw new Error("'data' option to addJavaScript must be a string");
if (typeof options.sourcePath !== "string")
throw new Error("'sourcePath' option must be supplied to addJavaScript. Consider passing inputPath.");
if (options.bare && ! archinfo.matches(self.arch, "browser"))
throw new Error("'bare' option may only be used for browser targets");
js.push({
source: options.data,
sourcePath: options.sourcePath,
servePath: path.join(self.pkg.serveRoot, options.path),
bare: !!options.bare,
sourceMap: options.sourceMap
});
},
addAsset: function (options) {
if (! (options.data instanceof Buffer))
throw new Error("'data' option to addAsset must be a Buffer");
addAsset(options.data, options.path);
},
error: function (options) {
buildmessage.error(options.message || ("error building " + relPath), {
file: options.sourcePath,
line: options.line ? options.line : undefined,
column: options.column ? options.column : undefined,
func: options.func ? options.func : undefined
});
}
};
try {
(buildmessage.markBoundary(handler))(compileStep);
} catch (e) {
e.message = e.message + " (compiling " + relPath + ")";
buildmessage.exception(e);
// Recover by ignoring this source file (as best we can -- the
// handler might already have emitted resources)
}
});
// Phase 1 link
// Load jsAnalyze from the js-analyze package... unless we are the
// js-analyze package, in which case never mind. (The js-analyze package's
// default slice is not allowed to depend on anything!)
var jsAnalyze = null;
if (! _.isEmpty(js) && self.pkg.name !== "js-analyze") {
jsAnalyze = unipackage.load({
library: self.pkg.library,
packages: ["js-analyze"]
})["js-analyze"].JSAnalyze;
}
var results = linker.prelink({
inputFiles: js,
useGlobalNamespace: isApp,
combinedServePath: isApp ? null :
"/packages/" + self.pkg.name +
(self.sliceName === "main" ? "" : (":" + self.sliceName)) + ".js",
name: self.pkg.name || null,
declaredExports: _.pluck(self.declaredExports, 'name'),
jsAnalyze: jsAnalyze
});
// Add dependencies on the source code to any plugins that we could have
// used. We need to depend even on plugins that we didn't use, because if
// they were changed they might become relevant to us. This means that we
// end up depending on every source file contributing to all plugins in the
// packages we use (including source files from other packages that the
// plugin program itself uses), as well as the package.js file from every
// package we directly use (since changing the package.js may add or remove
// a plugin).
_.each(self._activePluginPackages(), function (otherPkg) {
self.watchSet.merge(otherPkg.pluginWatchSet);
// XXX this assumes this is not overwriting something different
self.pkg.pluginProviderPackageDirs[otherPkg.name] =
otherPkg.packageDirectoryForBuildInfo;
});
self.prelinkFiles = results.files;
self.packageVariables = [];
var packageVariableNames = {};
_.each(self.declaredExports, function (symbol) {
if (_.has(packageVariableNames, symbol.name))
return;
self.packageVariables.push({
name: symbol.name,
export: symbol.testOnly? "tests" : true
});
packageVariableNames[symbol.name] = true;
});
_.each(results.assignedVariables, function (name) {
if (_.has(packageVariableNames, name))
return;
self.packageVariables.push({
name: name
});
packageVariableNames[name] = true;
});
// Forget about the *declared* exports; what matters is packageVariables
// now.
self.declaredExports = null;
self.resources = resources;
self.isBuilt = true;
},
// Get the resources that this function contributes to a bundle, in
// the same format as self.resources as documented above. This
// includes static assets and fully linked JavaScript.
//
// @param bundleArch The architecture targeted by the bundle. Might
// be more specific than self.arch.
//
// It is when you call this function that we read our dependent
// packages and commit to whatever versions of them we currently
// have in the library -- at least for the purpose of imports, which
// is resolved at bundle time. (On the other hand, when it comes to
// the extension handlers we'll use, we previously commited to those
// versions at package build ('compile') time.)
getResources: function (bundleArch) {
var self = this;
var library = self.pkg.library;
if (! self.isBuilt)
throw new Error("getting resources of unbuilt slice?" + self.pkg.name + " " + self.sliceName + " " + self.arch);
if (! archinfo.matches(bundleArch, self.arch))
throw new Error("slice of arch '" + self.arch + "' does not support '" +
bundleArch + "'?");
// Compute imports by merging the exports of all of the packages
// we use. Note that in the case of conflicting symbols, later
// packages get precedence.
//
// We don't get imports from unordered dependencies (since they may not be
// defined yet) or from weak dependencies (because the meaning of a name
// shouldn't be affected by the non-local decision of whether or not an
// unrelated package in the target depends on something).
var imports = {}; // map from symbol to supplying package name
self.eachUsedSlice(
bundleArch, {skipWeak: true, skipUnordered: true}, function (otherSlice) {
if (! otherSlice.isBuilt)
throw new Error("dependency wasn't built?");
_.each(otherSlice.packageVariables, function (symbol) {
// Slightly hacky implementation of test-only exports.
if (symbol.export === true ||
(symbol.export === "tests" && self.sliceName === "tests"))
imports[symbol.name] = otherSlice.pkg.name;
});
});
// Phase 2 link
var isApp = ! self.pkg.name;
var files = linker.link({
imports: imports,
useGlobalNamespace: isApp,
// XXX report an error if there is a package called global-imports
importStubServePath: isApp && '/packages/global-imports.js',
prelinkFiles: self.prelinkFiles,
noExports: self.noExports,
packageVariables: self.packageVariables,
includeSourceMapInstructions: archinfo.matches(self.arch, "browser"),
name: self.pkg.name || null
});
// Add each output as a resource
var jsResources = _.map(files, function (file) {
return {
type: "js",
data: new Buffer(file.source, 'utf8'), // XXX encoding
servePath: file.servePath,
sourceMap: file.sourceMap
};
});
return _.union(self.resources, jsResources); // union preserves order
},
// Calls `callback` with each slice (of architecture matching `arch`) that is
// "used" by this slice. This includes directly used slices, and slices that
// are transitively "implied" by used slices. (But not slices that are used by
// slices that we use!) Options are skipWeak and skipUnordered, meaning to
// ignore direct "uses" that are weak or unordered.
eachUsedSlice: function (arch, options, callback) {
var self = this;
if (typeof options === "function") {
callback = options;
options = {};
}
var processedSliceId = {};
var usesToProcess = [];
_.each(self.uses, function (use) {
if (options.skipUnordered && use.unordered)
return;
if (options.skipWeak && use.weak)
return;
usesToProcess.push(use);
});
while (!_.isEmpty(usesToProcess)) {
var use = usesToProcess.shift();
var slices =
self.pkg.library.getSlices(_.pick(use, 'package', 'spec'), arch);
_.each(slices, function (slice) {
if (_.has(processedSliceId, slice.id))
return;
processedSliceId[slice.id] = true;
callback(slice, {
unordered: !!use.unordered,
weak: !!use.weak
});
_.each(slice.implies, function (implied) {
usesToProcess.push(implied);
});
});
}
},
// Return an array of all plugins that are active in this slice, as
// a list of Packages.
_activePluginPackages: function () {
var self = this;
// XXX we used to include our own extensions only if we were the
// "use" role. now we include them everywhere because we don't
// have a special "use" role anymore. it's not totally clear to me
// what the correct behavior should be -- we need to resolve
// whether we think about extensions as being global to a package
// or particular to a slice.
// (there's also some weirdness here with handling implies, because
// the implies field is on the target slice, but we really only care
// about packages.)
var ret = [self.pkg];
// We don't use plugins from weak dependencies, because the ability to
// compile a certain type of file shouldn't depend on whether or not some
// unrelated package in the target has a dependency.
//
// We pass archinfo.host here, not self.arch, because it may be more
// specific, and because plugins always have to run on the host
// architecture.
self.eachUsedSlice(archinfo.host(), {skipWeak: true}, function (usedSlice) {
ret.push(usedSlice.pkg);
});
// Only need one copy of each package.
ret = _.uniq(ret);
_.each(ret, function (pkg) {
pkg._ensurePluginsInitialized();
});
return ret;
},
// Get all extensions handlers registered in this slice, as a map
// from extension (no leading dot) to handler function. Throws an
// exception if two packages are registered for the same extension.
_allHandlers: function () {
var self = this;
var ret = {};
// We provide a hardcoded handler for *.js files.. since plugins
// are written in JavaScript we have to start somewhere.
_.extend(ret, {
js: function (compileStep) {
compileStep.addJavaScript({
data: compileStep.read().toString('utf8'),
path: compileStep.inputPath,
sourcePath: compileStep.inputPath,
// XXX eventually get rid of backward-compatibility "raw" name
// XXX COMPAT WITH 0.6.4
bare: compileStep.fileOptions.bare || compileStep.fileOptions.raw
});
}
});
_.each(self._activePluginPackages(), function (otherPkg) {
_.each(otherPkg.sourceHandlers, function (handler, ext) {
if (ext in ret && ret[ext] !== handler) {
buildmessage.error(
"conflict: two packages included in " +
(self.pkg.name || "the app") + ", " +
(ret[ext].pkg.name || "the app") + " and " +
(otherPkg.name || "the app") + ", " +
"are both trying to handle ." + ext);
// Recover by just going with the first handler we saw
} else {
ret[ext] = handler;
}
});
});
return ret;
},
// Return a list of all of the extension that indicate source files
// for this slice, not including leading dots. Computed based on
// this.uses, so should only be called once that has been set.
registeredExtensions: function () {
var self = this;
return _.keys(self._allHandlers());
},
// Find the function that should be used to handle a source file for
// this slice, or return null if there isn't one. We'll use handlers
// that are defined in this package and in its immediate dependencies.
_getSourceHandler: function (filename) {
var self = this;
var handlers = self._allHandlers();
var parts = filename.split('.');
for (var i = 0; i < parts.length; i++) {
var extension = parts.slice(i).join('.');
if (_.has(handlers, extension))
return handlers[extension];
}
return null;
}
});
///////////////////////////////////////////////////////////////////////////////
// Packages
///////////////////////////////////////////////////////////////////////////////
// XXX This object conflates two things that now seem to be almost
// totally separate: source code for a package, and an actual built
// package that is ready to be used. In fact it contains a list of
// Slice objects about which the same thing can be said. To see the
// distinction, ask yourself, what fields are set when the package is
// initialized via initFromUnipackage?
//
// Package and Slice should each be split into two objects, eg
// PackageSource and SliceSource versus BuiltPackage and BuiltSlice
// (find better names, though.)
var nextPackageId = 1;
var Package = function (library, packageDirectoryForBuildInfo) {
var self = this;
// 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 = nextPackageId++;
// The name of the package, or null for an app pseudo-package or
// collection. The package's exports will reside in Package.<name>.
// When it is null it is linked like an application instead of like
// a package.
self.name = null;
// The path relative to which all source file paths are interpreted
// in this package. Also used to compute the location of the
// package's .npm directory (npm shrinkwrap state.) null if loaded
// from unipackage.
self.sourceRoot = null;
// Path that will be prepended to the URLs of all resources emitted
// by this package (assuming they don't end up getting
// concatenated.) For non-browser targets, the only effect this will
// have is to change the actual on-disk paths of the files in the
// bundle, for those that care to open up the bundle and look (but
// it's still nice to get it right.) null if loaded from unipackage.
self.serveRoot = null;
// The package's directory. This is used only by other packages that use this
// package in their buildinfo.json (to detect that they need to be rebuilt if
// the library's resolution of the package name changes); it is not used to
// read files or anything else. Notably, it should be the same if a package is
// read from a source tree or read from the .build unipackage inside that
// source tree.
self.packageDirectoryForBuildInfo = packageDirectoryForBuildInfo;
// Package library that should be used to resolve this package's
// dependencies
self.library = library;
// Package metadata. Keys are 'summary' and 'internal'.
self.metadata = {};
// Available editions/subpackages ("slices") of this package. Array
// of Slice.
self.slices = [];
// Map from an arch to the list of slice names that should be
// included by default if this package is used without specifying a
// slice (eg, as "ddp" rather than "ddp.server"). The most specific
// arch will be used.
self.defaultSlices = {};
// Map from an arch to the list of slice names that should be
// included when this package is tested. The most specific arch will
// be used.
self.testSlices = {};
// The information necessary to build the plugins in this
// package. Map from plugin name to object with keys 'name', 'use',
// 'sources', and 'npmDependencies'.
self.pluginInfo = {};
// Plugins in this package. Map from plugin name to JsImage. Present only when
// pluginsBuilt is true.
self.plugins = {};
// A WatchSet for the full transitive dependencies for all plugins in this
// package, as well as this package's package.js. If any of these dependencies
// change, our plugins need to be rebuilt... but also, any package that
// directly uses this package needs to be rebuilt in case the change to
// plugins affected compilation.
//
// Complete only when pluginsBuilt is true.
self.pluginWatchSet = new watch.WatchSet();
// Map from package name to packageDirectoryForBuildInfo of packages that are
// directly used by this package. We use this to figure out that we need to
// rebuild if the resolution of the package changes (eg, an app package is
// added that overshadows a warehouse package, or the release changes).
self.pluginProviderPackageDirs = {};
// True if plugins have been initialized (if _ensurePluginsInitialized has
// been called)
self._pluginsInitialized = false;
// Source file handlers registered by plugins. Map from extension
// (without a dot) to a handler function that takes a
// CompileStep. Valid only when _pluginsInitialized is true.
self.sourceHandlers = null;
// Is this package in a built state? If not (if you created it by
// means that doesn't create it in a build state to start with) you
// will need to call build() before you can use it. We break down
// the two phases of the build process, plugin building and
// slice building, into two flags.
self.pluginsBuilt = false;
self.slicesBuilt = false;
};
_.extend(Package.prototype, {
// Make a dummy (empty) package that contains nothing of interest.
initEmpty: function (name) {
var self = this;
self.name = name;
self.defaultSlices = {'': []};
self.testSlices = {'': []};
},
// Return the slice of the package to use for a given slice name
// (eg, 'main' or 'test') and target architecture (eg,
// 'os.linux.x86_64' or 'browser'), or throw an exception if
// that packages can't be loaded under these circumstances.
getSingleSlice: function (name, arch) {
var self = this;
var chosenArch = archinfo.mostSpecificMatch(
arch, _.pluck(_.where(self.slices, { sliceName: name }), 'arch'));
if (! chosenArch) {
// XXX need improvement. The user should get a graceful error
// message, not an exception, and all of this talk of slices an
// architectures is likely to be confusing/overkill in many
// contexts.
throw new Error((self.name || "this app") +
" does not have a slice named '" + name +
"' that runs on architecture '" + arch + "'");
}
return _.where(self.slices, { sliceName: name, arch: chosenArch })[0];
},
// Return the slices that should be used on a given arch if the
// package is named without any qualifiers (eg, 'ddp' rather than
// 'ddp.client').
//
// On error, throw an exception, or if inside
// buildmessage.capture(), log a build error and return [].
getDefaultSlices: function (arch) {
var self = this;
var chosenArch = archinfo.mostSpecificMatch(arch,
_.keys(self.defaultSlices));
if (! chosenArch) {
buildmessage.error(
(self.name || "this app") +
" is not compatible with architecture '" + arch + "'",
{ secondary: true });
// recover by returning by no slices
return [];
}
return _.map(self.defaultSlices[chosenArch], function (name) {
return self.getSingleSlice(name, arch);
});
},
// Return the slices that should be used to test the package on a
// given arch.
getTestSlices: function (arch) {
var self = this;
var chosenArch = archinfo.mostSpecificMatch(arch,
_.keys(self.testSlices));
if (! chosenArch) {
buildmessage.error(
(self.name || "this app") +
" does not have tests for architecture " + arch + "'",
{ secondary: true });
// recover by returning by no slices
return [];
}
return _.map(self.testSlices[chosenArch], function (name) {
return self.getSingleSlice(name, arch);
});
},
// This is called on all packages at Meteor install time so they can
// do any prep work necessary for the user's first Meteor run to be
// fast, for example fetching npm dependencies. Currently thanks to
// refactorings there's nothing to do here.
// XXX remove?
preheat: function () {
},
// If this package has plugins, initialize them (run the startup
// code in them so that they register their extensions.) Idempotent.
_ensurePluginsInitialized: function () {
var self = this;
if (! self.pluginsBuilt)
throw new Error("running plugins of unbuilt package?");
if (self._pluginsInitialized)
return;
var Plugin = {
// 'extension' is a file extension without the separation dot
// (eg 'js', 'coffee', 'coffee.md')
//
// 'handler' is a function that takes a single argument, a
// CompileStep (#CompileStep)
registerSourceHandler: function (extension, handler) {
if (_.has(self.sourceHandlers, extension)) {
buildmessage.error("duplicate handler for '*." +
extension + "'; may only have one per Plugin",
{ useMyCaller: true });
// recover by ignoring all but the first
return;
}
self.sourceHandlers[extension] = handler;
}
};
self.sourceHandlers = {};
_.each(self.plugins, function (plugin, name) {
buildmessage.enterJob({
title: "loading plugin `" + name +
"` from package `" + self.name + "`"
// don't necessarily have rootPath anymore
// (XXX we do, if the unipackage was locally built, which is
// the important case for debugging. it'd be nice to get this
// case right.)
}, function () {
plugin.load({Plugin: Plugin});
});
});
self._pluginsInitialized = true;
},
// Move a package to the built state (by running its source files
// through the appropriate compiler plugins.) Once build has
// completed, any errors detected in the package will have been
// emitted to buildmessage.
//
// build() may retrieve the package's dependencies from the library,
// so it is illegal to call build() from library.get() (until the
// package has actually been put in the loaded package list.)
build: function () {
var self = this;
if (self.pluginsBuilt || self.slicesBuilt)
throw new Error("package already built?");
// Build plugins
_.each(self.pluginInfo, function (info) {
buildmessage.enterJob({
title: "building plugin `" + info.name +
"` in package `" + self.name + "`",
rootPath: self.sourceRoot
}, function () {
var buildResult = bundler.buildJsImage({
name: info.name,
library: self.library,
use: info.use,
sourceRoot: self.sourceRoot,
sources: info.sources,
npmDependencies: info.npmDependencies,
// Plugins have their own npm dependencies separate from the
// rest of the package, so they need their own separate npm
// shrinkwrap and cache state.
npmDir: path.resolve(path.join(self.sourceRoot, '.npm', 'plugin',
info.name))
});
// Add this plugin's dependencies to our "plugin dependency" WatchSet.
self.pluginWatchSet.merge(buildResult.watchSet);
// Remember the library resolution of all packages used by the plugin.
// XXX assumes that this merges cleanly
_.extend(self.pluginProviderPackageDirs,
buildResult.pluginProviderPackageDirs);
// Register the built plugin's code.
self.plugins[info.name] = buildResult.image;
});
});
self.pluginsBuilt = true;
// Build slices. Might use our plugins, so needs to happen
// second.
_.each(self.slices, function (slice) {
slice.build();
});
self.slicesBuilt = true;
},
// Programmatically initialized a package from scratch. For now,
// cannot create browser packages. This function does not retrieve
// the package's dependencies from the library, and on return,
// the package will be in an unbuilt state.
//
// Unlike user-facing methods of creating a package
// (initFromPackageDir, initFromAppDir) this does not implicitly add
// a dependency on the 'meteor' package. If you want such a
// dependency then you must add it yourself.
//
// If called inside a buildmessage job, it will keep going if things
// go wrong. Be sure to call jobHasMessages to see if it actually
// succeeded.
//
// Options:
// - sourceRoot (required if sources present)
// - serveRoot (required if sources present)
// - sliceName
// - use
// - sources (array of paths or relPath/fileOptions objects)
// - npmDependencies
// - npmDir
initFromOptions: function (name, options) {
var self = this;
self.name = name;
if (options.sources && ! _.isEmpty(options.sources.length) &&
(! options.sourceRoot || ! options.serveRoot))
throw new Error("When source files are given, sourceRoot and " +
"serveRoot must be specified");
self.sourceRoot = options.sourceRoot || path.sep;
self.serveRoot = options.serveRoot || path.sep;
var isPortable = true;
var nodeModulesPath = null;
meteorNpm.ensureOnlyExactVersions(options.npmDependencies);
if (options.npmDir) {
// Always run updateDependencies, even if there are no dependencies: there
// may be a .npm directoryon disk to delete.
if (meteorNpm.updateDependencies(name, options.npmDir,
options.npmDependencies)) {
// At least one dependency was installed, and there were no errors.
if (!meteorNpm.dependenciesArePortable(options.npmDir))
isPortable = false;
nodeModulesPath = path.join(options.npmDir, 'node_modules');
}
}
var sources = _.map(options.sources, function (source) {
if (typeof source === "string")
return {relPath: source};
return source;
});
var arch = isPortable ? "os" : archinfo.host();
var slice = new Slice(self, {
name: options.sliceName,
arch: arch,
uses: _.map(options.use, parseSpec),
getSourcesFunc: function () { return sources; },
nodeModulesPath: nodeModulesPath
});
self.slices.push(slice);
self.defaultSlices = {'os': [options.sliceName]};
},
// Initialize a package from a legacy-style (package.js) package
// directory. This function does not retrieve the package's
// dependencies from the library, and on return, the package will be
// in an unbuilt state.
initFromPackageDir: function (name, dir, options) {
var self = this;
var isPortable = true;
options = options || {};
self.name = name;
self.sourceRoot = dir;
self.serveRoot = path.join(path.sep, 'packages', name);
if (! fs.existsSync(self.sourceRoot))
throw new Error("putative package directory " + dir + " doesn't exist?");
var roleHandlers = {use: null, test: null};
var npmDependencies = null;
var packageJsPath = path.join(self.sourceRoot, 'package.js');
var code = fs.readFileSync(packageJsPath);
var packageJsHash = Builder.sha1(code);
// Any package that depends on us needs to be rebuilt if our package.js file
// changes, because a change to package.js might add or remove a plugin,
// which could change a file from being handled by extension vs treated as
// an asset.
self.pluginWatchSet.addFile(packageJsPath, packageJsHash);
// == 'Package' object visible in package.js ==
var Package = {
// Set package metadata. Options:
// - summary: for 'meteor list'
// - internal: if true, hide in list
// There used to be a third option documented here,
// 'environments', but it was never implemented and no package
// ever used it.
describe: function (options) {
_.extend(self.metadata, options);
},
on_use: function (f) {
if (roleHandlers.use) {
buildmessage.error("duplicate on_use handler; a package may have " +
"only one", { useMyCaller: true });
// Recover by ignoring the duplicate
return;
}
roleHandlers.use = f;
},
on_test: function (f) {
if (roleHandlers.test) {
buildmessage.error("duplicate on_test handler; a package may have " +
"only one", { useMyCaller: true });
// Recover by ignoring the duplicate
return;
}
roleHandlers.test = f;
},
// XXX COMPAT WITH 0.6.4
// extension doesn't contain a dot
register_extension: function () {
buildmessage.error(
"Package.register_extension() is no longer supported. Use " +
"Package._transitional_registerBuildPlugin instead.",
{ useMyCaller: true });
// recover by ignoring
},
// Define a plugin. A plugin extends the build process for
// targets that use this package. For example, a Coffeescript
// compiler would be a plugin. A plugin is its own little
// program, with its own set of source files, used packages, and
// npm dependencies.
//
// This is an experimental API and for now you should assume
// that it will change frequently and radically (thus the
// '_transitional_'.) For maximum R&D velocity and for the good
// of the platform, we will push changes that break your
// packages that use this API. You've been warned.
//
// Options:
// - name: a name for this plugin. required (cosmetic -- string)
// - use: package to use for the plugin (names, as strings)
// - sources: sources for the plugin (array of string)
// - npmDependencies: map from npm package name to required
// version (string)
_transitional_registerBuildPlugin: function (options) {
if (! ('name' in options)) {
buildmessage.error("build plugins require a name",
{ useMyCaller: true });
// recover by ignoring plugin
return;
}
if (options.name in self.pluginInfo) {
buildmessage.error("this package already has a plugin named '" +
options.name + "'",
{ useMyCaller: true });
// recover by ignoring plugin
return;
}
if (options.name.match(/\.\./) || options.name.match(/[\\\/]/)) {
buildmessage.error("bad plugin name", { useMyCaller: true });
// recover by ignoring plugin
return;
}
// XXX probably want further type checking
self.pluginInfo[options.name] = options;
}
};
// == 'Npm' object visible in package.js ==
var Npm = {
depends: function (_npmDependencies) {
// XXX make npmDependencies be per slice, so that production
// doesn't have to ship all of the npm modules used by test
// code
if (npmDependencies) {
buildmessage.error("Npm.depends may only be called once per package",
{ useMyCaller: true });
// recover by ignoring the Npm.depends line
return;
}
if (typeof _npmDependencies !== 'object') {
buildmessage.error("the argument to Npm.depends should be an " +
"object, like this: {gcd: '0.0.0'}",
{ useMyCaller: true });
// recover by ignoring the Npm.depends line
return;
}
// don't allow npm fuzzy versions so that there is complete
// consistency when deploying a meteor app
//
// XXX use something like seal or lockdown to have *complete*
// confidence we're running the same code?
try {
meteorNpm.ensureOnlyExactVersions(_npmDependencies);
} catch (e) {
buildmessage.error(e.message, { useMyCaller: true, downcase: true });
// recover by ignoring the Npm.depends line
return;
}
npmDependencies = _npmDependencies;
},
require: function (name) {
var nodeModuleDir = path.join(self.sourceRoot,
'.npm', 'package', 'node_modules', name);
if (fs.existsSync(nodeModuleDir)) {
return require(nodeModuleDir);
} else {
try {
return require(name); // from the dev bundle
} catch (e) {
buildmessage.error("can't find npm module '" + name +
"'. Did you forget to call 'Npm.depends'?",
{ useMyCaller: true });
// recover by, uh, returning undefined, which is likely to
// have some knock-on effects
return undefined;
}
}
}
};
try {
files.runJavaScript(code.toString('utf8'), {
filename: 'package.js',
symbols: { Package: Package, Npm: Npm }
});
} catch (e) {
buildmessage.exception(e);
// Could be a syntax error or an exception. Recover by
// continuing as if package.js is empty. (Pressing on with
// whatever handlers were registered before the exception turns
// out to feel pretty disconcerting -- definitely violates the
// principle of least surprise.) Leave the metadata if we have
// it, though.
roleHandlers = {use: null, test: null};
self.pluginInfo = {};
npmDependencies = null;
}
// source files used
var sources = {use: {client: [], server: []},
test: {client: [], server: []}};
// symbols exported
var exports = {client: [], server: []};
// packages used and implied (keys are 'package', 'slice', 'unordered', and
// 'weak'). an "implied" package is a package that will be used by a slice
// which uses us. (since you can't use a test slice, only the use slice can
// have "implies".)
var uses = {use: {client: [], server: []},
test: {client: [], server: []}};
var implies = {client: [], server: []};
// For this old-style, on_use/on_test/where-based package, figure
// out its dependencies by calling its on_xxx functions and seeing
// what it does.
//
// We have a simple strategy. Call its on_xxx handler with no
// 'where', which is what happens when the package is added
// directly to an app, and see what files it adds to the client
// and the server. Call the former the client version of the
// package, and the latter the server version. Then, when a
// package is used, include it in both the client and the server
// by default. This simple strategy doesn't capture even 10% of
// the complexity possible with on_use, on_test, and where, but
// probably is sufficient for virtually all packages that actually
// exist in the field, if not every single
// one. #OldStylePackageSupport
_.each(["use", "test"], function (role) {
if (roleHandlers[role]) {
var toArray = function (x) {
if (x instanceof Array)
return x;
return x ? [x] : [];
};
var allWheres = ['client', 'server'];
var toWhereArray = function (where) {
if (!(where instanceof Array)) {
where = where ? [where] : allWheres;
}
where = _.uniq(where);
var realWhere = _.intersection(where, allWheres);
if (realWhere.length !== where.length) {
var badWheres = _.difference(where, allWheres);
// avoid using _.each so as to not add more frames to skip
for (var i = 0; i < badWheres.length; ++i) {
buildmessage.error(
"Invalid 'where' argument: '" + badWheres[i] + "'",
// skip toWhereArray in addition to the actual API function
{useMyCaller: 1});
};
// recover by using the real ones only
}
return realWhere;
};
var 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)
//
// options can include:
//
// - role: defaults to "use", but you could pass something
// like "test" if for some reason you wanted to include a
// package's tests
//
// - unordered: if true, don't require this package to load
// before us -- just require it to be loaded anytime. Also
// don't bring this package's imports into our
// namespace. If false, override a true value specified in
// a previous call to use for this package name. (A
// limitation of the current implementation is that this
// flag is not tracked per-environment or per-role.) This
// option can be used to resolve circular dependencies in
// exceptional circumstances, eg, the 'meteor' package
// depends on 'handlebars', but all packages (including
// 'handlebars') have an implicit dependency on
// 'meteor'. Internal use only -- future support of this
// is not guaranteed. #UnorderedPackageReferences
//
// - weak: if true, don't require this package to load at all, but if
// it's going to load, load it before us. Don't bring this
// package's imports into our namespace and don't allow us to use
// its plugins. (Has the same limitation as "unordered" that this
// flag is not tracked per-environment or per-role; this may
// change.)
use: function (names, where, options) {
// Support `api.use(package, {weak: true})` without where.
if (_.isObject(where) && !_.isArray(where) && !options) {
options = where;
where = null;
}
options = options || {};
names = toArray(names);
where = toWhereArray(where);
// A normal dependency creates an ordering constraint and a "if I'm
// used, use that" constraint. Unordered dependencies lack the
// former; weak dependencies lack the latter. There's no point to a
// dependency that lacks both!
if (options.unordered && options.weak) {
buildmessage.error(
"A dependency may not be both unordered and weak.",
{ useMyCaller: true });
// recover by ignoring
return;
}
_.each(names, function (name) {
_.each(where, function (w) {
if (options.role && options.role !== "use")
throw new Error("Role override is no longer supported");
uses[role][w].push(_.extend(parseSpec(name), {
unordered: options.unordered || false,
weak: options.weak || false
}));
});
});
},
// Called when this package wants packages using it to also use
// another package. eg, for umbrella packages which want packages
// using them to also get symbols or plugins from their components.
imply: function (names, where) {
if (role === "test") {
buildmessage.error(
"api.imply() is only allowed in on_use, not on_test.",
{ useMyCaller: true });
// recover by ignoring
return;
}
names = toArray(names);
where = toWhereArray(where);
_.each(names, function (name) {
_.each(where, function (w) {
// We don't allow weak or unordered implies, since the main
// purpose of imply is to provide imports and plugins.
implies[w].push(parseSpec(name));
});
});
},
// Top-level call to add a source file to a package. It will
// be processed according to its extension (eg, *.coffee
// files will be compiled to JavaScript.)
add_files: function (paths, where, fileOptions) {
paths = toArray(paths);
where = toWhereArray(where);
_.each(paths, function (path) {
_.each(where, function (w) {
var source = {relPath: path};
if (fileOptions)
source.fileOptions = fileOptions;
sources[role][w].push(source);
});
});
},
// Export symbols from this package.
//
// @param symbols String (eg "Foo") or array of String
// @param where 'client', 'server', or an array of those
// @param options 'testOnly', boolean.
export: function (symbols, where, options) {
if (role === "test") {
buildmessage.error("You cannot export symbols from a test.",
{ useMyCaller: true });
// recover by ignoring
return;
}
// Support `api.export("FooTest", {testOnly: true})` without
// where.
if (_.isObject(where) && !_.isArray(where) && !options) {
options = where;
where = null;
}
options = options || {};
symbols = toArray(symbols);
where = toWhereArray(where);
_.each(symbols, function (symbol) {
// XXX be unicode-friendlier
if (!symbol.match(/^([_$a-zA-Z][_$a-zA-Z0-9]*)$/)) {
buildmessage.error("Bad exported symbol: " + symbol,
{ useMyCaller: true });
// recover by ignoring
return;
}
_.each(where, function (w) {
exports[w].push({name: symbol, testOnly: !!options.testOnly});
});
});
},
// XXX COMPAT WITH 0.6.4
error: function () {
// I would try to support this but I don't even know what
// its signature was supposed to be anymore
buildmessage.error(
"api.error(), ironically, is no longer supported",
{ useMyCaller: true });
// recover by ignoring
},
// XXX COMPAT WITH 0.6.4
registered_extensions: function () {
buildmessage.error(
"api.registered_extensions() is no longer supported",
{ useMyCaller: true });
// recover by returning dummy value
return [];
}
};
try {
roleHandlers[role](api);
} catch (e) {
buildmessage.exception(e);
// Recover by ignoring all of the source files in the
// packages and any remaining role handlers. It violates the
// principle of least surprise to half-run a role handler
// and then continue.
sources = {use: {client: [], server: []},
test: {client: [], server: []}};
roleHandlers = {use: null, test: null};
self.pluginInfo = {};
npmDependencies = null;
}
}
});
// Grab any npm dependencies. Keep them in a cache in the package
// source directory so we don't have to do this from scratch on
// every build.
// We used to put this directly in .npm, but in linker-land, the package's
// own NPM dependencies go in .npm/package and build plugin X's goes in
// .npm/plugin/X. Notably, the former is NOT an ancestor of the latter, so
// that a build plugin does NOT see the package's node_modules.
// XXX maybe there should be separate NPM dirs for use vs test?
var packageNpmDir =
path.resolve(path.join(self.sourceRoot, '.npm', 'package'));
// If this package was previously built with pre-linker versions, it may
// have files directly inside `.npm` instead of nested inside
// `.npm/package`. Clean them up if they are there.
var preLinkerFiles = [
'npm-shrinkwrap.json', 'README', '.gitignore', 'node_modules'];
_.each(preLinkerFiles, function (f) {
files.rm_recursive(path.join(self.sourceRoot, '.npm', f));
});
// go through a specialized npm dependencies update process,
// ensuring we don't get new versions of any
// (sub)dependencies. this process also runs mostly safely
// multiple times in parallel (which could happen if you have
// two apps running locally using the same package)
// We run this even if we have no dependencies, because we might
// need to delete dependencies we used to have.
var nodeModulesPath = null;
if (meteorNpm.updateDependencies(name, packageNpmDir, npmDependencies)) {
nodeModulesPath = path.join(packageNpmDir, 'node_modules');
if (! meteorNpm.dependenciesArePortable(packageNpmDir))
isPortable = false;
}
// Create slices
var osArch = isPortable ? "os" : archinfo.host();
_.each(["use", "test"], function (role) {
_.each(["browser", osArch], function (arch) {
var where = (arch === "browser") ? "client" : "server";
// Everything depends on the package 'meteor', which sets up
// the basic environment) (except 'meteor' itself, and js-analyze
// which needs to be loaded by the linker).
// XXX add a better API for js-analyze to declare itself here
if (! (name === "meteor" && role === "use") && name !== "js-analyze") {
// Don't add the dependency if one already exists. This allows the
// package to create an unordered dependency and override the one that
// we'd add here. This is necessary to resolve the circular dependency
// between meteor and underscore (underscore has an unordered
// dependency on meteor dating from when the .js extension handler was
// in the "meteor" package.)
var alreadyDependsOnMeteor =
!! _.find(uses[role][where], function (u) {
return u.package === "meteor" && !u.slice;
});
if (! alreadyDependsOnMeteor)
uses[role][where].unshift({ package: "meteor" });
}
// Each slice has its own separate WatchSet. This is so that, eg, a test
// slice's dependencies doesn't end up getting merged into the
// pluginWatchSet of a package that uses it: only the use slice's
// dependencies need to go there!
var watchSet = new watch.WatchSet();
watchSet.addFile(packageJsPath, packageJsHash);
self.slices.push(new Slice(self, {
name: ({ use: "main", test: "tests" })[role],
arch: arch,
uses: uses[role][where],
implies: role === "use" && implies[where] || undefined,
getSourcesFunc: function () { return sources[role][where]; },
noExports: role === "test",
declaredExports: role === "use" ? exports[where] : null,
watchSet: watchSet,
nodeModulesPath: arch === osArch && nodeModulesPath || undefined
}));
});
});
// Default slices
self.defaultSlices = { browser: ['main'], 'os': ['main'] };
self.testSlices = { browser: ['tests'], 'os': ['tests'] };
},
// Initialize a package from a legacy-style application directory
// (has .meteor/packages.) This function does not retrieve the
// package's dependencies from the library, and on return, the
// package will be in an unbuilt state.
initFromAppDir: function (appDir, ignoreFiles) {
var self = this;
appDir = path.resolve(appDir);
self.name = null;
self.sourceRoot = appDir;
self.serveRoot = path.sep;
_.each(["client", "server"], function (sliceName) {
// Determine used packages
var names = project.get_packages(appDir);
var arch = sliceName === "server" ? "os" : "browser";
// Create slice
var slice = new Slice(self, {
name: sliceName,
arch: arch,
uses: _.map(names, parseSpec)
});
self.slices.push(slice);
// Watch control files for changes
// XXX this read has a race with the actual reads that are used
_.each([path.join(appDir, '.meteor', 'packages'),
path.join(appDir, '.meteor', 'release')], function (p) {
watch.readAndWatchFile(slice.watchSet, p);
});
// Determine source files
slice.getSourcesFunc = function () {
var sourceInclude = _.map(slice.registeredExtensions(), function (ext) {
return new RegExp('\\.' + quotemeta(ext) + '$');
});
var sourceExclude = [/^\./].concat(ignoreFiles);
// Wrapper around watch.readAndWatchDirectory which takes in and returns
// sourceRoot-relative directories.
var readAndWatchDirectory = function (relDir, filters) {
filters = filters || {};
var absPath = path.join(self.sourceRoot, relDir);
var contents = watch.readAndWatchDirectory(slice.watchSet, {
absPath: absPath,
include: filters.include,
exclude: filters.exclude
});
return _.map(contents, function (x) {
return path.join(relDir, x);
});
};
// Read top-level source files.
var sources = readAndWatchDirectory('', {
include: sourceInclude,
exclude: sourceExclude
});
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('', {
include: [/\/$/],
exclude: [/^packages\/$/, /^programs\/$/, /^tests\/$/,
/^public\/$/, /^private\/$/,
otherSliceRegExp].concat(sourceExclude)
});
checkForInfiniteRecursion('');
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,
exclude: sourceExclude
}));
// Find sub-sourceDirectories. Note that we DON'T need to ignore the
// directory names that are only special at the top level.
Array.prototype.push.apply(sourceDirectories, readAndWatchDirectory(dir, {
include: [/\/$/],
exclude: [/^tests\/$/, otherSliceRegExp].concat(sourceExclude)
}));
}
// We've found all the source files. Sort them!
sources.sort(files.sort);
// Convert into relPath/fileOptions objects.
sources = _.map(sources, function (relPath) {
var sourceObj = {relPath: relPath};
// Special case: on the client, JavaScript files in a
// `client/compatibility` directory don't get wrapped in a closure.
if (sliceName === "client" && relPath.match(/\.js$/)) {
var clientCompatSubstr =
path.sep + 'client' + path.sep + 'compatibility' + path.sep;
if ((path.sep + relPath).indexOf(clientCompatSubstr) !== -1)
sourceObj.fileOptions = {bare: true};
}
return sourceObj;
});
// Now look for assets for this slice.
var assetDir = sliceName === "client" ? "public" : "private";
var assetDirs = readAndWatchDirectory('', {
include: [new RegExp('^' + assetDir + '/$')]
});
if (!_.isEmpty(assetDirs)) {
if (!_.isEqual(assetDirs, [assetDir + '/']))
throw new Error("Surprising assetDirs: " + JSON.stringify(assetDirs));
while (!_.isEmpty(assetDirs)) {
dir = assetDirs.shift();
// 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: [/.?/],
// we DO look under dot directories here
exclude: ignoreFiles
});
_.each(assetsAndSubdirs, function (item) {
if (item[item.length - 1] === '/') {
// Recurse on this directory.
assetDirs.push(item);
} else {
// This file is an asset.
sources.push({
relPath: item,
fileOptions: {
isAsset: true
}
});
}
});
}
}
return sources;
};
});
self.defaultSlices = { browser: ['client'], 'os': ['server'] };
},
// Initialize a package from a prebuilt Unipackage on disk. On
// return, the package will be a built state. This function does not
// retrieve the package's dependencies from the library (it is not
// necessary.)
//
// options:
// - onlyIfUpToDate: if true, then first check the unipackage's
// dependencies (if present) to see if it's up to date. If not,
// return false without loading the package. Otherwise return
// true. (If onlyIfUpToDate is not passed, always return true.)
// - buildOfPath: If present, the source directory (as an absolute
// path on local disk) of which we think this unipackage is a
// build. If it's not (it was copied from somewhere else), we
// consider it not up to date (in the sense of onlyIfUpToDate) so
// that we can rebuild it and correct the absolute paths in the
// dependency information.
initFromUnipackage: function (name, dir, options) {
var self = this;
options = options || {};
var mainJson =
JSON.parse(fs.readFileSync(path.join(dir, 'unipackage.json')));
if (mainJson.format !== "unipackage-pre1")
throw new Error("Unsupported unipackage format: " +
JSON.stringify(mainJson.format));
var buildInfoPath = path.join(dir, 'buildinfo.json');
var buildInfoJson = fs.existsSync(buildInfoPath) ?
JSON.parse(fs.readFileSync(buildInfoPath)) : {};
// XXX should comprehensively sanitize (eg, typecheck) everything
// read from json files
// Read the watch sets for each slice; keep them separate (for passing to
// the Slice constructor below) as well as merging them into one big
// WatchSet.
var mergedWatchSet = new watch.WatchSet();
var sliceWatchSets = {};
_.each(buildInfoJson.sliceDependencies, function (watchSetJSON, sliceTag) {
var watchSet = watch.WatchSet.fromJSON(watchSetJSON);
mergedWatchSet.merge(watchSet);
sliceWatchSets[sliceTag] = watchSet;
});
// We do NOT put this (or anything!) onto self until we've passed the
// onlyIfUpToDate check.
var pluginWatchSet = watch.WatchSet.fromJSON(
buildInfoJson.pluginDependencies);
// This might be redundant (since pluginWatchSet was probably merged into
// each slice watchSet when it was built) but shouldn't hurt.
mergedWatchSet.merge(pluginWatchSet);
var pluginProviderPackageDirs = buildInfoJson.pluginProviderPackages || {};
// If we're supposed to check the dependencies, go ahead and do so
if (options.onlyIfUpToDate) {
// Do we think we'll generate different contents than the tool that built
// this package?
if (buildInfoJson.builtBy !== exports.BUILT_BY)
return false;
if (options.buildOfPath &&
(buildInfoJson.source !== options.buildOfPath)) {
// This catches the case where you copy a source tree that had
// a .build directory and then modify a file. Without this
// check you won't see a rebuild (even if you stop and restart
// meteor), at least not until you modify the *original*
// copies of the source files, because that is still where all
// of the dependency info points.
return false;
}
if (! self.checkUpToDate(mergedWatchSet, pluginProviderPackageDirs))
return false;
}
self.name = name;
self.metadata = {
summary: mainJson.summary,
internal: mainJson.internal
};
self.defaultSlices = mainJson.defaultSlices;
self.testSlices = mainJson.testSlices;
self.pluginWatchSet = pluginWatchSet;
self.pluginProviderPackageDirs = pluginProviderPackageDirs;
_.each(mainJson.plugins, function (pluginMeta) {
rejectBadPath(pluginMeta.path);
var plugin = bundler.readJsImage(path.join(dir, pluginMeta.path));
if (! archinfo.matches(archinfo.host(), plugin.arch)) {
buildmessage.error("package `" + name + "` is built for incompatible " +
"architecture: " + plugin.arch);
// Recover by ignoring plugin
return;
}
// XXX should refactor so that we can have plugins of multiple
// different arches happily coexisting in memory, to match
// slices. If this becomes a problem before we have a chance to
// refactor, could just ignore plugins for arches that we don't
// support, if we are careful to not then try to write out the
// package and expect them to be intact..
if (pluginMeta.name in self.plugins)
throw new Error("Implementation limitation: this program " +
"cannot yet handle fat plugins, sorry");
self.plugins[pluginMeta.name] = plugin;
});
self.pluginsBuilt = true;
_.each(mainJson.slices, function (sliceMeta) {
// aggressively sanitize path (don't let it escape to parent
// directory)
rejectBadPath(sliceMeta.path);
var sliceJson = JSON.parse(
fs.readFileSync(path.join(dir, sliceMeta.path)));
var sliceBasePath = path.dirname(path.join(dir, sliceMeta.path));
if (sliceJson.format!== "unipackage-slice-pre1")
throw new Error("Unsupported unipackage slice format: " +
JSON.stringify(sliceJson.format));
var nodeModulesPath = null;
if (sliceJson.node_modules) {
rejectBadPath(sliceJson.node_modules);
nodeModulesPath = path.join(sliceBasePath, sliceJson.node_modules);
}
var slice = new Slice(self, {
name: sliceMeta.name,
arch: sliceMeta.arch,
watchSet: sliceWatchSets[sliceMeta.path],
nodeModulesPath: nodeModulesPath,
uses: sliceJson.uses,
implies: sliceJson.implies
});
slice.isBuilt = true;
slice.noExports = !!sliceJson.noExports;
slice.packageVariables = sliceJson.packageVariables || [];
slice.prelinkFiles = [];
slice.resources = [];
_.each(sliceJson.resources, function (resource) {
rejectBadPath(resource.file);
var data = new Buffer(resource.length);
// Read the data from disk, if it is non-empty. Avoid doing IO for empty
// files, because (a) unnecessary and (b) fs.readSync with length 0
// throws instead of acting like POSIX read:
// https://github.com/joyent/node/issues/5685
if (resource.length > 0) {
var fd = fs.openSync(path.join(sliceBasePath, resource.file), "r");
try {
var count = fs.readSync(
fd, data, 0, resource.length, resource.offset);
} finally {
fs.closeSync(fd);
}
if (count !== resource.length)
throw new Error("couldn't read entire resource");
}
if (resource.type === "prelink") {
var prelinkFile = {
source: data.toString('utf8'),
servePath: resource.servePath
};
if (resource.sourceMap) {
rejectBadPath(resource.sourceMap);
prelinkFile.sourceMap = fs.readFileSync(
path.join(sliceBasePath, resource.sourceMap), 'utf8');
}
slice.prelinkFiles.push(prelinkFile);
} else if (_.contains(["head", "body", "css", "js", "asset"],
resource.type)) {
slice.resources.push({
type: resource.type,
data: data,
servePath: resource.servePath || undefined,
path: resource.path || undefined
});
} else
throw new Error("bad resource type in unipackage: " +
JSON.stringify(resource.type));
});
self.slices.push(slice);
});
self.slicesBuilt = true;
return true;
},
// Try to check if this package is up-to-date (that is, whether its source
// files have been modified.) True if we have dependency info and it says that
// the package is up-to-date. False if a source file has changed.
//
// The arguments _watchSet and _pluginProviderPackageDirs are used when
// reading from disk when there are no slices yet; don't pass them from
// outside this file.
checkUpToDate: function (_watchSet, _pluginProviderPackageDirs) {
var self = this;
if (!_watchSet) {
// This call was on an already-fully-loaded Package and we just want to
// see if it's changed. So we have some watchSets inside ourselves.
_watchSet = new watch.WatchSet();
_watchSet.merge(self.pluginWatchSet);
_.each(self.slices, function (slice) {
_watchSet.merge(slice.watchSet);
});
}
if (!_pluginProviderPackageDirs) {
_pluginProviderPackageDirs = self.pluginProviderPackageDirs;
}
// Are all of the packages we directly use (which can provide plugins which
// affect compilation) resolving to the same directory? (eg, have we updated
// our release version to something with a new version of a package?)
var packageResolutionsSame = _.all(
_pluginProviderPackageDirs, function (packageDir, name) {
return self.library.findPackageDirectory(name) === packageDir;
});
if (!packageResolutionsSame)
return false;
return watch.isUpToDate(_watchSet);
},
// True if this package can be saved as a unipackage
canBeSavedAsUnipackage: function () {
var self = this;
return true;
},
// options:
//
// - buildOfPath: Optional. The absolute path on local disk of the
// directory that was built to produce this package. Used as part
// of the dependency info to detect builds that were moved and
// then modified.
saveAsUnipackage: function (outputPath, options) {
var self = this;
if (!self.pluginsBuilt || !self.slicesBuilt)
throw new Error("Unbuilt packages cannot be saved");
if (! self.canBeSavedAsUnipackage())
throw new Error("This package can not yet be saved as a unipackage");
var builder = new Builder({ outputPath: outputPath });
try {
var mainJson = {
format: "unipackage-pre1",
summary: self.metadata.summary,
internal: self.metadata.internal,
slices: [],
defaultSlices: self.defaultSlices,
testSlices: self.testSlices,
plugins: []
};
// Note: The contents of buildInfoJson (with the root directory of the
// Meteor checkout naively deleted) gets its SHA taken to determine the
// built package's warehouse version. So it should not contain
// platform-dependent data and should contain all sources of change to the
// unipackage's output. See scripts/admin/build-package-tarballs.sh.
var buildInfoJson = {
builtBy: exports.BUILT_BY,
sliceDependencies: { },
pluginDependencies: self.pluginWatchSet.toJSON(),
pluginProviderPackages: self.pluginProviderPackageDirs,
source: options.buildOfPath || undefined
};
builder.reserve("unipackage.json");
builder.reserve("buildinfo.json");
// These is where we put the NPM dependencies of the slices (but not of
// plugins). The node_modules directory is nested inside "npm" so that it
// is not visible from within plugins.
builder.reserve("npm/node_modules", { directory: true });
builder.reserve("head");
builder.reserve("body");
// Pre-linker versions of Meteor expect all packages in the warehouse to
// contain a file called "package.js"; they use this as part of deciding
// whether or not they need to download a new package. Because packages
// are downloaded by the *existing* version of the tools, we need to
// include this file until we're comfortable breaking "meteor update" from
// 0.6.4. (Specifically, warehouse.packageExistsInWarehouse used to check
// to see if package.js exists instead of just looking for the package
// directory.)
// XXX Remove this once we can.
builder.write("package.js", {
data: new Buffer(
("// This file is included for compatibility with the Meteor " +
"0.6.4 package downloader.\n"),
"utf8")
});
// Slices
_.each(self.slices, function (slice) {
if (! slice.isBuilt)
throw new Error("saving unbuilt slice?");
// Make up a filename for this slice
var baseSliceName =
(slice.sliceName === "main" ? "" : (slice.sliceName + ".")) +
slice.arch;
var sliceDir =
builder.generateFilename(baseSliceName, { directory: true });
var sliceJsonFile =
builder.generateFilename(baseSliceName + ".json");
mainJson.slices.push({
name: slice.sliceName,
arch: slice.arch,
path: sliceJsonFile
});
// Save slice dependencies. Keyed by the json path rather than thinking
// too hard about how to encode pair (name, arch).
buildInfoJson.sliceDependencies[sliceJsonFile] =
slice.watchSet.toJSON();
// Construct slice metadata
var sliceJson = {
format: "unipackage-slice-pre1",
noExports: slice.noExports || undefined,
packageVariables: slice.packageVariables,
uses: _.map(slice.uses, function (u) {
return {
'package': u.package,
slice: u.slice || undefined,
// For cosmetic value, leave false values for these options out of
// the JSON file.
unordered: u.unordered || undefined,
weak: u.weak || undefined
};
}),
implies: (_.isEmpty(slice.implies) ? undefined : slice.implies),
node_modules: slice.nodeModulesPath ? 'npm/node_modules' : undefined,
resources: []
};
// Output 'head', 'body' resources nicely
var concat = {head: [], body: []};
var offset = {head: 0, body: 0};
_.each(slice.resources, function (resource) {
if (_.contains(["head", "body"], resource.type)) {
if (concat[resource.type].length) {
concat[resource.type].push(new Buffer("\n", "utf8"));
offset[resource.type]++;
}
if (! (resource.data instanceof Buffer))
throw new Error("Resource data must be a Buffer");
sliceJson.resources.push({
type: resource.type,
file: path.join(sliceDir, resource.type),
length: resource.data.length,
offset: offset[resource.type]
});
concat[resource.type].push(resource.data);
offset[resource.type] += resource.data.length;
}
});
_.each(concat, function (parts, type) {
if (parts.length) {
builder.write(path.join(sliceDir, type), {
data: Buffer.concat(concat[type], offset[type])
});
}
});
// Output other resources each to their own file
_.each(slice.resources, function (resource) {
if (_.contains(["head", "body"], resource.type))
return; // already did this one
sliceJson.resources.push({
type: resource.type,
file: builder.writeToGeneratedFilename(
path.join(sliceDir, resource.servePath),
{ data: resource.data }),
length: resource.data.length,
offset: 0,
servePath: resource.servePath || undefined,
path: resource.path || undefined
});
});
// Output prelink resources
_.each(slice.prelinkFiles, function (file) {
var data = new Buffer(file.source, 'utf8');
var resource = {
type: 'prelink',
file: builder.writeToGeneratedFilename(
path.join(sliceDir, file.servePath),
{ data: data }),
length: data.length,
offset: 0,
servePath: file.servePath || undefined
};
if (file.sourceMap) {
// Write the source map.
resource.sourceMap = builder.writeToGeneratedFilename(
path.join(sliceDir, file.servePath + '.map'),
{ data: new Buffer(file.sourceMap, 'utf8') }
);
}
sliceJson.resources.push(resource);
});
// If slice has included node_modules, copy them in
if (slice.nodeModulesPath) {
builder.copyDirectory({
from: slice.nodeModulesPath,
to: 'npm/node_modules'
});
}
// Control file for slice
builder.writeJson(sliceJsonFile, sliceJson);
});
// Plugins
_.each(self.plugins, function (plugin, name) {
var pluginDir =
builder.generateFilename('plugin.' + name + '.' + plugin.arch,
{ directory: true });
var relPath = plugin.write(builder.enter(pluginDir));
mainJson.plugins.push({
name: name,
arch: plugin.arch,
path: path.join(pluginDir, relPath)
});
});
builder.writeJson("unipackage.json", mainJson);
builder.writeJson("buildinfo.json", buildInfoJson);
builder.complete();
} catch (e) {
builder.abort();
throw e;
}
}
});
var packages = exports;
_.extend(exports, {
Package: Package,
parseSpec: parseSpec
});