mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
303 lines
11 KiB
JavaScript
303 lines
11 KiB
JavaScript
var _ = require('underscore');
|
|
var bundler = require('./bundler.js');
|
|
var Builder = require('./builder.js');
|
|
var buildmessage = require('./buildmessage.js');
|
|
var files = require('./files.js');
|
|
var compiler = require('./compiler.js');
|
|
var config = require('./config.js');
|
|
var watch = require('./watch.js');
|
|
var Console = require('./console.js').Console;
|
|
var isopackCacheModule = require('./isopack-cache.js');
|
|
var packageMapModule = require('./package-map.js');
|
|
var fiberHelpers = require('./fiber-helpers.js');
|
|
|
|
// TL;DR: Isopacket is a set of isopacks. Isopackets are used only inside
|
|
// meteor-tool.
|
|
|
|
// An isopacket is a predefined set of isopackages which the meteor command-line
|
|
// tool can load into its process. This is how we use the DDP client and many
|
|
// other packages inside the tool. The isopackets are listed below in the
|
|
// ISOPACKETS constant.
|
|
//
|
|
// All packages that are in isopackets and all of their transitive dependencies
|
|
// must be part of the core Meteor git checkout (not loaded from troposphere).
|
|
//
|
|
// The requested packages will be loaded together with all of their
|
|
// dependencies. If you request to load the same isopacket more than once, you
|
|
// will efficiently get the same pre-loaded isopacket. On the other hand, two
|
|
// different loaded isopackets contain distinct copies of all of their packages
|
|
// copy of all of the packages. The return value is an object that maps package
|
|
// name to package exports (that is, it is the Package object from inside the
|
|
// sandbox created for the newly loaded packages).
|
|
//
|
|
// For built releases, all of the isopackets are pre-compiled (as JsImages,
|
|
// similar to a plugin or a server program) into the tool.
|
|
//
|
|
// When run from a checkout, all isopackets are re-compiled early in the startup
|
|
// process if any of their sources have changed.
|
|
//
|
|
// Example usage:
|
|
// var DDP = require('./isopackets.js').load('ddp').ddp.DDP;
|
|
// var reverse = DDP.connect('reverse.meteor.com');
|
|
// Console.info(reverse.call('reverse', 'hello world'));
|
|
|
|
|
|
// All of the defined isopackets. Whenever they are being built, they will be
|
|
// built in the order listed here (which is mostly relevant for js-analyze).
|
|
var ISOPACKETS = {
|
|
// Note: when running from a checkout, js-analyze must always be the
|
|
// the first to be rebuilt, because it might need to be loaded as part
|
|
// of building other isopackets.
|
|
'js-analyze': ['js-analyze'],
|
|
'ddp': ['ddp'],
|
|
'mongo': ['mongo'],
|
|
'ejson': ['ejson'],
|
|
'minifiers': ['minifiers'],
|
|
'constraint-solver': ['constraint-solver'],
|
|
'cordova-support': ['boilerplate-generator', 'logging', 'webapp-hashing',
|
|
'xmlbuilder'],
|
|
'logging': ['logging']
|
|
};
|
|
|
|
// Caches isopackets in memory (each isopacket only needs to be loaded
|
|
// once). This is a map from isopacket name to either:
|
|
//
|
|
// - The 'Package' dictionary, if the isopacket has already been loaded
|
|
// into memory
|
|
// - null, if the isopacket hasn't been loaded into memory but its on-disk
|
|
// instance is known to be ready
|
|
//
|
|
// The subtlety here is that when running from a checkout, we don't want to
|
|
// accidentally load an isopacket before ensuring that it doesn't need to be
|
|
// rebuilt. But we do want to be able to load the js-analyze isopacket as part
|
|
// of building other isopackets in ensureIsopacketsLoadable.
|
|
var loadedIsopackets = {};
|
|
|
|
// The main entry point: loads and returns an isopacket from cache or from
|
|
// disk. Does not do a build step: ensureIsopacketsLoadable must be called
|
|
// first!
|
|
var load = function (isopacketName) {
|
|
return fiberHelpers.noYieldsAllowed(function () {
|
|
if (_.has(loadedIsopackets, isopacketName)) {
|
|
if (loadedIsopackets[isopacketName]) {
|
|
return loadedIsopackets[isopacketName];
|
|
}
|
|
|
|
// This is the case where the isopacket is up to date on disk but not
|
|
// loaded.
|
|
var isopacket = loadIsopacketFromDisk(isopacketName);
|
|
loadedIsopackets[isopacketName] = isopacket;
|
|
return isopacket;
|
|
}
|
|
|
|
if (_.has(ISOPACKETS, isopacketName)) {
|
|
throw Error("Can't load isopacket before it has been verified: "
|
|
+ isopacketName);
|
|
}
|
|
|
|
throw Error("Unknown isopacket: " + isopacketName);
|
|
});
|
|
};
|
|
|
|
var isopacketPath = function (isopacketName) {
|
|
return files.pathJoin(config.getIsopacketRoot(), isopacketName);
|
|
};
|
|
|
|
// ensureIsopacketsLoadable is called at startup and ensures that all isopackets
|
|
// exist on disk as up-to-date loadable programs.
|
|
var calledEnsure = false;
|
|
var ensureIsopacketsLoadable = function () {
|
|
if (calledEnsure) {
|
|
throw Error("can't ensureIsopacketsLoadable twice!");
|
|
}
|
|
calledEnsure = true;
|
|
|
|
// If we're not running from checkout, then there's nothing to build and we
|
|
// can declare that all isopackets are loadable.
|
|
if (! files.inCheckout()) {
|
|
_.each(ISOPACKETS, function (packages, name) {
|
|
loadedIsopackets[name] = null;
|
|
});
|
|
return;
|
|
}
|
|
|
|
// We make this object lazily later.
|
|
var isopacketBuildContext = null;
|
|
|
|
var failedPackageBuild = false;
|
|
// Look at each isopacket. Check to see if it's on disk and up to date. If
|
|
// not, build it. We rebuild them in the order listed in ISOPACKETS, which
|
|
// ensures that we deal with js-analyze first.
|
|
var messages = Console.withProgressDisplayVisible(function () {
|
|
return buildmessage.capture(function () {
|
|
_.each(ISOPACKETS, function (packages, isopacketName) {
|
|
if (failedPackageBuild)
|
|
return;
|
|
|
|
var isopacketRoot = isopacketPath(isopacketName);
|
|
var existingBuildinfo = files.readJSONOrNull(
|
|
files.pathJoin(isopacketRoot, 'isopacket-buildinfo.json'));
|
|
var needRebuild = ! existingBuildinfo;
|
|
if (! needRebuild && existingBuildinfo.builtBy !== compiler.BUILT_BY) {
|
|
needRebuild = true;
|
|
}
|
|
if (! needRebuild) {
|
|
var watchSet = watch.WatchSet.fromJSON(existingBuildinfo.watchSet);
|
|
if (! watch.isUpToDate(watchSet)) {
|
|
needRebuild = true;
|
|
}
|
|
}
|
|
if (! needRebuild) {
|
|
// Great, it's loadable without a rebuild.
|
|
loadedIsopackets[isopacketName] = null;
|
|
return;
|
|
}
|
|
|
|
// We're going to need to build! Make a catalog and loader if we haven't
|
|
// yet.
|
|
if (! isopacketBuildContext) {
|
|
isopacketBuildContext = makeIsopacketBuildContext();
|
|
}
|
|
|
|
buildmessage.enterJob({
|
|
title: "bundling " + isopacketName + " packages for the tool"
|
|
}, function () {
|
|
// Build the packages into the in-memory IsopackCache.
|
|
isopacketBuildContext.isopackCache.buildLocalPackages(packages);
|
|
if (buildmessage.jobHasMessages())
|
|
return;
|
|
|
|
// Now bundle them into a program.
|
|
var built = bundler.buildJsImage({
|
|
name: "isopacket-" + isopacketName,
|
|
packageMap: isopacketBuildContext.packageMap,
|
|
isopackCache: isopacketBuildContext.isopackCache,
|
|
use: packages
|
|
});
|
|
if (buildmessage.jobHasMessages())
|
|
return;
|
|
|
|
var builder = new Builder({outputPath: isopacketRoot});
|
|
builder.writeJson('isopacket-buildinfo.json', {
|
|
builtBy: compiler.BUILT_BY,
|
|
watchSet: built.watchSet.toJSON()
|
|
});
|
|
built.image.write(builder);
|
|
builder.complete();
|
|
// It's loadable now.
|
|
loadedIsopackets[isopacketName] = null;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// This is a build step ... but it's one that only happens in development, so
|
|
// it can just crash the app instead of being handled nicely.
|
|
if (messages.hasMessages()) {
|
|
Console.error("Errors prevented isopacket build:");
|
|
Console.printMessages(messages);
|
|
throw new Error("isopacket build failed?");
|
|
}
|
|
};
|
|
|
|
// Returns a new all-local-packages catalog to be used for building isopackets.
|
|
var newIsopacketBuildingCatalog = function () {
|
|
if (! files.inCheckout())
|
|
throw Error("No need to build isopackets unless in checkout!");
|
|
|
|
var catalogLocal = require('./catalog-local.js');
|
|
var isopacketCatalog = new catalogLocal.LocalCatalog;
|
|
var messages = buildmessage.capture(
|
|
{ title: "scanning local core packages" },
|
|
function () {
|
|
// When running from a checkout, isopacket building does use local
|
|
// packages, but *ONLY THOSE FROM THE CHECKOUT*: not app packages or
|
|
// $PACKAGE_DIRS packages. One side effect of this: we really really
|
|
// expect them to all build, and we're fine with dying if they don't
|
|
// (there's no worries about needing to springboard).
|
|
isopacketCatalog.initialize({
|
|
localPackageSearchDirs: [files.pathJoin(
|
|
files.getCurrentToolsDir(), 'packages')],
|
|
buildingIsopackets: true
|
|
});
|
|
});
|
|
if (messages.hasMessages()) {
|
|
Console.arrowError("Errors while scanning core packages:");
|
|
Console.printMessages(messages);
|
|
throw new Error("isopacket scan failed?");
|
|
}
|
|
return isopacketCatalog;
|
|
};
|
|
|
|
var makeIsopacketBuildContext = function () {
|
|
var context = {};
|
|
var catalog = newIsopacketBuildingCatalog();
|
|
var versions = {};
|
|
_.each(catalog.getAllPackageNames(), function (packageName) {
|
|
versions[packageName] = catalog.getLatestVersion(packageName).version;
|
|
});
|
|
context.packageMap = new packageMapModule.PackageMap(versions, {
|
|
localCatalog: catalog
|
|
});
|
|
// Make an isopack cache that doesn't save isopacks to disk and has no
|
|
// access to versioned packages.
|
|
context.isopackCache = new isopackCacheModule.IsopackCache({
|
|
packageMap: context.packageMap,
|
|
includeCordovaUnibuild: false,
|
|
// When linking JS files, don't include the padding spaces and line number
|
|
// comments. Since isopackets are loaded as part of possibly very short
|
|
// 'meteor' tool command invocations, we care more about startup time than
|
|
// legibility, and the difference is actually observable (eg 25% speedup
|
|
// loading constraint-solver).
|
|
noLineNumbers: true
|
|
});
|
|
return context;
|
|
};
|
|
|
|
// Loads a built isopacket from disk. Always loads (the cache is in 'load', not
|
|
// this function). Does not run a build process; it must already be built.
|
|
var loadIsopacketFromDisk = function (isopacketName) {
|
|
var image = bundler.readJsImage(
|
|
files.pathJoin(isopacketPath(isopacketName), 'program.json'));
|
|
|
|
// An incredibly minimalist version of the environment from
|
|
// tools/server/boot.js. Kind of a hack.
|
|
var env = {
|
|
__meteor_bootstrap__: { startupHooks: [] },
|
|
__meteor_runtime_config__: { meteorRelease: "ISOPACKET" }
|
|
};
|
|
|
|
var ret;
|
|
var messages = buildmessage.capture({
|
|
title: "loading isopacket `" + isopacketName + "`"
|
|
}, function () {
|
|
ret = image.load(env);
|
|
});
|
|
|
|
// This is a build step ... but it's one that only happens in development, so
|
|
// it can just crash the app instead of being handled nicely.
|
|
if (messages.hasMessages()) {
|
|
Console.error("Errors prevented isopacket load:");
|
|
Console.printMessages(messages);
|
|
throw new Error("isopacket load failed?");
|
|
}
|
|
|
|
// Run any user startup hooks.
|
|
while (env.__meteor_bootstrap__.startupHooks.length) {
|
|
var hook = env.__meteor_bootstrap__.startupHooks.shift();
|
|
hook();
|
|
}
|
|
// Setting this to null tells Meteor.startup to call hooks immediately.
|
|
env.__meteor_bootstrap__.startupHooks = null;
|
|
|
|
return ret;
|
|
};
|
|
|
|
var isopackets = exports;
|
|
_.extend(exports, {
|
|
load: load,
|
|
ensureIsopacketsLoadable: ensureIsopacketsLoadable,
|
|
ISOPACKETS: ISOPACKETS,
|
|
makeIsopacketBuildContext: makeIsopacketBuildContext
|
|
});
|