var fs = require("fs"); var path = require("path"); var files = require("./files.js"); var archinfo = require("./archinfo.js"); var compiler = require("./compiler.js"); var buildmessage = require("./buildmessage.js"); var PackageSource = require("./package-source.js"); var _ = require('underscore'); var unipackage = require("./unipackage.js"); var packageCache = exports; var PackageCache = function () { var self = this; // both map from package load path to: // - pkg: cached Unipackage object // - sourceDir: directory that contained its source code, or null // - buildDir: directory from which the built package was loaded self.softReloadCache = {}; self.loadedPackages = {}; }; _.extend(PackageCache.prototype, { // Force reload of changed packages. See description at loadPackageAtPath(). // // If soft is false, the default, the cache is totally flushed and // all packages are reloaded unconditionally. // // If soft is true, then built packages without dependency info (such as those // from the warehouse) aren't reloaded (there's no way to rebuild them, after // all), and if we loaded a built package with dependency info, we won't // reload it if the dependency info says that its source files are still up to // date. The ideas is that assuming the user is "following the rules", this // will correctly reload any changed packages while in most cases avoiding // nearly all reloading. refresh: function (soft) { var self = this; soft = soft || false; self.softReloadCache = soft ? self.loadedPackages : {}; self.loadedPackages = {}; }, // Adds a prebuilt package to the package cache. // // - name: package name // - loadPath: path of the source to the package // - unipackage (prebuilt package) cachePackageAtPath : function (name, loadPath, unip) { var self = this; var key = name + "@" + loadPath; var buildDir = path.join(loadPath, '.build.'+ name); self.loadedPackages[key] = { pkg: unip, sourceDir: loadPath, buildDir: buildDir }; }, // Given a path to a package on disk, retrieve a Package // object. // // loadPackageAtPath() caches the packages it returns, meaning if // you call loadPackageAtPath('/foo/bar') and later /foo/bar changes // on disk, you won't see the changes. To flush the package cache // and force all of the packages to be reloaded the next time // loadPackageAtPath() is called for them, see refresh(). loadPackageAtPath: function (name, loadPath) { var self = this; // We need to build and load both the test and normal package, which, // frequently means 2 packages per directory/loadPath. Rather than // special-casing it with some sort of a test flag, we can just // differentiate by name (and extend the interface if it ever comes up). var key = name + "@" + loadPath; if (_.has(self.loadedPackages, key)) { return self.loadedPackages[key].pkg; } // See if we can reuse a package that we have cached from before // the last soft refresh. // XXX XXX this is not very efficient. refactor if (_.has(self.softReloadCache, key)) { var entry = self.softReloadCache[key]; // Either we will decide that the cache is invalid, or we will "upgrade" // this entry into loadedPackages. Either way, it's not needed in // softReloadCache any more. delete self.softReloadCache[key]; var isUpToDate; var unip; if (fs.existsSync(path.join(loadPath, 'unipackage.json'))) { // We don't even have the source to this package, so it must // be up to date. isUpToDate = true; } else { buildmessage.enterJob({ title: "initializing package `" + name + "`", rootPath: loadPath }, function () { var packageSource = new PackageSource; packageSource.initFromPackageDir(name, loadPath); unip = new unipackage.Unipackage; unip.initFromPath(name, entry.buildDir); isUpToDate = compiler.checkUpToDate(packageSource, entry.pkg); }); } if (isUpToDate) { // Cache it self.loadedPackages[key] = entry; return entry.pkg; } } // Load package from disk // Does loadPath point directly at a unipackage (rather than a // source tree?) if (fs.existsSync(path.join(loadPath, 'unipackage.json'))) { unip = new unipackage.Unipackage; unip.initFromPath(name, loadPath); self.loadedPackages[key] = { pkg: unip, sourceDir: null, buildDir: loadPath }; return unip; }; // It's a source tree. Load it. var packageSource = new PackageSource; buildmessage.enterJob({ title: "initializing package `" + name + "`", rootPath: loadPath }, function () { packageSource.initFromPackageDir(name, loadPath); }); // Does it have an up-to-date build? var buildDir = path.join(loadPath, '.build.'+ name); if (fs.existsSync(buildDir)) { unip = new unipackage.Unipackage; var maybeUpToDate = true; try { unip.initFromPath(name, buildDir); } catch (e) { if (!(e instanceof unipackage.OldUnipackageFormatError)) throw e; maybeUpToDate = false; } if (maybeUpToDate && compiler.checkUpToDate(packageSource, unip)) { self.loadedPackages[key] = { pkg: unip, sourceDir: loadPath, buildDir: buildDir }; return unip; } } // Either we didn't have a build, or it was out of date, or the // caller wanted us to rebuild no matter what. Build the package. return buildmessage.enterJob({ title: "building package `" + name + "`", rootPath: loadPath }, function () { // We used to take great care to first put a // loaded-but-not-built package object (the equivalent of a // PackageSource) into self.loadedPackages before calling // build() as a hacky way of dealing with build-time // dependencies. // // We don't do that anymore and at the moment, we rely on catalog to // initalize ahead of us and swoop in and build all of the local packages // informed by a topological sort var unip = compiler.compile(packageSource).unipackage; self.loadedPackages[key] = { pkg: unip, sourceDir: null, buildDir: buildDir }; if (! buildmessage.jobHasMessages()) { // Save it, for a fast load next time try { files.addToGitignore(loadPath, '.build*'); unip.saveToPath(buildDir, { buildOfPath: loadPath }); } catch (e) { // If we can't write to this directory, we don't get to cache our // output, but otherwise life is good. if (!(e && (e.code === 'EACCES' || e.code === 'EPERM'))) throw e; } } return unip; }); }, // Get a package that represents an app. (ignoreFiles is optional // and if given, it should be an array of regexps for filenames to // ignore when scanning for source files.) loadAppAtPath: function (appDir, ignoreFiles) { var self = this; var packageSource = new PackageSource; packageSource.initFromAppDir(appDir, ignoreFiles); return compiler.compile(packageSource).unipackage; } }); packageCache.packageCache = new PackageCache;