UNPKG

9.79 kBJavaScriptView Raw
1var path = require('path');
2var fs = require('fs');
3var _ = require('underscore');
4var Dependencies = require('./dependencies/dependencies');
5var Config = require('./config');
6var Meteor = require('./meteor');
7var Command = require('./command');
8var wrench = require('wrench');
9var exec = require('child_process').exec;
10
11// The project is the current directory's personal version of meteor,
12// complete with its own set of packages.
13// it installs into ./meteor/meteorite
14
15Project = function(root, meteorArgs) {
16
17 // Figure out all the paths we'll need to know
18 this.root = root;
19 this.meteorArgs = meteorArgs;
20 this.smartJsonPath = path.join(this.root, 'smart.json');
21 this.smartLockPath = path.join(this.root, 'smart.lock');
22 this.packagesRoot = path.join(this.root, 'packages');
23
24 // set a base meteor if it's specified in the args (or a default one if not)
25 this.meteor = new Meteor(meteorArgs);
26};
27
28// read the config from smart.lock if it exists
29Project.prototype.initFromLock = function() {
30 var self = this;
31
32 if (fs.existsSync(this.smartLockPath)) {
33 var data = fs.readFileSync(this.smartLockPath).toString();
34 var lockData = JSON.parse(data);
35
36 // embed the root path in all this data
37 lockData.meteor.root = self.root;
38 _.each(lockData.dependencies.basePackages, function(pkg) {
39 pkg.root = self.root;
40 });
41 _.each(lockData.dependencies.packages, function(pkg) {
42 pkg.root = self.root;
43 });
44
45 this.meteor = new Meteor(lockData.meteor);
46 this.dependencies = Dependencies.newFromLockJson(lockData.dependencies);
47 }
48};
49
50// have a look in smart.json, see if it's different to what we have from smart.lock
51Project.prototype.checkSmartJson = function(forceUpdate) {
52
53 // this is the config specified in smart.json
54 var config = new Config(this.root);
55
56 var newMeteor = new Meteor(config.meteor);
57 // when running in the context of a package, we are the only required package
58 if (config.name) {
59 var specifier = {};
60 specifier[config.name] = {path: "."};
61 var newDeps = new Dependencies(specifier);
62 } else {
63 var newDeps = new Dependencies(config.packages);
64 }
65
66 if (forceUpdate || !this.meteor.equals(newMeteor) || !this.dependencies || !this.dependencies.equalBase(newDeps)) {
67
68 if (!forceUpdate && this.dependencies)
69 console.log('smart.json changed.. installing from smart.json');
70
71 this.lockChanged = true;
72 this.meteor = newMeteor;
73 this.dependencies = newDeps;
74 }
75};
76
77// FIXME - this doesn't actually fetch the packages.
78// we only fetch them when we are about to install them, or we need to
79// take a look inside them. I think this is reasonable.
80Project.prototype.fetch = function(fn, forceUpdate) {
81 var self = this;
82
83 // prepare dependencies and meteor
84 self.initFromLock();
85 self.checkSmartJson(forceUpdate);
86
87 // Ensure the right version of meteor has been fetched
88 var buildDevBundle = !! self.meteorArgs['build-dev-bundle'];
89 self.meteor.prepare(buildDevBundle, function() {
90
91 // resolving dependencies fetches them. We need to check otherwise
92 if (!self.dependencies.resolved()) {
93
94 console.verbose('Resolving dependency tree');
95
96 self.dependencies.resolve(self.meteorArgs.force, function(err, conflicts) {
97
98 _.each(conflicts, function(conflict, name) {
99 console.log(('Problem installing ' + name.bold).red);
100 console.log((" ✘ " + conflict).red);
101 });
102
103 if (err) {
104 console.log(err.message.red);
105 process.exit();
106 }
107
108 fn();
109 });
110 } else {
111 fn();
112 }
113 });
114};
115
116Project.prototype.uninstall = function() {
117 var self = this;
118
119 // for now, remove anything in packages/ that is a symlink
120 if (fs.existsSync(this.packagesRoot)) {
121 var dirs = fs.readdirSync(this.packagesRoot);
122
123 _.each(dirs, function(dir) {
124 var dirPath = path.join(self.packagesRoot, dir);
125
126 if (fs.lstatSync(dirPath).isSymbolicLink())
127 fs.unlink(dirPath)
128 });
129 }
130};
131
132// either install from smart.lock or prepare smart.lock and do so
133Project.prototype.install = function(fn, forceUpdate) {
134 var self = this;
135
136 self._optimizeFS();
137
138 // Fetch everything the project needs
139 self.fetch(function() {
140
141 // ensure that the installRoot exists
142 if (! self.dependencies.isEmpty())
143 wrench.mkdirSyncRecursive(self.packagesRoot);
144
145 // Link each package into installRoot
146 self.dependencies.installInto(self, function(packagesInstalled) {
147 console.log();
148 console.log('Done installing smart packages'.bold);
149
150 // install the smart.lock file
151 if (fs.existsSync(self.smartJsonPath) && (self.lockChanged || !fs.existsSync(self.smartLockPath)))
152 self.writeLockFile();
153
154 fn();
155 });
156 }, forceUpdate);
157};
158
159// if there's no dependencies we don't have to install
160Project.prototype.needsToInstall = function() {
161 return !this.dependencies.isEmpty();
162}
163
164// prepare a new smart.lock, then install
165Project.prototype.update = function(fn) {
166
167 this.install(fn, true);
168};
169
170Project.prototype.execute = function(args, fn) {
171 var self = this;
172
173 if (self.meteorArgs.version)
174 console.suppress();
175
176 console.log();
177 console.log("Stand back while Meteorite does its thing".bold);
178
179 // TODO -- what do we do here if not installed? I'm not sure we just go ahead
180 // and install, we should probably abort and tell them
181 self.install(function() {
182
183 console.log();
184 console.log("Ok, everything's ready. Here comes Meteor!".green);
185 console.log();
186
187 if (self.meteorArgs.version)
188 console.unsuppress();
189
190 self.meteor.execute(args, self.packagesRoot, fn);
191 });
192};
193
194// assumes that we are installed.
195Project.prototype.isUsing = function(packageName, fn) {
196 var self = this;
197
198 self.install(function() {
199 return self.meteor.isUsing(packageName, fn);
200 });
201}
202
203// ensure a named package is installed
204//
205// NOTE: Right now, if the package is already available (included in meteor, already in smart.json)
206// we ignore the version, and just stick with what we have
207//
208// TODO: In the future a version # would override anything in meteor + rewrite smart.json
209// but right now it's TBH to overwrite meteor's packages.
210Project.prototype.installPackage = function(pkgName, version, fn) {
211 var self = this;
212
213 // first ensure we are fetched, so we know _all_ the packages that are available
214 self.fetch(function() {
215
216 self.hasPackage(pkgName, function(check) {
217 // if we have the package already
218 if (check)
219 return fn();
220
221 // better check that the package exists on atmosphere
222 Atmosphere.package(pkgName, function(atmosphere_defn) {
223
224 if (!atmosphere_defn)
225 throw("Package named " + pkgName + " doesn't exist in your meteor installation, smart.json, or on atmosphere");
226
227 // ok, it's not installed. So we need to add it (permanently) to the smart.json
228 // and clear our dependencies
229 var smartJson = self.readSmartJson();
230 var defn = {}
231 if (version)
232 defn.version = version;
233 smartJson.packages = smartJson.packages || {};
234 smartJson.packages[pkgName] = defn;
235 self.writeSmartJson(smartJson);
236
237 // maybe a hack to read it back out from disk, but not a big deal I don't think
238 self.checkSmartJson(true);
239
240 fn();
241 });
242 });
243 });
244}
245
246// Is the package part of the meteor install, or is it a dependency?
247//
248// NOTE: assumes we have fetched. FIXME: figure out a better / systematic way
249// to write code that has this sort of assumption
250Project.prototype.hasPackage = function(pkgName, fn) {
251 if (this.dependencies.packages[pkgName]) {
252 return fn(true);
253 }
254
255 this.meteor.hasPackage(pkgName, fn)
256};
257
258// very simple version of what config does
259Project.prototype.readSmartJson = function() {
260
261 try {
262 var rawConfig = fs.readFileSync(path.join(this.root, 'smart.json')).toString();
263 return JSON.parse(rawConfig);
264
265 } catch (err) {
266 return {};
267 }
268};
269
270Project.prototype.smartJson = function() {
271 var data = {};
272
273 if (!this.meteor.defaultMeteor)
274 data.meteor = this.meteor.toJson();
275
276 if (this.dependencies)
277 data.packages = this.dependencies.toJson().basePackages;
278 else
279 data.packages = {};
280
281 return data;
282};
283
284
285Project.prototype.writeSmartJson = function(json) {
286 json = json || this.smartJson();
287
288 // Make a nicely formated default json string
289 var smartJsonString = JSON.stringify(json, null, 2) + "\n";
290
291 // Write to disk
292 if (fs.existsSync(this.root))
293 fs.writeFileSync(this.smartJsonPath, smartJsonString);
294};
295
296Project.prototype.lockJson = function() {
297
298 return {
299 meteor: this.meteor.toJson(true),
300 dependencies: this.dependencies.toJson(true)
301 };
302};
303
304// write out into smart.lock
305Project.prototype.writeLockFile = function() {
306
307 var smartJsonString = JSON.stringify(this.lockJson(), null, 2) + "\n";
308 fs.writeFileSync(this.smartLockPath, smartJsonString);
309};
310
311Project.prototype._optimizeFS = function() {
312 var self = this;
313
314 var deletable = [];
315
316 // remove old .meteor/meteorite directory
317 var oldInstallRoot = path.join(this.root, '.meteor', 'meteorite')
318 if (fs.existsSync(oldInstallRoot)) {
319 deletable.push(oldInstallRoot);
320 }
321
322 if (deletable.length > 0)
323 console.log("Yay! We're optimizing your installation!".yellow.bold);
324
325 _.each(deletable, function(filePath) {
326 console.log(" ✘ ".red + ("Deleting " + filePath).grey);
327
328 if (fs.lstatSync(filePath).isDirectory())
329 wrench.rmdirSyncRecursive(filePath);
330 else
331 fs.unlink(filePath);
332 });
333};
334
335module.exports = Project;
336
337// var _debug = require('./debug');
338// _.debugClass('Project');