forked from meteor/meteor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
package-api.js
597 lines (534 loc) · 21.2 KB
/
package-api.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
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
var assert = require("assert");
var _ = require("underscore");
var buildmessage = require('../utils/buildmessage.js');
var utils = require('../utils/utils.js');
var compiler = require('./compiler.js');
var archinfo = require('../utils/archinfo.js');
var catalog = require('../packaging/catalog/catalog.js');
// It's important that we import these functions individually instead of
// importing the whole files.* namespace, because now it's easier to tell
// that this module doesn't actually touch the file system.
import {
pathRelative,
convertToPosixPath,
} from "../fs/files.js";
function toArray (x) {
if (_.isArray(x)) {
return x;
}
return x ? [x] : [];
}
function toArchArray (arch) {
if (! _.isArray(arch)) {
arch = arch ? [arch] : compiler.ALL_ARCHES;
}
arch = _.uniq(arch);
arch = _.map(arch, mapWhereToArch);
// avoid using _.each so as to not add more frames to skip
for (var i = 0; i < arch.length; ++i) {
var inputArch = arch[i];
var isMatch = _.any(_.map(compiler.ALL_ARCHES, function (actualArch) {
return archinfo.matches(actualArch, inputArch);
}));
if (! isMatch) {
buildmessage.error(
"Invalid 'where' argument: '" + inputArch + "'",
// skip toArchArray in addition to the actual API function
{useMyCaller: 1});
}
}
return arch;
}
// We currently have a 1 to 1 mapping between 'where' and 'arch'.
// 'client' -> 'web'
// 'server' -> 'os'
// '*' -> '*'
function mapWhereToArch (where) {
if (where === 'server') {
return 'os';
} else if (where === 'client') {
return 'web';
} else {
return where;
}
}
// Iterates over the list of target archs and calls f(arch) for all archs
// that match an element of self.allarchs.
function forAllMatchingArchs (archs, f) {
_.each(archs, function (arch) {
_.each(compiler.ALL_ARCHES, function (matchArch) {
if (archinfo.matches(matchArch, arch)) {
f(matchArch);
}
});
});
}
/**
* @name PackageAPI
* @class PackageAPI
* @instanceName api
* @showInstanceName true
* @global
* @summary Type of the API object passed into the `Package.onUse` function.
*/
export function PackageAPI(options) {
var self = this;
assert.ok(self instanceof PackageAPI);
options = options || {};
self.buildingIsopackets = !!options.buildingIsopackets;
// source files used.
// It's a multi-level map structured as:
// arch -> sources|assets -> relPath -> {relPath, fileOptions}
self.files = {};
// symbols exported
self.exports = {};
// packages used and implied (keys are 'package', 'unordered', and
// 'weak'). an "implied" package is a package that will be used by a unibuild
// which uses us.
self.uses = {};
self.implies = {};
_.each(compiler.ALL_ARCHES, function (arch) {
self.files[arch] = {
assets: [],
sources: [],
main: null,
};
self.exports[arch] = [];
self.uses[arch] = [];
self.implies[arch] = [];
});
self.releaseRecords = [];
}
_.extend(PackageAPI.prototype, {
// 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)
//
// @param arch 'web', 'web.browser', 'web.cordova', 'server',
// or an array of those.
// The default is ['web', 'server'].
//
// options can include:
//
// - 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.)
/**
* @memberOf PackageAPI
* @instance
* @summary Depend on package `packagename`.
* @locus package.js
* @param {String|String[]} packageNames Packages being depended on.
* Package names may be suffixed with an @version tag.
*
* In general, you must specify a package's version (e.g.,
* `'accounts@1.0.0'` to use version 1.0.0 or a higher
* compatible version (ex: 1.0.1, 1.5.0, etc.) of the
* `accounts` package). If you are sourcing core
* packages from a Meteor release with `versionsFrom`, you may leave
* off version names for core packages. You may also specify constraints,
* such as `my:forms@=1.0.0` (this package demands `my:forms` at `1.0.0` exactly),
* or `my:forms@1.0.0 || =2.0.1` (`my:forms` at `1.x.y`, or exactly `2.0.1`).
* @param {String|String[]} [architecture] If you only use the package on the
* server (or the client), you can pass in the second argument (e.g.,
* `'server'`, `'client'`, `'web.browser'`, `'web.cordova'`) to specify
* what architecture the package is used with. You can specify multiple
* architectures by passing in an array, for example `['web.cordova', 'os.linux']`.
* @param {Object} [options]
* @param {Boolean} options.weak Establish a weak dependency on a
* package. If package A has a weak dependency on package B, it means
* that including A in an app does not force B to be included too — but,
* if B is included or by another package, then B will load before A.
* You can use this to make packages that optionally integrate with or
* enhance other packages if those packages are present.
* When you weakly depend on a package you don't see its exports.
* You can detect if the possibly-present weakly-depended-on package
* is there by seeing if `Package.foo` exists, and get its exports
* from the same place.
* @param {Boolean} options.unordered It's okay to load this dependency
* after your package. (In general, dependencies specified by `api.use`
* are loaded before your package.) You can use this option to break
* circular dependencies.
*/
use: function (names, arch, options) {
var self = this;
// Support `api.use(package, {weak: true})` without arch.
if (_.isObject(arch) && !_.isArray(arch) && !options) {
options = arch;
arch = null;
}
options = options || {};
names = toArray(names);
arch = toArchArray(arch);
// 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;
}
// using for loop rather than underscore to help with useMyCaller
for (var i = 0; i < names.length; ++i) {
var name = names[i];
try {
var parsed = utils.parsePackageConstraint(name);
} catch (e) {
if (!e.versionParserError) {
throw e;
}
buildmessage.error(e.message, {useMyCaller: true});
// recover by ignoring
continue;
}
forAllMatchingArchs(arch, function (a) {
self.uses[a].push({
package: parsed.package,
constraint: parsed.constraintString,
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.
/**
*
* @memberOf PackageAPI
* @summary Give users of this package access to another package (by passing
* in the string `packagename`) or a collection of packages (by passing in
* an array of strings [`packagename1`, `packagename2`]
* @locus package.js
* @instance
* @param {String|String[]} packageNames Name of a package, or array of
* package names, with an optional @version component for each.
* @param {String|String[]} [architecture] If you only use the package on
* the server (or the client), you can pass in the second argument (e.g.,
* `'server'`, `'client'`, `'web.browser'`, `'web.cordova'`) to specify what
* architecture the package is used with. You can specify multiple
* architectures by passing in an array, for example `['web.cordova',
* 'os.linux']`.
*/
imply: function (names, arch) {
var self = this;
// We currently disallow build plugins in
// debugOnly/prodOnly/testOnly packages; but if you could use
// imply in a debugOnly package, you could pull in the build
// plugin from an implied package, which would have the same
// problem as allowing build plugins directly in the package. So
// no imply either!
if (self.debugOnly || self.prodOnly || self.testOnly) {
buildmessage.error("can't use imply in packages that are debugOnly, prodOnly or testOnly");
// recover by ignoring
return;
}
names = toArray(names);
arch = toArchArray(arch);
// using for loop rather than underscore to help with useMyCaller
for (var i = 0; i < names.length; ++i) {
var name = names[i];
try {
var parsed = utils.parsePackageConstraint(name);
} catch (e) {
if (!e.versionParserError) {
throw e;
}
buildmessage.error(e.message, {useMyCaller: true});
// recover by ignoring
continue;
}
// api.imply('isobuild:compiler-plugin') doesn't really make any sense. If
// we change our mind and think it makes sense, we can always implement it
// later...
if (compiler.isIsobuildFeaturePackage(parsed.package)) {
buildmessage.error(
`to declare that your package requires the build tool feature ` +
`'{parsed.package}', use 'api.use', not 'api.imply'`);
// recover by ignoring
continue;
}
forAllMatchingArchs(arch, function (a) {
// We don't allow weak or unordered implies, since the main
// purpose of imply is to provide imports and plugins.
self.implies[a].push({
package: parsed.package,
constraint: parsed.constraintString
});
});
}
},
// 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).
/**
* @memberOf PackageAPI
* @instance
* @summary Specify source code files for your package.
* @locus package.js
* @param {String|String[]} filenames Paths to the source files.
* @param {String|String[]} [architecture] If you only want to use the file
* on the server (or the client), you can pass this argument
* (e.g., 'server', 'client', 'web.browser', 'web.cordova') to specify
* what architecture the file is used with. You can specify multiple
* architectures by passing in an array, for example
* `['web.cordova', 'os.linux']`. By default, the file will be loaded on both
* server and client.
* @param {Object} [options] Options that will be passed to build
* plugins.
* @param {Boolean} [options.bare] If this file is JavaScript code or will
* be compiled into JavaScript code by a build plugin, don't wrap the
* resulting file in a closure. Has the same effect as putting a file into the
* `client/compatibility` directory in an app.
*/
addFiles: function (paths, arch, fileOptions) {
if (fileOptions && fileOptions.isAsset) {
// XXX it would be great to print a warning here, see the issue:
// https://github.com/meteor/meteor/issues/5495
this._addFiles("assets", paths, arch);
return;
}
// Watch out - we rely on the levels of stack traces inside this
// function so don't wrap it in another function without changing that logic
this._addFiles("sources", paths, arch, fileOptions);
},
mainModule(path, arch) {
arch = toArchArray(arch);
forAllMatchingArchs(arch, a => {
const filesForArch = this.files[a];
const source = {
relPath: pathRelative(".", path),
fileOptions: {
mainModule: true
}
};
const oldMain = filesForArch.main;
if (oldMain) {
// It's not an error to call api.mainModule multiple times, but
// the last call takes precedence over the earlier calls.
oldMain.fileOptions.mainModule = false;
}
filesForArch.main = source;
filesForArch.sources.push(source);
});
},
/**
* @memberOf PackageAPI
* @instance
* @summary Specify asset files for your package. They can be accessed via
* the [Assets API](#assets) from the server, or at the URL
* `/packages/username_package-name/file-name` from the client, depending on the
* architecture passed.
* @locus package.js
* @param {String|String[]} filenames Paths to the asset files.
* @param {String|String[]} architecture Specify where this asset should be
* available (e.g., 'server', 'client', 'web.browser', 'web.cordova'). You can
* specify multiple architectures by passing in an array, for example
* `['web.cordova', 'os.linux']`.
*/
addAssets(paths, arch) {
if(!arch) {
buildmessage.error('addAssets requires a second argument specifying ' +
'where the asset should be available. For example: "client", ' +
'"server", or ["client", "server"].', { useMyCaller: true });
return;
}
// Watch out - we rely on the levels of stack traces inside this
// function so don't wrap it in another function without changing that logic
this._addFiles("assets", paths, arch);
},
/**
* Internal method used by addFiles and addAssets.
*/
_addFiles(type, paths, arch, fileOptions) {
if (type !== "sources" && type !== "assets") {
throw new Error(`Can only handle sources and assets, not '${type}'.`);
}
var self = this;
paths = toArray(paths);
arch = toArchArray(arch);
// Convert Dos-style paths to Unix-style paths.
// XXX it is possible to convert an already Unix-style path by mistake
// and break it. e.g.: 'some\folder/anotherFolder' is a valid path
// consisting of two components. #WindowsPathApi
paths = _.map(paths, function (p) {
// Normalize ./foo.js to foo.js.
p = pathRelative(".", p);
if (p.indexOf('/') !== -1) {
// it is already a Unix-style path most likely
return p;
}
return convertToPosixPath(p, true);
});
var errors = [];
_.each(paths, function (path) {
forAllMatchingArchs(arch, function (a) {
const filesOfType = self.files[a][type];
// Check if we have already added a file at this path
if (_.has(filesOfType, path)) {
// We want the singular form of the file type
const typeName = {
sources: 'source',
assets: 'asset'
}[type];
errors.push(`Duplicate ${typeName} file: ${path}`);
return;
}
const source = {
relPath: path
};
if (fileOptions) {
source.fileOptions = fileOptions;
}
filesOfType.push(source);
});
});
// Spit out all the errors at the end, where the number of stack frames to
// skip is just 2 (this function and its callers) instead of something like
// 7 from forAllMatchingArchs and _.each. Avoid using _.each here to keep
// stack predictable.
for (var i = 0; i < errors.length; ++i) {
buildmessage.error(errors[i], { useMyCaller: 1 });
}
},
// Use this release to resolve unclear dependencies for this package. If
// you don't fill in dependencies for some of your implies/uses, we will
// look at the packages listed in the release to figure that out.
/**
* @memberOf PackageAPI
* @instance
* @summary Use versions of core packages from a release. Unless provided,
* all packages will default to the versions released along with
* `meteorRelease`. This will save you from having to figure out the exact
* versions of the core packages you want to use. For example, if the newest
* release of meteor is `METEOR@0.9.0` and it includes `jquery@1.0.0`, you
* can write `api.versionsFrom('METEOR@0.9.0')` in your package, and when you
* later write `api.use('jquery')`, it will be equivalent to
* `api.use('jquery@1.0.0')`. You may specify an array of multiple releases,
* in which case the default value for constraints will be the "or" of the
* versions from each release: `api.versionsFrom(['METEOR@0.9.0',
* 'METEOR@0.9.5'])` may cause `api.use('jquery')` to be interpreted as
* `api.use('jquery@1.0.0 || 2.0.0')`.
* @locus package.js
* @param {String | String[]} meteorRelease Specification of a release:
* track@version. Just 'version' (e.g. `"0.9.0"`) is sufficient if using the
* default release track `METEOR`. Can be an array of specifications.
*/
versionsFrom: function (releases) {
var self = this;
// Packages in isopackets really ought to be in the core release, by
// definition, so saying that they should use versions from another
// release doesn't make sense. Moreover, if we're running from a
// checkout, we build isopackets before we initialize catalog.official
// (since we may need the ddp isopacket to refresh catalog.official),
// so we wouldn't actually be able to interpret the release name
// anyway.
if (self.buildingIsopackets) {
buildmessage.error(
"packages in isopackets may not use versionsFrom");
// recover by ignoring
return;
}
releases = toArray(releases);
// using for loop rather than underscore to help with useMyCaller
for (var i = 0; i < releases.length; ++i) {
var release = releases[i];
// If you don't specify a track, use our default.
if (release.indexOf('@') === -1) {
release = catalog.DEFAULT_TRACK + "@" + release;
}
var relInf = release.split('@');
if (relInf.length !== 2) {
buildmessage.error("Release names in versionsFrom may not contain '@'.",
{ useMyCaller: true });
return;
}
var releaseRecord = catalog.official.getReleaseVersion(
relInf[0], relInf[1]);
if (!releaseRecord) {
buildmessage.error("Unknown release "+ release,
{ tags: { refreshCouldHelp: true } });
} else {
self.releaseRecords.push(releaseRecord);
}
}
},
// Export symbols from this package.
//
// @param symbols String (eg "Foo") or array of String
// @param arch 'web', 'server', 'web.browser', 'web.cordova'
// or an array of those.
// The default is ['web', 'server'].
// @param options 'testOnly', boolean.
/**
*
* @memberOf PackageAPI
* @instance
* @summary Export package-level variables in your package. The specified
* variables (declared without `var` in the source code) will be available
* to packages that use your package. If your package sets the `debugOnly`,
* `prodOnly` or `testOnly` options to `true` when it calls
* `Package.describe()`, then packages that use your package will need to use
* `Package["package-name"].ExportedVariableName` to access the value of an
* exported variable.
* @locus package.js
* @param {String|String[]} exportedObjects Name of the object to export, or
* an array of object names.
* @param {String|String[]} [architecture] If you only want to export the
* object on the server (or the client), you can pass in the second argument
* (e.g., 'server', 'client', 'web.browser', 'web.cordova') to specify what
* architecture the export is used with. You can specify multiple
* architectures by passing in an array, for example `['web.cordova',
* 'os.linux']`.
* @param {Object} [exportOptions]
* @param {Boolean} exportOptions.testOnly If true, this symbol will only be
* exported when running tests for this package.
*/
export: function (symbols, arch, options) {
var self = this;
// Support `api.export("FooTest", {testOnly: true})` without
// arch.
if (_.isObject(arch) && !_.isArray(arch) && !options) {
options = arch;
arch = null;
}
options = options || {};
symbols = toArray(symbols);
arch = toArchArray(arch);
_.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;
}
forAllMatchingArchs(arch, function (w) {
self.exports[w].push({name: symbol, testOnly: !!options.testOnly});
});
});
}
});
// XXX COMPAT WITH 0.8.x
PackageAPI.prototype.add_files = PackageAPI.prototype.addFiles;