UNPKG

twreporter-react

Version:

React-Redux site for The Reporter Foundation in Taiwan

455 lines (382 loc) 12.5 kB
// Hacking too much time // Based on Node.js Module class sources: // https://github.com/nodejs/node/blob/master/lib/module.js import fs from 'fs' import path from 'path' import Module from 'module' import Log from './tools/log' import { exists, starts_with, ends_with } from './helpers' import serialize from './tools/serialize-javascript' const require_hacker = { preceding_path_resolvers: [], path_resolvers: [], global_hook_resolved_modules: {}, global_hooks_enabled: true, occupied_file_extensions: new Set(), // logging log: new Log('require-hook', { debug: false }), // this.options.debug // installs a require() path resolver // // resolve - a function which takes two parameters: // // the path to be resolved // the module in which the require() call was originated // // must return either a new path to the require()d module // or it can return nothing to fall back to the original require()d module path // // returns an object with an .unmount() method // resolver(resolve) { validate.resolve(resolve) const resolver = (path, module) => { // resolve the path for this require() call const resolved_path = resolve(path, module) // if no path was resolved - do nothing if (!exists(resolved_path)) { return } // return the path to be require()d return resolved_path } require_hacker.preceding_path_resolvers.push(resolver) const result = { unmount: () => { // javascript arrays still have no .remove() method in the XXI-st century require_hacker.preceding_path_resolvers = require_hacker.preceding_path_resolvers.filter(x => x !== resolver) } } return result }, // installs a global require() hook for all paths // // (if these paths are certain to exist in the filesystem // and if you need only a specific file extension // then use the .hook(extension, resolve) method instead) // // id - a meaningful textual identifier // // resolve - a function which takes two parameters: // // the path to be resolved // the module in which the require() call was originated // // must return either a javascript CommonJS module source code // (i.e. "module.exports = ...", etc) // or it can return nothing to fall back to the original Node.js loader // // returns an object with an .unmount() method // // options: // // precede_node_loader: // // true - this require() hook will intercept all require() calls // before they go into the original Node.js loader // // false - this require() hook will only intercept those require() calls // which failed to be resolved by the original Node.js loader // // default value: true // global_hook(id, resolve, options = {}) { validate.global_hook(id, resolve) const resolver = (path, module) => { // get CommonJS module source code for this require() call const source = resolve(path, module) // if no CommonJS module source code returned - skip this require() hook if (!exists(source)) { return } // CommonJS module source code returned, // so put it into a hash for a corresponding key const resolved_path = `${path}.${id}` // flush require() cache delete require.cache[resolved_path] // put the CommonJS module source code into the hash require_hacker.global_hook_resolved_modules[resolved_path] = source // return the path to be require()d // in order to get the CommonJS module source code return resolved_path } if (options.precede_node_loader === false) { require_hacker.path_resolvers.push(resolver) } else { require_hacker.preceding_path_resolvers.push(resolver) } const hook = this.hook(id, path => { const source = require_hacker.global_hook_resolved_modules[path] delete require_hacker.global_hook_resolved_modules[path] return source }) const result = { unmount: () => { // javascript arrays still have no .remove() method in the XXI-st century require_hacker.preceding_path_resolvers = require_hacker.preceding_path_resolvers.filter(x => x !== resolver) require_hacker.path_resolvers = require_hacker.path_resolvers.filter(x => x !== resolver) hook.unmount() } } return result }, // installs a require() hook for the extension // // extension - a file extension to hook into require()s of // (examples: 'css', 'jpg', 'js') // // resolve - a function that takes two parameters: // // the path requested in the require() call // the module in which the require() call was originated // // must return either a javascript CommonJS module source code // (i.e. "module.exports = ...", etc) // or it can return nothing to fall back to the original Node.js loader // hook(extension, resolve) { this.log.debug(`Hooking into *.${extension} files loading`) // validation validate.extension(extension) validate.resolve(resolve) // occupy file extension this.occupied_file_extensions.add(extension) // dotted extension const dot_extension = `.${extension}` // keep original extension loader const original_loader = Module._extensions[dot_extension] // display a warning in case of extension loader override if (original_loader) { // output a debug message in case of extension loader override, // not a warning, so that it doesn't scare people this.log.debug(`-----------------------------------------------`) this.log.debug(`Overriding an already existing require() hook `) this.log.debug(`for file extension ${dot_extension}`) this.log.debug(`-----------------------------------------------`) } // the list of cached modules const cached_modules = new Set() // Node.js inner API check /* istanbul ignore if */ if (!Module._extensions) { throw new Error('Incompatilbe Node.js version detected: "Module._extensions" array is missing. File an issue on GitHub.') } // set new loader for this extension Module._extensions[dot_extension] = (module, filename) => { this.log.debug(`require() hook fired for ${filename}`) // var source = fs.readFileSync(filename, 'utf8') const source = resolve(filename, module) if (!exists(source)) { this.log.debug(`Fallback to original loader`) // this message would appear if there was no loader // for the extension of the filename if (path.extname(filename) !== dot_extension) { this.log.info(`Trying to load "${path.basename(filename)}" as a "*${dot_extension}"`) } // load the file with the original loader return (original_loader || Module._extensions['.js'])(module, filename) } // add this file path to the list of cached modules cached_modules.add(filename) // Node.js inner API check /* istanbul ignore if */ if (!module._compile) { throw new Error('Incompatilbe Node.js version detected: "Module.prototype._compile" function is missing. File an issue on GitHub.') } // compile javascript module from its source // https://github.com/nodejs/node/blob/master/lib/module.js#L379 module._compile(source, filename) } const result = { // uninstall the hook unmount: () => { // clear require() cache for this file extension for (let path of cached_modules) { delete require.cache[path] } // mount the original loader for this file extension Module._extensions[dot_extension] = original_loader // free file extension this.occupied_file_extensions.delete(extension) } } return result }, // returns a CommonJS modules source. to_javascript_module_source(anything) { // if the asset source wasn't found - return an empty CommonJS module if (!exists(anything)) { return 'module.exports = undefined' } // if it's already a common js module source if (typeof anything === 'string' && is_a_module_declaration(anything)) { return anything } // generate javascript module source code based on the `source` variable return 'module.exports = ' + serialize(anything) }, // resolves a requireable `path` to a real filesystem path relative to the `module` // (resolves `npm link`, etc) resolve(path_to_resolve, module) { // Module._resolveFilename existence check is perfomed outside of this method try { require_hacker.global_hooks_enabled = false return original_resolveFilename(path_to_resolve, module) } finally { require_hacker.global_hooks_enabled = true } } } // validation const validate = { extension(extension) { // if (typeof extension !== 'string') // { // throw new Error(`Expected string extension. Got ${extension}`) // } if (path.extname(`test.${extension}`) !== `.${extension}`) { throw new Error(`Invalid file extension "${extension}"`) } // check if the file extension is already occupied if (require_hacker.occupied_file_extensions.has(extension)) { throw new Error(`File extension "${extension}" is already occupied by require-hacker`) } }, resolve(resolve) { if (typeof resolve !== 'function') { throw new Error(`Resolve should be a function. Got "${resolve}"`) } }, global_hook(id, resolver) { if (!id) { throw new Error(`You must specify global hook id`) } if (path.extname(`test.${id}`) !== `.${id}`) { throw new Error(`Invalid global hook id "${id}". Expected a valid file extension.`) } // check if the file extension is already occupied if (require_hacker.occupied_file_extensions.has(id)) { throw new Error(`File extension "${id}" is already occupied by require-hacker`) } validate.resolve(resolver) } } // Node.js inner API check /* istanbul ignore if */ if (!Module._resolveFilename) { throw new Error('Incompatilbe Node.js version detected: "Module._resolveFilename" function is missing. File an issue on GitHub.') } // Node.js inner API check /* istanbul ignore if */ if (!Module._findPath) { throw new Error('Incompatilbe Node.js version detected: "Module._findPath" function is missing. File an issue on GitHub.') } // the module in which the require() call originated let require_caller // instrument Module._resolveFilename // https://github.com/nodejs/node/blob/master/lib/module.js#L322 // // `arguments` would conflict with Babel, therefore `...parameters` // // const native_module = require('native_module') const original_resolveFilename = Module._resolveFilename Module._resolveFilename = function(...parameters) { const request = parameters[0] const parent = parameters[1] // take note of the require() caller require_caller = parent return original_resolveFilename.apply(this, parameters) } // instrument Module._findPath // https://github.com/nodejs/node/blob/master/lib/module.js#L335-L341 // // `arguments` would conflict with Babel, therefore `...parameters` // const original_findPath = Module._findPath Module._findPath = (...parameters) => { const request = parameters[0] // const paths = parameters[1] // preceeding resolvers if (require_hacker.global_hooks_enabled) { for (let resolver of require_hacker.preceding_path_resolvers) { const resolved_path = resolver(request, require_caller) if (exists(resolved_path)) { return resolved_path } } } // original Node.js loader const filename = original_findPath.apply(undefined, parameters) if (filename !== false) { return filename } // rest resolvers if (require_hacker.global_hooks_enabled) { for (let resolver of require_hacker.path_resolvers) { const resolved = resolver.resolve(request, require_caller) if (exists(resolved)) { return resolved } } } return false } // detect if it is a CommonJS module declaration function is_a_module_declaration(text) { return text.indexOf('module.exports = ') === 0 || /\s+module\.exports = .+/.test(text) } export default require_hacker