forked from meteor/meteor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbundler.js
1855 lines (1613 loc) · 63.3 KB
/
bundler.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
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// == Site Archive (*.star) file layout (subject to rapid change) ==
//
// /star.json
//
// - format: "site-archive-pre1" for this version
//
// - builtBy: human readable banner (eg, "Meteor 0.6.0")
//
// - programs: array of programs in the star, each an object:
// - name: short, unique name for program, for referring to it
// programmatically
// - arch: architecture that this program targets. Something like
// "os", "os.linux.x86_64", or "browser.w3c".
// - path: directory (relative to star.json) containing this program
//
// XXX in the future this will also contain instructions for
// mounting packages into the namespace of each program, and
// possibly for mounting programs on top of each other (this would
// be the principled mechanism by which a server program could read
// a client program so it can server it)
//
// - plugins: array of plugins in the star, each an object:
// - name: short, unique name for plugin, for referring to it
// programmatically
// - arch: typically 'os' (for a portable plugin) or eg
// 'os.linux.x86_64' for one that include native node_modules
// - path: path (relative to star.json) to the control file (eg,
// program.json) for this plugin
//
// /README: human readable instructions
//
// /main.js: script that can be run in node.js to start the site
// running in standalone mode (after setting appropriate environment
// variables as documented in README)
//
// /server/.bundle_version.txt: contains the dev_bundle version that
// legacy (read: current) Galaxy version read in order to set
// NODE_PATH to point to arch-specific builds of binary node modules
// (primarily this is for node-fibers)
//
// XXX in the future one program (which must be a server-type
// architecture) will be designated as the 'init' program. The
// container will call it with arguments to signal app lifecycle
// events like 'start' and 'stop'.
//
//
// Conventionally programs will be located at /programs/<name>, but
// really the build tool can lay out the star however it wants.
//
//
// == Format of a program when arch is "browser.*" ==
//
// Standard:
//
// /program.json
//
// - format: "browser-program-pre1" for this version
//
// - page: path to the template for the HTML to serve when a browser
// loads a page that is part of the application. In the file
// ##HTML_ATTRIBUTES## and ##RUNTIME_CONFIG## will be replaced with
// appropriate values at runtime.
//
// - manifest: array of resources to serve with HTTP, each an object:
// - path: path of file relative to program.json
// - where: "client"
// - type: "js", "css", or "asset"
// - cacheable: is it safe to ask the browser to cache this file (boolean)
// - url: relative url to download the resource, includes cache busting
// parameter when used
// - size: size of file in bytes
// - hash: sha1 hash of the file contents
// - sourceMap: optional path to source map file (relative to program.json)
// Additionally there will be an entry with where equal to
// "internal", path equal to page (above), and hash equal to the
// sha1 of page (before replacements.) Currently this is used to
// trigger HTML5 appcache reloads at the right time (if the
// 'appcache' package is being used.)
//
// Convention:
//
// page is 'app.html'.
//
//
// == Format of a program when arch is "os.*" ==
//
// Standard:
//
// /server.js: script to run inside node.js to start the program
//
// XXX Subject to change! This will likely change to a shell script
// (allowing us to represent types of programs that don't use or
// depend on node) -- or in fact, rather than have anything fixed here
// at all, star.json just contains a path to a program to run, which
// can be anything than can be exec()'d.
//
// Convention:
//
// /program.json:
//
// - load: array with each item describing a JS file to load at startup:
// - path: path of file, relative to program.json
// - node_modules: if Npm.require is called from this file, this is
// the path (relative to program.json) of the directory that should
// be search for npm modules
// - assets: map from path (the argument to Assets.getText and
// Assets.getBinary) to path on disk (relative to program.json)
// of the asset
// - sourceMap: if present, path of a file that contains a source
// map for this file, relative to program.json
//
// /config.json:
//
// - client: the client program that should be served up by HTTP,
// expressed as a path (relative to program.json) to the *client's*
// program.json.
//
// - meteorRelease: the value to use for Meteor.release, if any
//
//
// /app/*: source code of the (server part of the) app
// /packages/foo.js: the (linked) source code for package foo
// /package-tests/foo.js: the (linked) source code for foo's tests
// /npm/foo/bar: node_modules for slice bar of package foo. may be
// symlinked if developing locally.
//
// /node_modules: node_modules needed for server.js. omitted if
// deploying (see .bundle_version.txt above), copied if bundling,
// symlinked if developing locally.
//
//
// == Format of a program that is to be used as a plugin ==
//
// /program.json:
// - format: "javascript-image-pre1" for this version
// - arch: the architecture that this build requires
// - load: array with each item describing a JS file to load, in load order:
// - path: path of file, relative to program.json
// - node_modules: if Npm.require is called from this file, this is
// the path (relative to program.json) of the directory that should
// be search for npm modules
//
// It's a little odd that architecture is stored twice (in both the
// top-level star control file and in the plugin control file) but
// it's fine and it's probably actually cleaner, because it means that
// the plugin can be treated as a self-contained unit.
//
// Note that while the spec for "os.*" is going to change to
// represent an arbitrary POSIX (or Windows) process rather than
// assuming a nodejs host, these plugins will always refer to
// JavaScript code (that potentially might be a plugin to be loaded
// into an existing JS VM). But this seems to be a concern that is
// somewhat orthogonal to arch (these plugins can still use packages
// of arch "os.*".) There is probably a missing abstraction here
// somewhere (decoupling target type from architecture) but it can
// wait until later.
var path = require('path');
var files = require(path.join(__dirname, 'files.js'));
var packages = require(path.join(__dirname, 'packages.js'));
var linker = require(path.join(__dirname, 'linker.js'));
var Builder = require(path.join(__dirname, 'builder.js'));
var archinfo = require(path.join(__dirname, 'archinfo.js'));
var buildmessage = require('./buildmessage.js');
var fs = require('fs');
var _ = require('underscore');
var project = require(path.join(__dirname, 'project.js'));
var builder = require(path.join(__dirname, 'builder.js'));
var unipackage = require(path.join(__dirname, 'unipackage.js'));
var watch = require('./watch.js');
var Fiber = require('fibers');
var Future = require(path.join('fibers', 'future'));
var sourcemap = require('source-map');
// files to ignore when bundling. node has no globs, so use regexps
var ignoreFiles = [
/~$/, /^\.#/, /^#.*#$/,
/^\.DS_Store\/?$/, /^ehthumbs\.db$/, /^Icon.$/, /^Thumbs\.db$/,
/^\.meteor\/$/, /* avoids scanning N^2 files when bundling all packages */
/^\.git\/$/ /* often has too many files to watch */
];
// http://davidshariff.com/blog/javascript-inheritance-patterns/
var inherits = function (child, parent) {
var tmp = function () {};
tmp.prototype = parent.prototype;
child.prototype = new tmp;
child.prototype.constructor = child;
};
var rejectBadPath = function (p) {
if (p.match(/\.\./))
throw new Error("bad path: " + p);
};
var stripLeadingSlash = function (p) {
if (p.charAt(0) !== '/')
throw new Error("bad path: " + p);
return p.slice(1);
};
///////////////////////////////////////////////////////////////////////////////
// NodeModulesDirectory
///////////////////////////////////////////////////////////////////////////////
// Represents a node_modules directory that we need to copy into the
// bundle or otherwise make available at runtime.
var NodeModulesDirectory = function (options) {
var self = this;
// The absolute path (on local disk) to a directory that contains
// the built node_modules to use.
self.sourcePath = options.sourcePath;
// The path (relative to the bundle root) where we would preferably
// like the node_modules to be output (essentially cosmetic.)
self.preferredBundlePath = options.preferredBundlePath;
};
///////////////////////////////////////////////////////////////////////////////
// File
///////////////////////////////////////////////////////////////////////////////
// Allowed options:
// - sourcePath: path to file on disk that will provide our contents
// - data: contents of the file as a Buffer
// - sourceMap: if 'data' is given, can be given instead of sourcePath. a string
// - cacheable
var File = function (options) {
var self = this;
if (options.data && ! (options.data instanceof Buffer))
throw new Error('File contents must be provided as a Buffer');
if (! options.sourcePath && ! options.data)
throw new Error("Must provide either sourcePath or data");
// The absolute path in the filesystem from which we loaded (or will
// load) this file (null if the file does not correspond to one on
// disk.)
self.sourcePath = options.sourcePath;
// If this file was generated, a sourceMap (as a string) with debugging
// information, as well as the "root" that paths in it should be resolved
// against. Set with setSourceMap.
self.sourceMap = null;
self.sourceMapRoot = null;
// Where this file is intended to reside within the target's
// filesystem.
self.targetPath = null;
// The URL at which this file is intended to be served, relative to
// the base URL at which the target is being served (ignored if this
// file is not intended to be served over HTTP.)
self.url = null;
// Is this file guaranteed to never change, so that we can let it be
// cached forever? Only makes sense of self.url is set.
self.cacheable = options.cacheable || false;
// The node_modules directory that Npm.require() should search when
// called from inside this file, given as a NodeModulesDirectory, or
// null if Npm.depend() is not in effect for this file. Only works
// in the "server" architecture.
self.nodeModulesDirectory = null;
// For server JS only. Assets associated with this slice; map from the path
// that is the argument to Assets.getBinary, to a Buffer that is its contents.
self.assets = null;
self._contents = options.data || null; // contents, if known, as a Buffer
self._hash = null; // hash, if known, as a hex string
};
_.extend(File.prototype, {
hash: function () {
var self = this;
if (! self._hash)
self._hash = Builder.sha1(self.contents());
return self._hash;
},
// Omit encoding to get a buffer, or provide something like 'utf8'
// to get a string
contents: function (encoding) {
var self = this;
if (! self._contents) {
if (! self.sourcePath) {
throw new Error("Have neither contents nor sourcePath for file");
}
else
self._contents = fs.readFileSync(self.sourcePath);
}
return encoding ? self._contents.toString(encoding) : self._contents;
},
setContents: function (b) {
var self = this;
if (!(b instanceof Buffer))
throw new Error("Must set contents to a Buffer");
self._contents = b;
// Un-cache hash.
self._hash = null;
},
size: function () {
var self = this;
return self.contents().length;
},
// Set the URL of this file to "/<hash><suffix>". suffix will
// typically be used to pick a reasonable extension. Also set
// cacheable to true, since the file's name is now derived from its
// contents.
setUrlToHash: function (suffix) {
var self = this;
self.url = "/" + self.hash() + suffix;
self.cacheable = true;
self.targetPath = self.hash() + suffix;
},
// Append "?<hash>" to the URL and mark the file as cacheable.
addCacheBuster: function () {
var self = this;
if (! self.url)
throw new Error("File must have a URL");
if (self.cacheable)
return; // eg, already got setUrlToHash
if (/\?/.test(self.url))
throw new Error("URL already has a query string");
self.url += "?" + self.hash();
self.cacheable = true;
},
// Given a relative path like 'a/b/c' (where '/' is this system's
// path component separator), produce a URL that always starts with
// a forward slash and that uses a literal forward slash as the
// component separator.
setUrlFromRelPath: function (relPath) {
var self = this;
var url = relPath.split(path.sep).join('/');
if (url.charAt(0) !== '/')
url = '/' + url;
self.url = url;
},
setTargetPathFromRelPath: function (relPath) {
var self = this;
// XXX hack
if (relPath.match(/^packages\//) || relPath.match(/^assets\//))
self.targetPath = relPath;
else
self.targetPath = path.join('app', relPath);
},
// Set a source map for this File. sourceMap is given as a string.
setSourceMap: function (sourceMap, root) {
var self = this;
if (typeof sourceMap !== "string")
throw new Error("sourceMap must be given as a string");
self.sourceMap = sourceMap;
self.sourceMapRoot = root;
},
// note: this assets object may be shared among multiple files!
setAssets: function (assets) {
var self = this;
if (!_.isEmpty(assets))
self.assets = assets;
}
});
///////////////////////////////////////////////////////////////////////////////
// Target
///////////////////////////////////////////////////////////////////////////////
// options:
// - library: package library to use for resolving package dependenices
// - arch: the architecture to build
//
// see subclasses for additional options
var Target = function (options) {
var self = this;
// Package library to use for resolving package dependenices.
self.library = options.library;
// Something like "browser.w3c" or "os" or "os.osx.x86_64"
self.arch = options.arch;
// All of the Slices that are to go into this target, in the order
// that they are to be loaded.
self.slices = [];
// JavaScript files. List of File. They will be loaded at startup in
// the order given.
self.js = [];
// On-disk dependencies of this target.
self.watchSet = new watch.WatchSet();
// Map from package name to package directory of all packages used.
self.pluginProviderPackageDirs = {};
// node_modules directories that we need to copy into the target (or
// otherwise make available at runtime.) A map from an absolute path
// on disk (NodeModulesDirectory.sourcePath) to a
// NodeModulesDirectory object that we have created to represent it.
self.nodeModulesDirectories = {};
// Static assets to include in the bundle. List of File.
// For browser targets, these are served over HTTP.
self.asset = [];
};
_.extend(Target.prototype, {
// Top-level entry point for building a target. Generally to build a
// target, you create with 'new', call make() to specify its sources
// and build options and actually do the work of buliding the
// target, and finally you retrieve the build product with a
// target-type-dependent function such as write() or toJsImage().
//
// options
// - packages: packages to include (Package or 'foo' or 'foo.slice'),
// per _determineLoadOrder
// - test: packages to test (Package or 'foo'), per _determineLoadOrder
// - minify: true to minify
// - addCacheBusters: if true, make all files cacheable by adding
// unique query strings to their URLs. unlikely to be of much use
// on server targets.
make: function (options) {
var self = this;
// Populate the list of slices to load
self._determineLoadOrder({
packages: options.packages || [],
test: options.test || []
});
// Link JavaScript and set up self.js, etc.
self._emitResources();
// Minify, if requested
if (options.minify) {
var minifiers = unipackage.load({
library: self.library,
packages: ['minifiers']
}).minifiers;
self.minifyJs(minifiers);
if (self.minifyCss) // XXX a bit of a hack
self.minifyCss(minifiers);
}
if (options.addCacheBusters) {
// Make client-side CSS and JS assets cacheable forever, by
// adding a query string with a cache-busting hash.
self._addCacheBusters("js");
self._addCacheBusters("css");
}
},
// Determine the packages to load, create Slices for
// them, put them in load order, save in slices.
//
// options include:
// - packages: an array of packages (or, properly speaking, slices)
// to include. Each element should either be a Package object or a
// package name as a string (to include that package's default
// slices for this arch, or a string of the form 'package.slice'
// to include a particular named slice from a particular package.
// - test: an array of packages (as Package objects or as name
// strings) whose test slices should be included
_determineLoadOrder: function (options) {
var self = this;
var library = self.library;
// Find the roots
var rootSlices =
_.flatten([
_.map(options.packages || [], function (p) {
if (typeof p === "string")
return library.getSlices(p, self.arch);
else
return p.getDefaultSlices(self.arch);
}),
_.map(options.test || [], function (p) {
var pkg = (typeof p === "string" ? library.get(p) : p);
return pkg.getTestSlices(self.arch);
})
]);
// PHASE 1: Which slices will be used?
//
// Figure out which slices are going to be used in the target, regardless of
// order. We ignore weak dependencies here, because they don't actually
// create a "must-use" constraint, just an ordering constraint.
// What slices will be used in the target? Built in Phase 1, read in
// Phase 2.
var getsUsed = {}; // Map from slice.id to Slice.
var addToGetsUsed = function (slice) {
if (_.has(getsUsed, slice.id))
return;
getsUsed[slice.id] = slice;
slice.eachUsedSlice(self.arch, {skipWeak: true}, addToGetsUsed);
};
_.each(rootSlices, addToGetsUsed);
// PHASE 2: In what order should we load the slices?
//
// Set self.slices to be all of the roots, plus all of their non-weak
// dependencies, in the correct load order. "Load order" means that if X
// depends on (uses) Y, and that relationship is not marked as unordered, Y
// appears before X in the ordering. Raises an exception iff there is no
// such ordering (due to circular dependency.)
// What slices have not yet been added to self.slices?
var needed = _.clone(getsUsed); // Map from slice.id to Slice.
// Slices that we are in the process of adding; used to detect circular
// ordered dependencies.
var onStack = {}; // Map from slice.id to true.
// This helper recursively adds slice's ordered dependencies to self.slices,
// then adds slice itself.
var add = function (slice) {
// If this has already been added, there's nothing to do.
if (!_.has(needed, slice.id))
return;
// Process each ordered dependency. (If we have an unordered dependency
// `u`, then there's no reason to add it *now*, and for all we know, `u`
// will depend on `slice` and need to be added after it. So we ignore
// those edge. Because we did follow those edges in Phase 1, any unordered
// slices were at some point in `needed` and will not be left out.)
slice.eachUsedSlice(
self.arch, {skipUnordered: true}, function (usedSlice, useOptions) {
// If this is a weak dependency, and nothing else in the target had a
// strong dependency on it, then ignore this edge.
if (useOptions.weak && ! _.has(getsUsed, usedSlice.id))
return;
if (onStack[usedSlice.id]) {
buildmessage.error("circular dependency between packages " +
slice.pkg.name + " and " + usedSlice.pkg.name);
// recover by not enforcing one of the depedencies
return;
}
onStack[usedSlice.id] = true;
add(usedSlice);
delete onStack[usedSlice.id];
});
self.slices.push(slice);
delete needed[slice.id];
};
while (true) {
// Get an arbitrary slice from those that remain, or break if none remain.
var first = null;
for (first in needed) break;
if (! first)
break;
// Now add it, after its ordered dependencies.
add(needed[first]);
}
},
// Process all of the sorted slices (which includes running the JavaScript
// linker).
_emitResources: function () {
var self = this;
var isBrowser = archinfo.matches(self.arch, "browser");
var isOs = archinfo.matches(self.arch, "os");
// Copy their resources into the bundle in order
_.each(self.slices, function (slice) {
var isApp = ! slice.pkg.name;
// Emit the resources
var resources = slice.getResources(self.arch);
// First, find all the assets, so that we can associate them with each js
// resource (for os slices).
var sliceAssets = {};
_.each(resources, function (resource) {
if (resource.type !== "asset")
return;
var f = new File({data: resource.data, cacheable: false});
var relPath = isOs
? path.join("assets", resource.servePath)
: stripLeadingSlash(resource.servePath);
f.setTargetPathFromRelPath(relPath);
if (isBrowser)
f.setUrlFromRelPath(resource.servePath);
else {
sliceAssets[resource.path] = resource.data;
}
self.asset.push(f);
});
// Now look for the other kinds of resources.
_.each(resources, function (resource) {
if (resource.type === "asset")
return; // already handled
if (_.contains(["js", "css"], resource.type)) {
if (resource.type === "css" && ! isBrowser)
// XXX might be nice to throw an error here, but then we'd
// have to make it so that packages.js ignores css files
// that appear in the server directories in an app tree
// XXX XXX can't we easily do that in the css handler in
// meteor.js?
return;
var f = new File({data: resource.data, cacheable: false});
var relPath = stripLeadingSlash(resource.servePath);
f.setTargetPathFromRelPath(relPath);
if (isBrowser) {
f.setUrlFromRelPath(resource.servePath);
}
if (resource.type === "js" && isOs) {
// Hack, but otherwise we'll end up putting app assets on this file.
if (resource.servePath !== "/packages/global-imports.js")
f.setAssets(sliceAssets);
if (! isApp && slice.nodeModulesPath) {
var nmd = self.nodeModulesDirectories[slice.nodeModulesPath];
if (! nmd) {
nmd = new NodeModulesDirectory({
sourcePath: slice.nodeModulesPath,
// It's important that this path end with
// node_modules. Otherwise, if two modules in this package
// depend on each other, they won't be able to find each
// other!
preferredBundlePath: path.join(
'npm', slice.pkg.name, slice.sliceName, 'node_modules')
});
self.nodeModulesDirectories[slice.nodeModulesPath] = nmd;
}
f.nodeModulesDirectory = nmd;
}
}
if (resource.type === "js" && resource.sourceMap) {
f.setSourceMap(resource.sourceMap, path.dirname(relPath));
}
self[resource.type].push(f);
return;
}
if (_.contains(["head", "body"], resource.type)) {
if (! isBrowser)
throw new Error("HTML segments can only go to the browser");
self[resource.type].push(resource.data);
return;
}
throw new Error("Unknown type " + resource.type);
});
// Depend on the source files that produced these resources.
self.watchSet.merge(slice.watchSet);
// Remember the library resolution of all packages used in these
// resources.
// XXX assumes that this merges cleanly
_.extend(self.pluginProviderPackageDirs,
slice.pkg.pluginProviderPackageDirs)
});
},
// Minify the JS in this target
minifyJs: function (minifiers) {
var self = this;
var allJs = _.map(self.js, function (file) {
return file.contents('utf8');
}).join('\n;\n');
allJs = minifiers.UglifyJSMinify(allJs, {
fromString: true,
compress: {drop_debugger: false}
}).code;
self.js = [new File({ data: new Buffer(allJs, 'utf8') })];
self.js[0].setUrlToHash(".js");
},
// For each resource of the given type, make it cacheable by adding
// a query string to the URL based on its hash.
_addCacheBusters: function (type) {
var self = this;
_.each(self[type], function (file) {
file.addCacheBuster();
});
},
// Return the WatchSet for this target's dependency info.
getWatchSet: function () {
var self = this;
return self.watchSet;
},
getPluginProviderPackageDirs: function () {
var self = this;
return self.pluginProviderPackageDirs;
},
// Return the most inclusive architecture with which this target is
// compatible. For example, if we set out to build a
// 'os.linux.x86_64' version of this target (by passing that as
// the 'arch' argument to the constructor), but ended up not
// including anything that was specific to Linux, the return value
// would be 'os'.
mostCompatibleArch: function () {
var self = this;
return archinfo.leastSpecificDescription(_.pluck(self.slices, 'arch'));
}
});
//////////////////// ClientTarget ////////////////////
var ClientTarget = function (options) {
var self = this;
Target.apply(this, arguments);
// CSS files. List of File. They will be loaded at page load in the
// order given.
self.css = [];
// List of segments of additional HTML for <head>/<body>.
self.head = [];
self.body = [];
if (! archinfo.matches(self.arch, "browser"))
throw new Error("ClientTarget targeting something that isn't a browser?");
};
inherits(ClientTarget, Target);
_.extend(ClientTarget.prototype, {
// Minify the CSS in this target
minifyCss: function (minifiers) {
var self = this;
var allCss = _.map(self.css, function (file) {
return file.contents('utf8');
}).join('\n');
allCss = minifiers.CleanCSSProcess(allCss);
self.css = [new File({ data: new Buffer(allCss, 'utf8') })];
self.css[0].setUrlToHash(".css");
},
generateHtmlBoilerplate: function () {
var self = this;
var templatePath = path.join(__dirname, "app.html.in");
var template = watch.readAndWatchFile(self.watchSet, templatePath);
var f = require('handlebars').compile(template.toString());
return new Buffer(f({
scripts: _.pluck(self.js, 'url'),
stylesheets: _.pluck(self.css, 'url'),
head_extra: self.head.join('\n'),
body_extra: self.body.join('\n')
}), 'utf8');
},
// Output the finished target to disk
//
// Returns the path (relative to 'builder') of the control file for
// the target
write: function (builder) {
var self = this;
builder.reserve("program.json");
builder.reserve("app.html");
// Helper to iterate over all resources that we serve over HTTP.
var eachResource = function (f) {
_.each(["js", "css", "asset"], function (type) {
_.each(self[type], function (file) {
f(file, type);
});
});
};
// Reserve all file names from the manifest, so that interleaved
// generateFilename calls don't overlap with them.
eachResource(function (file, type) {
builder.reserve(file.targetPath);
});
// Build up a manifest of all resources served via HTTP.
var manifest = [];
eachResource(function (file, type) {
var fileContents = file.contents();
var manifestItem = {
path: file.targetPath,
where: "client",
type: type,
cacheable: file.cacheable,
url: file.url
};
if (file.sourceMap) {
// Add anti-XSSI header to this file which will be served over
// HTTP. Note that the Mozilla and WebKit implementations differ as to
// what they strip: Mozilla looks for the four punctuation characters
// but doesn't care about the newline; WebKit only looks for the first
// three characters (not the single quote) and then strips everything up
// to a newline.
// https://groups.google.com/forum/#!topic/mozilla.dev.js-sourcemap/3QBr4FBng5g
var mapData = new Buffer(")]}'\n" + file.sourceMap, 'utf8');
manifestItem.sourceMap = builder.writeToGeneratedFilename(
file.targetPath + '.map', {data: mapData});
// Use a SHA to make this cacheable.
var sourceMapBaseName = file.hash() + ".map";
// XXX When we can, drop all of this and just use the SourceMap
// header. FF doesn't support that yet, though:
// https://bugzilla.mozilla.org/show_bug.cgi?id=765993
// Note: if we use the older '//@' comment, FF 24 will print a lot
// of warnings to the console. So we use the newer '//#' comment...
// which Chrome (28) doesn't support. So we also set X-SourceMap
// in webapp_server.
file.setContents(Buffer.concat([
file.contents(),
new Buffer("\n//# sourceMappingURL=" + sourceMapBaseName + "\n")
]));
manifestItem.sourceMapUrl = require('url').resolve(
file.url, sourceMapBaseName);
}
// Set this now, in case we mutated the file's contents.
manifestItem.size = file.size();
manifestItem.hash = file.hash();
writeFile(file, builder);
manifest.push(manifestItem);
});
// HTML boilerplate (the HTML served to make the client load the
// JS and CSS files and start the app)
var htmlBoilerplate = self.generateHtmlBoilerplate();
builder.write('app.html', { data: htmlBoilerplate });
manifest.push({
path: 'app.html',
where: 'internal',
hash: Builder.sha1(htmlBoilerplate)
});
// Control file
builder.writeJson('program.json', {
format: "browser-program-pre1",
page: 'app.html',
manifest: manifest
});
return "program.json";
}
});
//////////////////// JsImageTarget and JsImage ////////////////////
// A JsImage (JavaScript Image) is a fully linked JavaScript program
// that is totally ready to go. (Image is used in the sense of "the
// output of a linker", as in "executable image", rather than in the
// sense of "visual picture".)
//
// A JsImage can be loaded into its own new JavaScript virtual
// machine, or it can be loaded into an existing virtual machine as a
// plugin.
var JsImage = function () {
var self = this;
// Array of objects with keys:
// - targetPath: relative path to use if saved to disk (or for stack traces)
// - source: JS source code to load, as a string
// - nodeModulesDirectory: a NodeModulesDirectory indicating which
// directory should be searched by Npm.require()
// - sourceMap: if set, source map for this code, as a string
// note: this can't be called `load` at it would shadow `load()`
self.jsToLoad = [];
// node_modules directories that we need to copy into the target (or
// otherwise make available at runtime.) A map from an absolute path
// on disk (NodeModulesDirectory.sourcePath) to a
// NodeModulesDirectory object that we have created to represent it.
self.nodeModulesDirectories = {};
// Architecture required by this image
self.arch = null;
};
_.extend(JsImage.prototype, {
// Load the image into the current process. It gets its own unique
// Package object containing its own private copy of every
// unipackage that it uses. This Package object is returned.
//
// If `bindings` is provided, it is a object containing a set of
// variables to set in the global environment of the executed
// code. The keys are the variable names and the values are their
// values. In addition to the contents of `bindings`, Package and
// Npm will be provided.
//
// XXX throw an error if the image includes any "app-style" code
// that is built to put symbols in the global namespace rather than
// in a compartment of Package
load: function (bindings) {
var self = this;
var ret = {};
// XXX This is mostly duplicated from server/boot.js, as is Npm.require
// below. Some way to avoid this?
var getAsset = function (assets, assetPath, encoding, callback) {
var fut;
if (! callback) {
if (! Fiber.current)
throw new Error("The synchronous Assets API can " +
"only be called from within a Fiber.");
fut = new Future();
callback = fut.resolver();
}
var _callback = function (err, result) {
if (result && ! encoding)
// Sadly, this copies in Node 0.10.
result = new Uint8Array(result);
callback(err, result);
};
if (!assets || !_.has(assets, assetPath)) {
_.callback(new Error("Unknown asset: " + assetPath));
} else {
var buffer = assets[assetPath];
var result = encoding ? buffer.toString(encoding) : buffer;
_callback(null, result);
}
if (fut)
return fut.wait();
};
// Eval each JavaScript file, providing a 'Npm' symbol in the same
// way that the server environment would, a 'Package' symbol
// so the loaded image has its own private universe of loaded
// packages, and an 'Assets' symbol to help the package find its
// static assets.
var failed = false;
_.each(self.jsToLoad, function (item) {
if (failed)
return;
var env = _.extend({
Package: ret,
Npm: {
require: function (name) {
if (! item.nodeModulesDirectory) {
// No Npm.depends associated with this package
return require(name);
}
var nodeModuleDir =
path.join(item.nodeModulesDirectory.sourcePath, name);
if (fs.existsSync(nodeModuleDir)) {
return require(nodeModuleDir);
}
try {
return require(name);
} catch (e) {
throw new Error("Can't load npm module '" + name +
"' while loading " + item.targetPath +
". Check your Npm.depends().'");
}
}
},
Assets: {
getText: function (assetPath, callback) {
return getAsset(item.assets, assetPath, "utf8", callback);
},
getBinary: function (assetPath, callback) {
return getAsset(item.assets, assetPath, undefined, callback);
}