forked from meteor/meteor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
library.js
466 lines (411 loc) · 17.1 KB
/
library.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
var path = require('path');
var _ = require('underscore');
var files = require('./files.js');
var watch = require('./watch.js');
var packages = require('./packages.js');
var warehouse = require('./warehouse.js');
var bundler = require('./bundler.js');
var buildmessage = require('./buildmessage.js');
var fs = require('fs');
// Under the hood, packages in the library (/packages/foo), and user
// applications, are both Packages -- they are just represented
// differently on disk.
// Options:
// - releaseManifest: a parsed release manifest
// - localPackageDirs: array of directories to search before checking
// the manifest and the warehouse. Directories that don't exist (or
// paths that aren't directories) will be silently ignored.
var Library = function (options) {
var self = this;
options = options || {};
self.releaseManifest = options.releaseManifest;
// Trim down localPackageDirs to just those that actually exist (and
// that are actually directories)
self.localPackageDirs = _.filter(options.localPackageDirs, function (dir) {
try {
// use stat rather than lstat since symlink to dir is OK
var stats = fs.statSync(dir);
} catch (e) {
return false;
}
return stats.isDirectory();
});
self.overrides = {}; // package name to package directory
// both map from package name to:
// - pkg: cached Package object
// - packageDir: directory from which it was loaded
self.softReloadCache = {};
self.loadedPackages = {};
};
_.extend(Library.prototype, {
// Temporarily add a package to the library (or override a package
// that actually exists in the library.) `packageName` is the name
// to use for the package and `packageDir` is the directory that
// contains its source. For now, it is an error to try to install
// two overrides for the same packageName.
override: function (packageName, packageDir) {
var self = this;
if (_.has(self.overrides, packageName))
throw new Error("Duplicate override for package '" + packageName + "'");
self.overrides[packageName] = path.resolve(packageDir);
},
// Undo an override previously set up with override().
removeOverride: function (packageName) {
var self = this;
if (!_.has(self.overrides, packageName))
throw new Error("No override present for package '" + packageName + "'");
delete self.loadedPackages[packageName];
delete self.overrides[packageName];
delete self.softReloadCache[packageName];
},
// Force reload of changed packages. See description at get().
//
// 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 = {};
},
// Given a package name as a string, returns the absolute path to the package
// directory (which is the *source* tree in the source-with-built-unipackage
// case, not the .build directory), or null if not found.
//
// Does NOT load the package or make any recursive calls, so can safely be
// called from Package initialization code. Intended primarily for comparison
// to the packageDirForBuildInfo field on a Package object; also used
// internally to implement 'get'.
findPackageDirectory: function (name) {
var self = this;
// Packages cached from previous calls
if (_.has(self.loadedPackages, name)) {
return self.loadedPackages[name].packageDir;
}
// If there's an override for this package, use that without
// looking at any other options.
if (_.has(self.overrides, name))
return self.overrides[name];
for (var i = 0; i < self.localPackageDirs.length; ++i) {
var packageDir = path.join(self.localPackageDirs[i], name);
// A directory is a package if it either contains 'package.js' (a package
// source tree) or 'unipackage.json' (a compiled unipackage). (Actually,
// for now, unipackages contain a dummy package.js too.)
//
// XXX support for putting unipackages in a local package dir is
// incomplete! They will be properly loaded, but other packages that
// depend on them have no way of knowing when they change! unipackages
// that are the .build of a source tree work fine (they have a
// buildinfo.json and can be rebuilt), and warehouse unipackages work fine
// too (users are not supposed to edit them (they are read-only on disk),
// and their pathname specifies a version). But if you, eg, have a
// unipackage of coffeescript in a local package directory, build another
// package dependending on it, and substitute another version of the
// unipackage in the same location, nothing will ever rebuild your
// package!
if (fs.existsSync(path.join(packageDir, 'package.js')) ||
fs.existsSync(path.join(packageDir, 'unipackage.json'))) {
return packageDir;
}
}
// Try the Meteor distribution, if we have one.
var version = self.releaseManifest && self.releaseManifest.packages[name];
if (version) {
packageDir = path.join(warehouse.getWarehouseDir(),
'packages', name, version);
// The warehouse is theoretically constructed carefully enough that the
// directory really should not exist unless it is complete.
if (! fs.existsSync(packageDir))
throw new Error("Package missing from warehouse: " + name +
" version " + version);
return packageDir;
}
// Nope!
return null;
},
// Given a package name as a string, retrieve a Package object. If
// throwOnError is true, the default, throw an error if the package
// can't be found. (If false is passed for throwOnError, then return
// null if the package can't be found.) When called inside
// buildmessage.enterJob, however, instead of throwing an error it
// will record a build error and return a dummy (empty) package.
//
// Searches overrides first, then any localPackageDirs you have
// provided, then the manifest/warehouse if provided.
//
// get() caches the packages it returns, meaning if you call
// get('foo') and later foo 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 get() is called for them, see
// refresh().
get: function (name, throwOnError) {
var self = this;
// Passed a Package?
if (name instanceof packages.Package)
return name;
// Packages cached from previous calls
if (_.has(self.loadedPackages, name)) {
return self.loadedPackages[name].pkg;
}
// Check for invalid package names. Currently package names can only contain
// ASCII alphanumerics, dash, and dot, and must contain at least one
// letter.
//
// XXX revisit this later. What about unicode package names?
if (/[^A-Za-z0-9.\-]/.test(name) || !/[A-Za-z]/.test(name) ) {
if (throwOnError === false)
return null;
throw new Error("Invalid package name: " + name);
}
var packageDir = self.findPackageDirectory(name);
if (! packageDir) {
if (throwOnError === false)
return null;
buildmessage.error("package not available: " + name);
// recover by returning a dummy (empty) package
var pkg = new packages.Package(self);
pkg.initEmpty(name);
return pkg;
}
// See if we can reuse a package that we have cached from before
// the last soft refresh.
if (_.has(self.softReloadCache, name)) {
var entry = self.softReloadCache[name];
// 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[name];
if (entry.packageDir === packageDir && entry.pkg.checkUpToDate()) {
// Cache hit
self.loadedPackages[name] = entry;
return entry.pkg;
}
}
// Load package from disk
var pkg = new packages.Package(self, packageDir);
if (fs.existsSync(path.join(packageDir, 'unipackage.json'))) {
// It's an already-built package
pkg.initFromUnipackage(name, packageDir);
self.loadedPackages[name] = {pkg: pkg, packageDir: packageDir};
} else {
// It's a source tree. Does it have a built unipackage inside it?
var buildDir = path.join(packageDir, '.build');
if (fs.existsSync(buildDir) &&
pkg.initFromUnipackage(name, buildDir,
{ onlyIfUpToDate: true,
buildOfPath: packageDir })) {
// We already had a build and it was up to date.
self.loadedPackages[name] = {pkg: pkg, packageDir: packageDir};
} else {
// Either we didn't have a build, or it was out of date. Build the
// package.
buildmessage.enterJob({
title: "building package `" + name + "`",
rootPath: packageDir
}, function () {
// This has to be done in the right sequence: initialize
// (which loads the dependency list but does not get() those
// packages), then put the package into the package list,
// then call build() to get() the dependencies and finish
// the build. If you called build() before putting the
// package in the package list then you'd recurse
// forever. (build() needs the dependencies because it needs
// to look at the handlers registered by any plugins in the
// packages that we use.)
pkg.initFromPackageDir(name, packageDir);
self.loadedPackages[name] = {pkg: pkg, packageDir: packageDir};
pkg.build();
if (! buildmessage.jobHasMessages() && // ensure no errors!
pkg.canBeSavedAsUnipackage()) {
// Save it, for a fast load next time
try {
files.add_to_gitignore(packageDir, '.build*');
pkg.saveAsUnipackage(buildDir, { buildOfPath: packageDir });
} 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 pkg;
},
// 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.)
getForApp: function (appDir, ignoreFiles) {
var self = this;
var pkg = new packages.Package(self);
pkg.initFromAppDir(appDir, ignoreFiles || []);
pkg.build();
return pkg;
},
// Given a slice set spec -- either a package name like "ddp", or a particular
// slice within the package like "ddp:client", or a parsed object like
// {package: "ddp", slice: "client"} -- return the list of matching slices (as
// an array of Slice objects) for a given architecture.
getSlices: function (spec, arch) {
var self = this;
if (typeof spec === "string")
spec = packages.parseSpec(spec);
var pkg = self.get(spec.package, true);
if (spec.slice)
return [pkg.getSingleSlice(spec.slice, arch)];
else
return pkg.getDefaultSlices(arch);
},
// Register local package directories with a watchSet. We want to know if a
// package is created or deleted, which includes both its top-level source
// directory and its main package metadata file.
watchLocalPackageDirs: function (watchSet) {
var self = this;
_.each(self.localPackageDirs, function (packageDir) {
var packages = watch.readAndWatchDirectory(watchSet, {
absPath: packageDir,
include: [/\/$/]
});
_.each(packages, function (p) {
watch.readAndWatchFile(watchSet,
path.join(packageDir, p, 'package.js'));
watch.readAndWatchFile(watchSet,
path.join(packageDir, p, 'unipackage.json'));
});
});
},
// Get all packages available. Returns a map from the package name
// to a Package object.
//
// XXX Hack: If errors occur while generating the list (which could
// easily happen, since it currently involves building packages)
// print them to the console and exit(1)! Certainly not ideal but is
// expedient since, eg, test-packages calls list() before it does
// anything else.
list: function () {
var self = this;
var names = [];
var ret = {};
var messages = buildmessage.capture(function () {
names = _.keys(self.overrides);
_.each(self.localPackageDirs, function (dir) {
names = _.union(names, fs.readdirSync(dir));
});
if (self.releaseManifest) {
names = _.union(names, _.keys(self.releaseManifest.packages));
}
_.each(names, function (name) {
var pkg = self.get(name, false);
if (pkg)
ret[name] = pkg;
});
});
if (messages.hasMessages()) {
process.stdout.write("=> Errors while scanning packages:\n\n");
process.stdout.write(messages.formatMessages());
process.exit(1);
}
return ret;
},
// Rebuild all source packages in our search paths -- even including
// any source packages in the warehouse. (Perhaps we shouldn't
// include the warehouse since it's supposed to be immutable.. or
// maybe if the warehouse wants to be immutable perhaps it shouldn't
// include source packages. This is intended primarily for
// convenience when developing the package build code.)
//
// This will force the rebuild even of packages that are
// shadowed. However, for now, it's undefined whether shadowed
// packages are rebuilt (eg, if you have two packages named 'foo' in
// your search path, both of them will have their builds deleted but
// only the visible one might get rebuilt immediately.)
//
// Returns a count of packages rebuilt.
rebuildAll: function () {
var self = this;
// XXX refactor to combine logic with list()? important difference
// here is that we want shadowed packages too
var all = {}; // map from path to name
// Assemble a list of all packages
_.each(self.overrides, function (packageDir, name) {
all[packageDir] = name;
});
_.each(self.localPackageDirs, function (dir) {
var subdirs = fs.readdirSync(dir);
_.each(subdirs, function (subdir) {
var packageDir = path.resolve(dir, subdir);
all[packageDir] = subdir;
});
});
// We *DON'T* look in the warehouse here, because warehouse packages are
// prebuilt.
// Delete any that are source packages with builds.
var count = 0;
_.each(_.keys(all), function (packageDir) {
var isRealPackage = true;
try {
if (! fs.statSync(packageDir).isDirectory())
isRealPackage = false;
} catch (e) {
// stat failed -- path doesn't exist
isRealPackage = false;
}
if (! isRealPackage) {
delete all[packageDir];
return;
}
var buildDir = path.join(packageDir, '.build');
files.rm_recursive(buildDir);
});
// Now reload them, forcing a rebuild. We have to do this in two
// passes because otherwise we might end up rebuilding a package
// and then immediately deleting it.
self.refresh();
_.each(all, function (name, packageDir) {
// Tolerate missing packages. This can happen because our crude
// logic above misdetects an empty directory as a package.
if (self.get(name, /* throwOnError */ false))
count ++;
});
return count;
}
});
var library = exports;
_.extend(exports, {
Library: Library,
// returns a pretty list suitable for showing to the user. input is
// a list of package objects, each of which must have a name (not be
// an application package.)
formatList: function (pkgs) {
var longest = '';
_.each(pkgs, function (pkg) {
if (!pkg.metadata.internal && pkg.name.length > longest.length)
longest = pkg.name;
});
var pad = longest.replace(/./g, ' ');
// it'd be nice to read the actual terminal width, but I tried
// several methods and none of them work (COLUMNS isn't set in
// node's environment; `tput cols` returns a constant 80.) maybe
// node is doing something weird with ptys.
var width = 80;
var out = '';
_.each(pkgs, function (pkg) {
if (pkg.metadata.internal)
return;
var name = pkg.name + pad.substr(pkg.name.length);
var summary = pkg.metadata.summary || 'No description';
out += (name + " " +
summary.substr(0, width - 2 - pad.length) + "\n");
});
return out;
}
});