1 | import path from 'path'
|
2 | import fs from 'fs'
|
3 |
|
4 | import Require_hacker from 'require-hacker'
|
5 | import Log from './tools/log'
|
6 |
|
7 | import { exists, clone, alias_camel_case } from './helpers'
|
8 | import { 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
|
12 | export 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 |