1 | import path from 'path'
|
2 | import fs from 'fs'
|
3 |
|
4 | import require_hacker from 'require-hacker'
|
5 | import UglifyJS from 'uglify-js'
|
6 |
|
7 | import Log from './tools/log'
|
8 | import request from './tools/synchronous http'
|
9 |
|
10 | import { exists, clone, convert_from_camel_case, starts_with, ends_with, alias_properties_with_camel_case } from './helpers'
|
11 | import { default_webpack_assets, normalize_options, alias_hook, normalize_asset_path, uniform_path } from './common'
|
12 |
|
13 |
|
14 |
|
15 | export default class webpack_isomorphic_tools
|
16 | {
|
17 |
|
18 | hooks = []
|
19 |
|
20 |
|
21 | cached_assets = []
|
22 |
|
23 | constructor(options)
|
24 | {
|
25 |
|
26 | this.options = convert_from_camel_case(clone(options))
|
27 |
|
28 |
|
29 | normalize_options(this.options)
|
30 |
|
31 |
|
32 | this.options.development = process.env.NODE_ENV !== 'production'
|
33 |
|
34 |
|
35 | if (this.options.debug)
|
36 | {
|
37 | require_hacker.log.options.debug = true
|
38 | }
|
39 |
|
40 |
|
41 | this.log = new Log('webpack-isomorphic-tools', { debug: this.options.debug })
|
42 |
|
43 | this.log.debug(`instantiated webpack-isomorphic-tools v${require('../package.json').version} with options`, this.options)
|
44 | }
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | development()
|
50 | {
|
51 |
|
52 | this.log.error('`.development()` method is now deprecated ' +
|
53 | '(for server-side instance only, not for webpack plugin instance) ' +
|
54 | 'and has no effect. Set up a proper `process.env.NODE_ENV` variable instead: ' +
|
55 | 'it should be "production" for production, otherwise it assumes development. ' +
|
56 | 'The currently used mode is: ' + (this.options.development ? 'development' : 'production') + '. ' +
|
57 | '`process.env.NODE_ENV is: ' + process.env.NODE_ENV)
|
58 |
|
59 |
|
60 | return this
|
61 | }
|
62 |
|
63 |
|
64 |
|
65 | assets()
|
66 | {
|
67 |
|
68 | if (this.options.development)
|
69 | {
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 | if (this.options.port)
|
77 | {
|
78 | try
|
79 | {
|
80 | return request(this.options.port)
|
81 | }
|
82 | catch (error)
|
83 | {
|
84 | this.log.error(`Couldn't contact webpack-isomorphic-tools plugin over HTTP. Using an empty stub for webpack assets map.`)
|
85 | this.log.error(error)
|
86 | return default_webpack_assets()
|
87 | }
|
88 | }
|
89 |
|
90 | else
|
91 | {
|
92 | if (!fs.existsSync(this.webpack_assets_path))
|
93 | {
|
94 | this.log.error(`"${this.webpack_assets_path}" not found. Most likely it hasn't yet been generated by Webpack. The most probable cause of this error is that you placed your server code outside of the callback in "webpack_isomorphic_tools.server(path, callback)" (or outside of the ".then()" call if you are using promises API). Using an empty stub instead.`)
|
95 | return default_webpack_assets()
|
96 | }
|
97 | }
|
98 | }
|
99 |
|
100 |
|
101 | if (!this.webpack_assets_path)
|
102 | {
|
103 | throw new Error(`You seem to have forgotten to call the .server() method`)
|
104 | }
|
105 |
|
106 | return require(this.webpack_assets_path)
|
107 | }
|
108 |
|
109 |
|
110 | refresh()
|
111 | {
|
112 |
|
113 | if (!this.options.development)
|
114 | {
|
115 | throw new Error('.refresh() called in production mode. It shouldn\'t be called in production mode because that would degrade website performance by discarding caches.')
|
116 | }
|
117 |
|
118 | this.log.debug('flushing require() caches')
|
119 |
|
120 |
|
121 |
|
122 |
|
123 | delete require.cache[this.webpack_assets_path]
|
124 |
|
125 |
|
126 | for (let path of this.cached_assets)
|
127 | {
|
128 | this.log.debug(` flushing require() cache for ${path}`)
|
129 | delete require.cache[path]
|
130 | }
|
131 |
|
132 |
|
133 | this.cached_assets = []
|
134 | }
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 | enable_aliasing()
|
142 | {
|
143 |
|
144 | this.alias_hook = require_hacker.resolver((path, module) =>
|
145 | {
|
146 |
|
147 | return alias_hook(path, module, this.options.project_path, this.options.alias, this.log)
|
148 | })
|
149 |
|
150 |
|
151 | return this
|
152 | }
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 | server(project_path, callback)
|
166 | {
|
167 |
|
168 | this.options.project_path = project_path
|
169 |
|
170 |
|
171 | this.webpack_assets_path = path.resolve(this.options.project_path, this.options.webpack_assets_file_path)
|
172 |
|
173 |
|
174 | this.register()
|
175 |
|
176 |
|
177 | if (this.options.alias)
|
178 | {
|
179 | this.enable_aliasing()
|
180 | }
|
181 |
|
182 |
|
183 | if (this.options.modules_directories)
|
184 | {
|
185 | this.inject_modules_directories(this.options.modules_directories)
|
186 | }
|
187 |
|
188 |
|
189 | if (this.options.patch_require)
|
190 | {
|
191 | this.log.debug('Patching Node.js require() function')
|
192 | this.patch_require()
|
193 | }
|
194 |
|
195 |
|
196 |
|
197 |
|
198 | if (callback)
|
199 | {
|
200 |
|
201 | return this.wait_for_assets(callback)
|
202 | }
|
203 |
|
204 | else
|
205 | {
|
206 |
|
207 | return new Promise((resolve, reject) => this.wait_for_assets(resolve))
|
208 | }
|
209 | }
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 | register()
|
222 | {
|
223 | this.log.debug('registering require() hooks for assets')
|
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 | for (let asset_type of Object.keys(this.options.assets))
|
260 | {
|
261 | const description = this.options.assets[asset_type]
|
262 |
|
263 | for (let extension of description.extensions)
|
264 | {
|
265 | this.register_extension(extension, description)
|
266 | }
|
267 | }
|
268 |
|
269 |
|
270 | this.loaders_hook = require_hacker.global_hook('webpack-loaders', (required_path, module) =>
|
271 | {
|
272 |
|
273 |
|
274 |
|
275 | if (starts_with(required_path, '/')
|
276 | || starts_with(required_path, './')
|
277 | || starts_with(required_path, '../')
|
278 | || required_path.indexOf(':') > 0
|
279 | || required_path.indexOf('!') < 0)
|
280 | {
|
281 | return
|
282 | }
|
283 |
|
284 | let parts = required_path.split('!')
|
285 | const local_asset_path = parts.pop()
|
286 |
|
287 |
|
288 |
|
289 |
|
290 | if (!starts_with(local_asset_path, './')
|
291 | && !starts_with(local_asset_path, '../'))
|
292 | {
|
293 | return
|
294 | }
|
295 |
|
296 | parts = parts.map(loader =>
|
297 | {
|
298 | let loader_parts = loader.split('?')
|
299 |
|
300 | if (!ends_with(loader_parts[0], '-loader'))
|
301 | {
|
302 | loader_parts[0] += '-loader'
|
303 | }
|
304 |
|
305 | return `./~/${loader_parts.join('?')}`
|
306 | })
|
307 |
|
308 | const global_asset_path = require_hacker.resolve(local_asset_path, module)
|
309 |
|
310 | const path = parts.join('!') + '!' + this.normalize_asset_path(global_asset_path)
|
311 |
|
312 | const asset = this.asset_source(path)
|
313 |
|
314 | if (asset === undefined)
|
315 | {
|
316 | return
|
317 | }
|
318 |
|
319 | return this.require_asset(asset, { require_cache_path: required_path + '.webpack-loaders' })
|
320 | })
|
321 |
|
322 |
|
323 | return this
|
324 | }
|
325 |
|
326 |
|
327 | register_extension(extension, description)
|
328 | {
|
329 | this.log.debug(` registering a require() hook for *.${extension}`)
|
330 |
|
331 |
|
332 | if (extension === 'json')
|
333 | {
|
334 | this.hooks.push(require_hacker.hook(extension, path =>
|
335 | {
|
336 |
|
337 | if (path === this.webpack_assets_path)
|
338 | {
|
339 | return
|
340 | }
|
341 |
|
342 | return this.require(path, description)
|
343 | }))
|
344 | }
|
345 | else
|
346 | {
|
347 | this.hooks.push(require_hacker.hook(extension, path => this.require(path, description)))
|
348 | }
|
349 | }
|
350 |
|
351 |
|
352 | inject_modules_directories(modules_directories)
|
353 | {
|
354 | modules_directories = modules_directories.filter(x => x !== 'node_modules')
|
355 |
|
356 |
|
357 |
|
358 |
|
359 | const original_find_paths = require('module')._findPath
|
360 |
|
361 | require('module')._findPath = function(request, paths)
|
362 | {
|
363 | paths.map(function(a_path)
|
364 | {
|
365 | var parts = a_path.split(path.sep)
|
366 | if (parts[parts.length - 1] === 'node_modules')
|
367 | {
|
368 | parts[parts.length - 1] = ''
|
369 | return parts.join(path.sep)
|
370 | }
|
371 | })
|
372 | .filter(function(a_path)
|
373 | {
|
374 | return a_path
|
375 | })
|
376 | .forEach(function(a_path)
|
377 | {
|
378 | modules_directories.forEach(function(modules_directory)
|
379 | {
|
380 | paths.push(a_path + modules_directory)
|
381 | })
|
382 | })
|
383 |
|
384 | return original_find_paths(request, paths)
|
385 | }
|
386 | }
|
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
392 | patch_require()
|
393 | {
|
394 |
|
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 | let require_context = `require.context = function(base, scan_subdirectories, regular_expression)
|
401 | {
|
402 | base = require('path').join(require('path').dirname(module.filename), base)
|
403 |
|
404 | var contents = {}
|
405 |
|
406 | // recursive function
|
407 | function read_directory(directory)
|
408 | {
|
409 | require('fs').readdirSync(directory).forEach(function(child)
|
410 | {
|
411 | var full_path = require('path').resolve(directory, child)
|
412 |
|
413 | if (require('fs').statSync(full_path).isDirectory())
|
414 | {
|
415 | if (scan_subdirectories)
|
416 | {
|
417 | read_directory(full_path)
|
418 | }
|
419 | }
|
420 | else
|
421 | {
|
422 | var asset_path = require('path').relative(base, full_path)
|
423 |
|
424 | // analogous to "uniform_path" from "./common.js"
|
425 | asset_path = (asset_path[0] === '.' ? asset_path : ('./' + asset_path)).replace(/\\\\/g, '/')
|
426 |
|
427 | if (regular_expression && !regular_expression.test(asset_path))
|
428 | {
|
429 | return
|
430 | }
|
431 |
|
432 | contents[asset_path] = full_path
|
433 | }
|
434 | })
|
435 | }
|
436 |
|
437 | read_directory(base)
|
438 |
|
439 | var result = function(asset_path)
|
440 | {
|
441 | return require(contents[asset_path])
|
442 | }
|
443 |
|
444 | result.keys = function()
|
445 | {
|
446 | return Object.keys(contents)
|
447 | }
|
448 |
|
449 | result.resolve = function(asset_path)
|
450 | {
|
451 | return contents[asset_path]
|
452 | }
|
453 |
|
454 | return result
|
455 | };`
|
456 |
|
457 |
|
458 | require_context = UglifyJS.minify(require_context, { fromString: true }).code
|
459 |
|
460 |
|
461 |
|
462 | const require_ensure = `require.ensure=function(d,c){c(require)};`
|
463 |
|
464 | const debug = this.log.debug.bind(this.log)
|
465 |
|
466 |
|
467 |
|
468 |
|
469 | const original_compile = require('module').prototype._compile
|
470 |
|
471 | require('module').prototype._compile = function(content, filename)
|
472 | {
|
473 |
|
474 | if (!ends_with(filename, '.js'))
|
475 | {
|
476 |
|
477 | return original_compile.call(this, content, filename)
|
478 | }
|
479 |
|
480 |
|
481 | let preamble = ''
|
482 |
|
483 |
|
484 |
|
485 | if (content.indexOf('require.context') >= 0)
|
486 | {
|
487 | debug(`Injecting require.context() into "${filename}"`)
|
488 | preamble += require_context
|
489 | }
|
490 |
|
491 |
|
492 |
|
493 | if (content.indexOf('require.ensure') >= 0)
|
494 | {
|
495 | debug(`Injecting require.ensure() into "${filename}"`)
|
496 | preamble += require_ensure
|
497 | }
|
498 |
|
499 |
|
500 | if (preamble)
|
501 | {
|
502 |
|
503 | if (starts_with(content, `'use strict'`) || starts_with(content, `"use strict"`))
|
504 | {
|
505 | preamble = `"use strict";` + preamble
|
506 | }
|
507 | }
|
508 |
|
509 |
|
510 | content = preamble + content
|
511 |
|
512 |
|
513 | return original_compile.call(this, content, filename)
|
514 | }
|
515 | }
|
516 |
|
517 | normalize_asset_path(global_asset_path)
|
518 | {
|
519 |
|
520 |
|
521 | if (!this.options.project_path)
|
522 | {
|
523 | throw new Error(`You forgot to call the .server() method passing it your project's base path`)
|
524 | }
|
525 |
|
526 |
|
527 | return normalize_asset_path(global_asset_path, this.options.project_path)
|
528 | }
|
529 |
|
530 |
|
531 | require(global_asset_path, description)
|
532 | {
|
533 | this.log.debug(`require() called for ${global_asset_path}`)
|
534 |
|
535 |
|
536 | const asset_path = this.normalize_asset_path(global_asset_path)
|
537 |
|
538 |
|
539 |
|
540 |
|
541 | if (!this.includes(asset_path, description) || this.excludes(asset_path, description))
|
542 | {
|
543 | this.log.debug(` skipping require call for ${asset_path}`)
|
544 | return
|
545 | }
|
546 |
|
547 |
|
548 | const asset = this.asset_source(asset_path)
|
549 |
|
550 |
|
551 | if (asset === undefined)
|
552 | {
|
553 | this.log.error(`asset not found: ${asset_path}`)
|
554 | }
|
555 |
|
556 | return this.require_asset(asset, { require_cache_path: global_asset_path })
|
557 | }
|
558 |
|
559 |
|
560 | require_asset(asset, options)
|
561 | {
|
562 |
|
563 |
|
564 |
|
565 | if (this.options.development)
|
566 | {
|
567 |
|
568 | this.cached_assets.push(options.require_cache_path)
|
569 | }
|
570 |
|
571 |
|
572 | return require_hacker.to_javascript_module_source(asset)
|
573 | }
|
574 |
|
575 |
|
576 | asset_source(asset_path)
|
577 | {
|
578 | this.log.debug(` requiring ${asset_path}`)
|
579 |
|
580 |
|
581 |
|
582 |
|
583 |
|
584 | function possible_webpack_paths(asset_path)
|
585 | {
|
586 |
|
587 | if (starts_with(asset_path, './node_modules/'))
|
588 | {
|
589 | asset_path = asset_path.replace('./node_modules/', './~/')
|
590 | }
|
591 |
|
592 |
|
593 |
|
594 |
|
595 |
|
596 | const parts = asset_path.split('/node_modules/')
|
597 |
|
598 | function construct_guesses(parts)
|
599 | {
|
600 | if (parts.length === 1)
|
601 | {
|
602 | return [parts]
|
603 | }
|
604 |
|
605 | const last = parts.pop()
|
606 | const rest = construct_guesses(parts)
|
607 |
|
608 | const guesses = []
|
609 |
|
610 | for (let guess of rest)
|
611 | {
|
612 | const one = clone(guess)
|
613 | one.push('/~/')
|
614 | one.push(last)
|
615 |
|
616 | const two = clone(guess)
|
617 | two.push('/node_modules/')
|
618 | two.push(last)
|
619 |
|
620 | guesses.push(one)
|
621 | guesses.push(two)
|
622 | }
|
623 |
|
624 | return guesses
|
625 | }
|
626 |
|
627 | return construct_guesses(parts)
|
628 | }
|
629 |
|
630 |
|
631 | const assets = this.assets().assets
|
632 |
|
633 | const possible_webpack_asset_paths = possible_webpack_paths(asset_path).map(path => path.join(''))
|
634 |
|
635 | for (let webpack_asset_path of possible_webpack_asset_paths)
|
636 | {
|
637 | if (possible_webpack_asset_paths.length > 1)
|
638 | {
|
639 | this.log.debug(` trying "${webpack_asset_path}"`)
|
640 | }
|
641 |
|
642 |
|
643 | const asset = assets[webpack_asset_path]
|
644 |
|
645 | if (exists(asset))
|
646 | {
|
647 |
|
648 | return asset
|
649 | }
|
650 | }
|
651 |
|
652 |
|
653 | return
|
654 | }
|
655 |
|
656 |
|
657 | undo()
|
658 | {
|
659 |
|
660 |
|
661 | for (let hook of this.hooks)
|
662 | {
|
663 | hook.unmount()
|
664 | }
|
665 |
|
666 |
|
667 |
|
668 |
|
669 | if (this.alias_hook)
|
670 | {
|
671 | this.alias_hook.unmount()
|
672 | }
|
673 |
|
674 |
|
675 | if (this.loaders_hook)
|
676 | {
|
677 | this.loaders_hook.unmount()
|
678 | }
|
679 | }
|
680 |
|
681 |
|
682 | excludes(path, options)
|
683 | {
|
684 |
|
685 | if (!exists(options.exclude))
|
686 | {
|
687 | return false
|
688 | }
|
689 |
|
690 |
|
691 | for (let exclude of options.exclude)
|
692 | {
|
693 |
|
694 | if (exclude instanceof RegExp)
|
695 | {
|
696 | if (exclude.test(path))
|
697 | {
|
698 | return true
|
699 | }
|
700 | }
|
701 |
|
702 | else if (typeof exclude === 'function')
|
703 | {
|
704 | if (exclude(path))
|
705 | {
|
706 | return true
|
707 | }
|
708 | }
|
709 |
|
710 | else
|
711 | {
|
712 | if (exclude === path)
|
713 | {
|
714 | return true
|
715 | }
|
716 | }
|
717 | }
|
718 |
|
719 |
|
720 |
|
721 | return false
|
722 | }
|
723 |
|
724 |
|
725 | includes(path, options)
|
726 | {
|
727 |
|
728 | if (!exists(options.include))
|
729 | {
|
730 | return true
|
731 | }
|
732 |
|
733 |
|
734 | for (let include of options.include)
|
735 | {
|
736 |
|
737 | if (include instanceof RegExp)
|
738 | {
|
739 | if (include.test(path))
|
740 | {
|
741 | return true
|
742 | }
|
743 | }
|
744 |
|
745 | else if (typeof include === 'function')
|
746 | {
|
747 | if (include(path))
|
748 | {
|
749 | return true
|
750 | }
|
751 | }
|
752 |
|
753 | else
|
754 | {
|
755 | if (include === path)
|
756 | {
|
757 | return true
|
758 | }
|
759 | }
|
760 | }
|
761 |
|
762 |
|
763 |
|
764 | return false
|
765 | }
|
766 |
|
767 |
|
768 |
|
769 |
|
770 |
|
771 |
|
772 |
|
773 | wait_for_assets(done)
|
774 | {
|
775 |
|
776 | const check_interval = 300
|
777 | const message_interval = 2000
|
778 |
|
779 |
|
780 | let message_timer = 0
|
781 |
|
782 |
|
783 | const tools = this
|
784 |
|
785 |
|
786 | function wait_for(condition, proceed)
|
787 | {
|
788 | function check()
|
789 | {
|
790 |
|
791 | if (condition())
|
792 | {
|
793 | return proceed()
|
794 | }
|
795 |
|
796 | message_timer += check_interval
|
797 |
|
798 | if (message_timer >= message_interval)
|
799 | {
|
800 | message_timer = 0
|
801 |
|
802 | tools.log.debug(`(${tools.webpack_assets_path} not found)`)
|
803 | tools.log.info('(waiting for the first Webpack build to finish)')
|
804 | }
|
805 |
|
806 | setTimeout(check, check_interval)
|
807 | }
|
808 |
|
809 | check()
|
810 | }
|
811 |
|
812 |
|
813 |
|
814 |
|
815 | let ready_check
|
816 |
|
817 |
|
818 | if (this.options.development && this.options.port)
|
819 | {
|
820 | ready_check = () =>
|
821 | {
|
822 | try
|
823 | {
|
824 | request(this.options.port)
|
825 | return true
|
826 | }
|
827 | catch (error)
|
828 | {
|
829 | if (!starts_with(error.message, 'Server responded with status code 404:\nWebpack assets not generated yet')
|
830 | && !starts_with(error.message, 'connect ECONNREFUSED')
|
831 | && !starts_with(error.message, 'Request timed out after'))
|
832 | {
|
833 | this.log.error(`Couldn't contact webpack-isomorphic-tools plugin over HTTP. Using an empty stub for webpack assets map.`)
|
834 | this.log.error(error)
|
835 | }
|
836 |
|
837 | return false
|
838 | }
|
839 | }
|
840 | }
|
841 |
|
842 | else
|
843 | {
|
844 | ready_check = () => fs.existsSync(this.webpack_assets_path)
|
845 | }
|
846 |
|
847 | setImmediate(() => wait_for(ready_check, done))
|
848 |
|
849 |
|
850 | return this
|
851 | }
|
852 | }
|
853 |
|
854 |
|
855 |
|
856 |
|
\ | No newline at end of file |