UNPKG

11.6 kBJavaScriptView Raw
1import path from 'path'
2import fs from 'fs'
3
4import Require_hacker from 'require-hacker'
5import Log from './tools/log'
6
7import { exists, clone, alias_camel_case } from './helpers'
8import { default_webpack_assets, normalize_options, to_javascript_module_source } from './common'
9
10// using ES6 template strings
11// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings
12export default class webpack_isomorphic_tools
13{
14 // require() hooks for assets
15 hooks = []
16
17 // used to keep track of cached assets and flush their caches on .refresh() call
18 cached_assets = []
19
20 constructor(options)
21 {
22 // take the passed in options
23 this.options = alias_camel_case(clone(options))
24
25 // add missing fields, etc
26 normalize_options(this.options)
27
28 // logging
29 this.log = new Log('webpack-isomorphic-tools', { debug: this.options.debug })
30
31 this.log.debug('instantiated webpack-isomorphic-tools with options', this.options)
32 }
33
34 // sets development mode flag to whatever was passed (or true if nothing was passed)
35 // (development mode allows asset hot reloading when used with webpack-dev-server)
36 development(flag)
37 {
38 // set development mode flag
39 this.options.development = exists(flag) ? flag : true
40
41 if (this.options.development)
42 {
43 this.log.debug('entering development mode')
44 }
45 else
46 {
47 this.log.debug('entering production mode')
48 }
49
50 // allows method chaining
51 return this
52 }
53
54 // returns a mapping to read file paths for all the user specified asset types
55 // along with a couple of predefined ones: javascripts and styles
56 assets()
57 {
58 // when in development mode
59 if (this.options.development)
60 {
61 // webpack and node.js start in parallel
62 // so webpack-assets.json might not exist on the very first run
63 // if a developer chose not to use the .server() method with a callback
64 // (or if a developer chose not to wait for a Promise returned by the .server() method)
65 if (!fs.existsSync(this.webpack_assets_path))
66 {
67 this.log.error(`"${this.webpack_assets_path}" not found. Most likely it hasn't yet been generated by Webpack. Using an empty stub instead.`)
68 return default_webpack_assets()
69 }
70 }
71
72 return require(this.webpack_assets_path)
73 }
74
75 // clear the require.cache (only used in developer mode with webpack-dev-server)
76 refresh()
77 {
78 // ensure this is development mode
79 if (!this.options.development)
80 {
81 throw new Error('.refresh() called in production mode. Did you forget to call .development() method on your webpack-isomorphic-tools server instance?')
82 }
83
84 this.log.debug('flushing require() caches')
85
86 // uncache webpack-assets.json file
87 // this.log.debug(' flushing require() cache for webpack assets json file')
88 // this.log.debug(` (was cached: ${typeof(require.cache[this.webpack_assets_path]) !== 'undefined'})`)
89 delete require.cache[this.webpack_assets_path]
90
91 // uncache cached assets
92 for (let path of this.cached_assets)
93 {
94 this.log.debug(` flushing require() cache for ${path}`)
95 delete require.cache[path]
96 }
97
98 // no assets are cached now
99 this.cached_assets = []
100 }
101
102 // Initializes server-side instance of `webpack-isomorphic-tools`
103 // with the base path for your project, then calls `.register()`,
104 // and after that calls .wait_for_assets(callback).
105 //
106 // The `project_path` parameter must be identical
107 // to the `context` parameter of your Webpack configuration
108 // and is needed to locate `webpack-assets.json`
109 // which is output by Webpack process.
110 //
111 // sets up "project_path" option
112 // (this option is required on the server to locate webpack-assets.json)
113 server(project_path, callback)
114 {
115 // project base path, required to locate webpack-assets.json
116 this.options.project_path = project_path
117
118 // resolve webpack-assets.json file path
119 this.webpack_assets_path = path.resolve(this.options.project_path, this.options.webpack_assets_file_path)
120
121 // register require() hooks
122 this.register()
123
124 // when ready:
125
126 // if callback is given, call it back
127 if (callback)
128 {
129 // call back when ready
130 return this.wait_for_assets(callback)
131 }
132 // otherwise resolve a Promise
133 else
134 {
135 // no callback given, return a Promise
136 return new Promise((resolve, reject) => this.wait_for_assets(resolve))
137 }
138 }
139
140 // Registers Node.js require() hooks for the assets
141 //
142 // This is what makes the `requre()` magic work on server.
143 // These `require()` hooks must be set before you `require()`
144 // any of your assets
145 // (e.g. before you `require()` any React components
146 // `require()`ing your assets).
147 //
148 // read this article if you don't know what a "require hook" is
149 // http://bahmutov.calepin.co/hooking-into-node-loader-for-fun-and-profit.html
150 register()
151 {
152 this.log.debug('registering require() hooks for assets')
153
154 // hacking Node.js require() calls
155 this.require_hacker = new Require_hacker({ debug: this.options.debug })
156
157 // for each user specified asset type,
158 // register a require() hook for each file extension of this asset type
159 for (let asset_type of Object.keys(this.options.assets))
160 {
161 const description = this.options.assets[asset_type]
162
163 for (let extension of description.extensions)
164 {
165 this.register_extension(extension, description)
166 }
167 }
168
169 // allows method chaining
170 return this
171 }
172
173 // registers a require hook for a particular file extension
174 register_extension(extension, description)
175 {
176 this.log.debug(` registering a require() hook for *.${extension}`)
177
178 // place the require() hook for this extension
179 this.hooks.push(this.require_hacker.hook(extension, (path, fallback) => this.require(path, description, fallback)))
180 }
181
182 // require()s an asset by a path
183 require(global_asset_path, description, fallback)
184 {
185 this.log.debug(`require() called for ${global_asset_path}`)
186
187 // sanity check
188 if (!this.options.project_path)
189 {
190 throw new Error(`You forgot to call the .server() method passing it your project's base path`)
191 }
192
193 // // if the require()d file is not part of the project - skip it
194 // // (should be faster, but who needs faster if the modules are cached by Node.js)
195 // if (!global_asset_path.indexOf(this.options.project_path) === 0)
196 // {
197 // this.log.debug(` skipping require call for ${asset_path} (not in project folder)`)
198 // return fallback()
199 // }
200
201 // // asset path relative to the project folder
202 // // (should be faster, but who needs faster if the modules are cached by Node.js)
203 // let asset_path = global_asset_path.slice(this.options.project_path.length + fs.sep.length)
204
205 // asset path relative to the project folder
206 let asset_path = path.relative(this.options.project_path, global_asset_path)
207
208 // for Windows:
209 //
210 // convert Node.js path to a correct Webpack path
211 asset_path = asset_path.replace(/\\/g, '/')
212 // add './' in the beginning if it's missing (for example, in case of Windows)
213 if (asset_path.indexOf('.') !== 0)
214 {
215 asset_path = './' + asset_path
216 }
217
218 // if this filename is in the user specified exceptions list
219 // (or is not in the user explicitly specified inclusion list)
220 // then fallback to the normal require() behaviour
221 if (!this.includes(asset_path, description) || this.excludes(asset_path, description))
222 {
223 this.log.debug(` skipping require call for ${asset_path}`)
224 return fallback()
225 }
226
227 // track cached assets (only in development mode)
228 if (this.options.development)
229 {
230 // mark this asset as cached
231 this.cached_assets.push(global_asset_path)
232 }
233
234 // webpack has a shortcut from "node_modules"
235 if (asset_path.indexOf('./node_modules/') === 0)
236 {
237 asset_path = asset_path.replace('./node_modules/', './~/')
238 }
239
240 // return CommonJS module source for this asset
241 return to_javascript_module_source(this.asset_source(asset_path))
242 }
243
244 // returns asset source by path (looks it up in webpack-assets.json)
245 asset_source(asset_path)
246 {
247 this.log.debug(` requiring ${asset_path}`)
248
249 // sanity check
250 if (!asset_path)
251 {
252 return undefined
253 }
254
255 // get real file path list
256 var assets = this.assets().assets
257
258 // find this asset in the real file path list
259 const asset = assets[asset_path]
260
261 // if the asset was found in the list - return it
262 if (exists(asset))
263 {
264 return asset
265 }
266
267 // serve a not-found asset maybe
268 this.log.error(`asset not found: ${asset_path}`)
269 return undefined
270 }
271
272 // unregisters require() hooks
273 undo()
274 {
275 // for each user specified asset type,
276 // unregister a require() hook for each file extension of this asset type
277 for (let hook of this.hooks)
278 {
279 hook.unmount()
280 }
281 }
282
283 // Checks if the required path should be excluded from the custom require() hook
284 excludes(path, options)
285 {
286 // if "exclude" parameter isn't specified, then exclude nothing
287 if (!exists(options.exclude))
288 {
289 return false
290 }
291
292 // for each exclusion case
293 for (let exclude of options.exclude)
294 {
295 // supports regular expressions
296 if (exclude instanceof RegExp)
297 {
298 if (exclude.test(path))
299 {
300 return true
301 }
302 }
303 // check for a compex logic match
304 else if (typeof exclude === 'function')
305 {
306 if (exclude(path))
307 {
308 return true
309 }
310 }
311 // otherwise check for a simple textual match
312 else
313 {
314 if (exclude === path)
315 {
316 return true
317 }
318 }
319 }
320
321 // no matches found.
322 // returns false so that it isn't undefined (for testing purpose)
323 return false
324 }
325
326 // Checks if the required path should be included in the custom require() hook
327 includes(path, options)
328 {
329 // if "include" parameter isn't specified, then include everything
330 if (!exists(options.include))
331 {
332 return true
333 }
334
335 // for each inclusion case
336 for (let include of options.include)
337 {
338 // supports regular expressions
339 if (include instanceof RegExp)
340 {
341 if (include.test(path))
342 {
343 return true
344 }
345 }
346 // check for a compex logic match
347 else if (typeof include === 'function')
348 {
349 if (include(path))
350 {
351 return true
352 }
353 }
354 // otherwise check for a simple textual match
355 else
356 {
357 if (include === path)
358 {
359 return true
360 }
361 }
362 }
363
364 // no matches found.
365 // returns false so that it isn't undefined (for testing purpose)
366 return false
367 }
368
369 // Waits for webpack-assets.json to be created after Webpack build process finishes
370 //
371 // The callback is called when `webpack-assets.json` has been found
372 // (it's needed for development because `webpack-dev-server`
373 // and your application server are usually run in parallel).
374 //
375 wait_for_assets(done)
376 {
377 // condition check interval
378 const interval = 300 // in milliseconds
379
380 // selfie
381 const tools = this
382
383 // waits for condition to be met, then proceeds
384 function wait_for(condition, proceed)
385 {
386 function check()
387 {
388 // if the condition is met, then proceed
389 if (condition())
390 {
391 return proceed()
392 }
393
394 tools.log.debug(`(${tools.webpack_assets_path} not found)`)
395 tools.log.info('(waiting for the first Webpack build to finish)')
396
397 setTimeout(check, interval)
398 }
399
400 check()
401 }
402
403 // wait for webpack-assets.json to be written to disk by Webpack
404 wait_for(() => fs.existsSync(this.webpack_assets_path), done)
405
406 // allows method chaining
407 return this
408 }
409}
\No newline at end of file