Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80668 views
1
/**
2
* Copyright 2013 Facebook, Inc.
3
*
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
7
*
8
* http://www.apache.org/licenses/LICENSE-2.0
9
*
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
15
*/
16
var inherits = require('util').inherits;
17
var path = require('path');
18
var zlib = require('zlib');
19
20
var docblock = require('../parse/docblock');
21
var extract = require('../parse/extract');
22
var extractJavelinSymbols = require('../parse/extractJavelinSymbols');
23
var JS = require('../resource/JS');
24
var MessageList = require('../MessageList');
25
var PathResolver = require('../PathResolver');
26
var ResourceLoader = require('./ResourceLoader');
27
28
/**
29
* @class Loads and parses JavaScript files
30
* Extracts options from the docblock, extracts javelin symbols, calculates
31
* gziped network size. Both javalin symbols parsing and network size
32
* calculation are off by default due to their perf cost. Use options parameter
33
* to switch them on.
34
*
35
* @extends {ResourceLoader}
36
* @param {Object|null} options Object with the following options:
37
* - networkSize
38
* - invalidRelativePaths
39
* - extractSpecialRequires
40
*/
41
function JSLoader(options) {
42
ResourceLoader.call(this, options);
43
44
if (this.options.networkSize) {
45
this.extractExtra = this.extractNetworkSize;
46
} else {
47
this.extractExtra = function(js, sourceCode, messages, callback) {
48
// make async to break long stack traces
49
process.nextTick(function() {
50
callback(messages, js);
51
});
52
};
53
}
54
}
55
inherits(JSLoader, ResourceLoader);
56
JSLoader.prototype.path = __filename;
57
58
JSLoader.prototype.getResourceTypes = function() {
59
return [JS];
60
};
61
62
JSLoader.prototype.getExtensions = function() {
63
return this.options.extensions || ['.js', '.jsx'];
64
};
65
66
67
/**
68
* Extracts aproximate network size by gziping the source
69
* @todo (voloko) why not minify?
70
* Off by default due to perf cost
71
*
72
* @protected
73
* @param {JS} js
74
* @param {String} sourceCode
75
* @param {Function} callback
76
*/
77
JSLoader.prototype.extractNetworkSize =
78
function(js, sourceCode, messages,callback) {
79
zlib.gzip(sourceCode, function(err, buffer) {
80
js.networkSize = buffer.length;
81
callback(messages, js);
82
});
83
};
84
85
var spaceRe = /\s+/;
86
/**
87
* Syncronously extracts docblock options from the source
88
*
89
* @protected
90
* @param {JS} js
91
* @param {String} sourceCode
92
*/
93
JSLoader.prototype.parseDocblockOptions =
94
function(js, sourceCode, messages) {
95
96
var props = docblock.parse(docblock.extract(sourceCode));
97
props.forEach(function(pair) {
98
var name = pair[0];
99
var value = pair[1];
100
switch (name) {
101
case 'provides':
102
js.id = value.split(spaceRe)[0];
103
break;
104
case 'providesModule':
105
js.isModule = true;
106
js.id = value.split(spaceRe)[0];
107
break;
108
case 'providesLegacy':
109
js.isRunWhenReady = true;
110
js.isLegacy = true;
111
js.isModule = true;
112
js.id = 'legacy:' + value.split(spaceRe)[0];
113
break;
114
case 'css':
115
value.split(spaceRe).forEach(js.addRequiredCSS, js);
116
break;
117
case 'requires':
118
value.split(spaceRe).forEach(js.addRequiredLegacyComponent, js);
119
break;
120
case 'javelin':
121
// hack to ignore javelin docs (voloko)
122
if (js.path.indexOf('/js/javelin/docs/') !== -1) {
123
break;
124
}
125
js.isModule = true;
126
js.isJavelin = true;
127
js.isRunWhenReady = true;
128
break;
129
case 'polyfill':
130
js.isPolyfill = true;
131
if (value.match(/\S/)) {
132
js.polyfillUAs = value.split(spaceRe);
133
} else {
134
js.polyfillUAs = ['all'];
135
}
136
break;
137
case 'runWhenReady_DEPRECATED':
138
js.isRunWhenReady = true;
139
break;
140
case 'jsx':
141
// Anything before the first dot.
142
// @jsx React.DOM should end up requiring React.
143
var match = value && value.match(/^([^\.]+)/);
144
js.isJSXEnabled = true;
145
js.jsxDOMImplementor = value;
146
if (match[0]) {
147
js.addRequiredModule(match[0]);
148
}
149
break;
150
case 'permanent':
151
js.isPermanent = true;
152
break;
153
case 'nopackage':
154
js.isNopackage = true;
155
break;
156
case 'option':
157
case 'options':
158
value.split(spaceRe).forEach(function(key) {
159
js.options[key] = true;
160
});
161
break;
162
case 'suggests':
163
messages.addClowntownError(js.path, 'docblock',
164
'@suggests is deprecated. Simply use the Bootloader APIs.');
165
break;
166
case 'author':
167
case 'deprecated':
168
// Support these so Diviner can pick them up.
169
break;
170
case 'bolt':
171
// Used by bolt transformation
172
break;
173
case 'javelin-installs':
174
// This is used by Javelin to identify installed symbols.
175
break;
176
case 'param':
177
case 'params':
178
case 'task':
179
case 'return':
180
case 'returns':
181
case 'access':
182
messages.addWarning(js.path, 'docblock',
183
"File has a header docblock, but the docblock is class or " +
184
"function documentation, not file documentation. Header blocks " +
185
"should not have @param, @task, @returns, @access, etc.");
186
break;
187
case 'nolint':
188
case 'generated':
189
case 'preserve-header':
190
case 'emails':
191
// various options
192
break;
193
case 'layer':
194
// This directive is currently used by Connect JS library
195
break;
196
default:
197
messages.addClowntownError(js.path, 'docblock',
198
'Unknown directive ' + name);
199
}
200
});
201
};
202
203
204
/**
205
* Initialize a resource with the source code and configuration
206
* Loader can parse, gzip, minify the source code to build the resulting
207
* Resource value object
208
*
209
* @protected
210
* @param {String} path resource being built
211
* @param {ProjectConfiguration} configuration configuration for the path
212
* @param {String} sourceCode
213
* @param {Function} callback
214
*/
215
JSLoader.prototype.loadFromSource =
216
function(path, configuration, sourceCode, messages, callback) {
217
var js = new JS(path);
218
if (configuration) {
219
js.isModule = true;
220
}
221
222
this.parseDocblockOptions(js, sourceCode, messages);
223
224
if (js.isJavelin) {
225
var data = extractJavelinSymbols(sourceCode);
226
js.definedJavelinSymbols = data.defines;
227
js.requiredJavelinSymbols = data.requires;
228
if (data.id) {
229
js.id = data.id;
230
}
231
if (js.id != 'javelin-magical-init') {
232
js.addRequiredModule('javelin-magical-init');
233
}
234
}
235
236
// resolve module ids through configuration
237
if (js.isModule || js.path.indexOf('__browsertests__') !== -1) {
238
// require calls outside of modules are not supported
239
if (configuration) {
240
if (!js.id) {
241
js.id = configuration.resolveID(js.path);
242
}
243
}
244
extract.requireCalls(sourceCode).forEach(js.addRequiredModule, js);
245
246
if (this.options.extractSpecialRequires) {
247
js.requiredLazyModules =
248
extract.requireLazyCalls(sourceCode);
249
js.suggests = extract.loadModules(sourceCode);
250
}
251
} else {
252
if (this.options.extractSpecialRequires) {
253
js.requiredLazyModules =
254
extract.requireLazyCalls(sourceCode);
255
js.suggests = extract.loadComponents(sourceCode);
256
}
257
}
258
extract.cxModules(sourceCode).forEach(js.addRequiredCSS, js);
259
// call generated function
260
this.extractExtra(js, sourceCode, messages, function(m, js) {
261
if (js) {
262
js.finalize();
263
}
264
callback(m, js);
265
});
266
};
267
268
/**
269
* Only match *.js files
270
* @param {String} filePath
271
* @return {Boolean}
272
*/
273
JSLoader.prototype.matchPath = function(filePath) {
274
return this.getExtensions().some(function (ext) {
275
return filePath.lastIndexOf(ext) === filePath.length - ext.length;
276
});
277
};
278
279
280
/**
281
* Resolving the absolute file path for a `require(x)` call is actually nuanced
282
* and difficult to reimplement. Instead, we'll use an implementation based on
283
* node's (private) path resolution to ensure that we're compliant with
284
* commonJS. This doesn't take into account `providesModule`, so we deal with
285
* that separately. Unfortunately, node's implementation will read files off of
286
* the disk that we've likely already pulled in `ProjectConfigurationLoader`
287
* etc, so we can't use it directly - we had to factor out the pure logic into
288
* `PathResolver.js`.
289
*
290
* @param {string} requiredText Text inside of require() function call.
291
* @param {string} callersPath Path of the calling module.
292
* @param {ResourceMap} resourceMap ResourceMap containing project configs and
293
* JS resources.
294
* @return {string} Absolute path of the file corresponding to requiredText, or
295
* null if the module can't be resolved.
296
*/
297
function findAbsolutePathForRequired(requiredText, callersPath, resourceMap) {
298
var callerData = {
299
id: callersPath,
300
paths: resourceMap.getAllInferredProjectPaths(),
301
fileName: callersPath
302
};
303
return PathResolver._resolveFileName(requiredText, callerData, resourceMap);
304
}
305
306
/**
307
* Post process is called after the map is updated but before the update task is
308
* complete. `JSLoader` uses `postProcess` to _statically_ resolve
309
* dependencies. What this means, is analyzing the argument to `require()` calls
310
* in the JS, and determining the _unique_ logical ID of the resource being
311
* referred to. This is something that *must* be done in `postProcess`, once
312
* every module's ID has been determined, and must be done statically in order
313
* to do anything useful with packaging etc.
314
*
315
* Two modules both might:
316
*
317
* `require('../path/to.js')`
318
*
319
* But they end up resolving to two distinct dependencies/IDs, because the
320
* calling file is located in a different base directory.
321
*
322
* @param {ResourceMap} map
323
* @param {Array.<Resource>} resources
324
* @param {Function} callback
325
*/
326
JSLoader.prototype.postProcess = function(map, resources, callback) {
327
var messages = MessageList.create();
328
var isJavelin = false;
329
330
// Required text that doesn't have a '.' at the beginning should always
331
// resolve to the same path for a given hasteMap, regardless of which file is
332
// calling require, that means that we can optimize the lookup by caching the
333
// resolved paths to these modules. If we don't do this *SECONDS* will be
334
// spend calling `findAbsolutePathForRequired`. This cache is only valid for
335
// non relative require texts. If relative requires ('.') the calling file
336
// needs to be taken into consideration and this cache doesn't take that into
337
// consideration.
338
var nonRelativePathCache = {};
339
340
resources.forEach(function(r) {
341
var required = r.requiredModules;
342
343
if (r.isJavelin) {
344
isJavelin = true;
345
}
346
347
for (var i = 0; i < required.length; i++) {
348
var requiredText = required[i]; // require('requiredText')
349
var resourceByID = map.getResource('JS', requiredText);
350
if (resourceByID) { // Already requiring by ID - no static
351
continue; // resolution needed.
352
}
353
354
// @providesModule and standard require('projectName/path/to.js') would
355
// have been caught above - now handle commonJS relative dirs, and
356
// package.json main files.
357
var beginsWithDot = requiredText.charAt(0) !== '.';
358
var textInCache = requiredText in nonRelativePathCache;
359
var commonJSResolvedPath = beginsWithDot && textInCache ?
360
nonRelativePathCache[requiredText] :
361
findAbsolutePathForRequired(requiredText, r.path, map);
362
if (beginsWithDot && !textInCache) {
363
nonRelativePathCache[requiredText] = commonJSResolvedPath;
364
}
365
366
// If not found by ID, we use commonJS conventions for lookup.
367
var resolvedResource =
368
commonJSResolvedPath &&
369
map.getResourceByPath(commonJSResolvedPath);
370
371
// Some modules may not have ids - this is likely a bug - their package's
372
// haste roots might be incorrect.
373
if (resolvedResource && resolvedResource.id) {
374
if (resolvedResource.id !== required[i]) {
375
// 'JSTest' files end up here. They don't have this method.
376
if (r.recordRequiredModuleOrigin) {
377
r.recordRequiredModuleOrigin(required[i], resolvedResource.id);
378
required[i] = resolvedResource.id;
379
}
380
}
381
}
382
}
383
});
384
385
// legacy namespace
386
resources.forEach(function(r) {
387
var resource, i, required;
388
389
required = r.requiredCSS;
390
if (required) {
391
for (i = 0; i < required.length; i++) {
392
resource = map.getResource('CSS', 'css:' + required[i]);
393
if (resource && resource.isModule) {
394
required[i] = 'css:' + required[i];
395
}
396
}
397
}
398
399
if (r.isModule) {
400
return;
401
}
402
403
required = r.requiredLegacyComponents;
404
if (required) {
405
for (i = 0; i < required.length; i++) {
406
resource = map.getResource('JS', 'legacy:' + required[i]);
407
if (resource && resource.isLegacy) {
408
required[i] = 'legacy:' + required[i];
409
}
410
}
411
}
412
413
required = r.suggests;
414
if (required) {
415
for (i = 0; i < required.length; i++) {
416
resource = map.getResource('JS', 'legacy:' + required[i]);
417
if (resource && resource.isLegacy) {
418
required[i] = 'legacy:' + required[i];
419
}
420
}
421
}
422
423
});
424
425
// rebuild javelin map
426
if (isJavelin) {
427
var providesMap = {};
428
map.getAllResourcesByType('JS').forEach(function(r) {
429
if (r.isJavelin) {
430
r.definedJavelinSymbols.forEach(function(s) {
431
if (providesMap[s]) {
432
messages.addClowntownError(r.path, 'javelin',
433
'Javlin symbol ' + s + ' is already defined in ' +
434
providesMap[s].path);
435
return;
436
}
437
providesMap[s] = r;
438
});
439
}
440
});
441
map.getAllResourcesByType('JS').forEach(function(r) {
442
if (r.isJavelin) {
443
r.requiredJavelinSymbols.forEach(function(s) {
444
var resolved = providesMap[s];
445
if (!resolved) {
446
messages.addClowntownError(r.path, 'javelin',
447
'Javlin symbol ' + s + ' is required but never defined');
448
return;
449
}
450
if (r.requiredModules.indexOf(resolved.id) === -1) {
451
r.requiredModules.push(resolved.id);
452
}
453
if (r.requiredLegacyComponents.indexOf(resolved.id) !== -1) {
454
r.requiredLegacyComponents = r.requiredLegacyComponents
455
.filter(function(id) { return id !== resolved.id; });
456
}
457
});
458
}
459
});
460
}
461
462
process.nextTick(function() {
463
callback(messages);
464
});
465
};
466
467
module.exports = JSLoader;
468
469