╦═╗ ╔═╗ ╔═╗ ╦ ╦ ╦ ╦═╗ ╔═╗ ╦ ╔╗╔ ╦ ╔═╗ ╔═╗ ╔╦╗ ╔═╗ ╦═╗
╠╦╝ ║╣ ║═╬╗ ║ ║ ║ ╠╦╝ ║╣ ║ ║║║ ║ ║╣ ║ ║ ║ ║ ╠╦╝
╩╚═ ╚═╝ ╚═╝╚ ╚═╝ ╩ ╩╚═ ╚═╝ ╩ ╝╚╝ ╚╝ ╚═╝ ╚═╝ ╩ ╚═╝ ╩╚═
require-injector
Injecting and replacing require()
and import
statement in both NodeJS and browser side JS or Typescript file via packing tool like Webpack.
When it is used for Node, it is a little bit like app-module-path,
when it is used for browser environment JS bundle tool, it is like Webpack resolve.alias
configuration, but more fine-grained.
You may use it as a simple IoC container, which helps you decouple modules. Or if you just want to replace some third-party package's dependency without doing git-fork and create a whole new package.
Be aware that Node.js ECMAScript modules is not supported at this moment.
Installation
npm install require-injector
Node project example
Assume you have project structure like below,
/
├─── src
│ ├─── dir1
│ │ ├─── sub-dir
│ │ │ └─── feature1-1.js
│ │ └─── feature1.js
│ └─── dir2
│ └─── feature2.js
├─── app.js
├─── utils
│ └─── index.js
└─── node_modules
├─── module1/index.js, package.json, ...
└─── module2/index.js, package.json, ...
Injection for local files
In src/dir1/some1.js, there is require()
calling to module1
var m1 = require('module1');
You can inject this require('module1')
with the exports value from module2
.
In your entry js file app.js:
const Injector = require('require-injector').default;
let rj = new Injector({basedir: __dirname});
rj.fromDir('src/dir1')
.substitute('module1', 'module2');
Or in Typescript
import Injector from 'require-injector';
let rj = new Injector({basedir: __dirname});
rj.fromDir('src/dir1')
.substitute('module1', 'module2');
From then on, any file require('module1')
will actually be requiring module2 instead.
Also you can inject it with a value returned from a lazy factory function, or just give a value to it;
rj.fromDir(['src/dir1', 'src/dir2'])
.factory('module1', function(file) { return something;})
.value('module2', 123);
No relative path needed in require()
You may don't need require messy relative path anymore. Image you have a common utils
always be required by different feature folders. Same effect like app-module-path
const Injector = require('require-injector').default;
let rj = new Injector();
rj.fromDir(['src/dir1', 'src/dir2']).factory('_utils', function() {
return require('./utils');
});
Now you have a common fake module name called _utils
to be required from dir1,dir2
In dir1/feature1.js
var utils = require('_utils');
In dir1/sub-dir/feature1-1.js
var utils = require('_utils');
Since v2.0.0
hierarchical directors setting is supported. Injector setting can be configured on
different level directories, lower level directory's setting takes precedence.
rj.fromDir('src/dir1')
.substitute('module1', 'module2');
rj.fromDir('src')
.substitute('module1', 'module3');
All files under src/dir1
will be injected with 'module2'.
Any of other files from src/**/*
will be injected with 'module3'.
Injection for Node packages
You can setup injection for JS file of specific packages, e.g. module1
...
rj.fromPackage('module1')
.substitute('module1-dependencyA', 'anotherPackage');
rj.fromPackage('module1', require('browser-resolve').sync)
.substitute('module1-dependencyA', 'anotherPackage');
Browserify transform
If you are packing files to browser side by Browserify,
let Injector = require('require-injector').default;
let rj = new Injector({noNode: true});
rj.fromPackage('...')
...
var browserify = require('browserify');
var b = browserify();
b.transform(rj.transform, {global: true});
It uses acorn JavaScript language parser to parse each JS file and replace line of require("matchedModule")
.
Set noNode
to true
to disable injection on NodeJS modules, only work as a replacer.
Browserify from command line
browserify entry.js --global-transform
[ require-injector/transform --inject inject.js ]
inject.js
is where you initialize require-injector settings
var rj = require('require-injector').getInstance({noNode: true});
rj.fromDir('folderA')
.value('moduleX', 'anotherPackage')
.factory('moduleY', function() {return something;})
.substitute('moduleZ', 'anotherPackage');
rj.fromPackage('moduleB')
...
Webpack loader
Requires Webpack version above 2.0
The whole module is a Webpack loader, you can use it in webpack.config.js
.
Consider it as more advanced solution for Webpack resolve.alias
option.
e.g. You want to resolve jquery
to zepto
only for a part of source code.
Also we provide a css-loader
to replace "import" syntax:
@import "npm://<package>/path"
@import "~<package>/path"
- LESS like
@import (reference | ...)
syntax
But only work for .substitute()
, .alias()
to replace dependency, and replaceCode(<package>, '')
to delete dependency.
var browserInjector = rj({noNode: true});
browserInjector.fromDir('src/mobile').substitute('jquery', 'zepto');
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
use: [{loader: 'require-injector', options: {injector: browserInjector}],
parser: {
amd: false
}
},
{
test: /\.css$/,
use: [
...
{loader: 'require-injector/css-loader', options: {
injector: browserInjector
}}
]
}
}
};
Webpack-like split loading module replacement: require.ensure()
.substitute()
works for call expression like require.ensure()
Injection setup in gulp script for Webpack like below,
rj({noNode: true, basedir: __dirname});
rj.fromDir('dir1').substitute('replaceMe', 'module1');
In browser side file
require.ensure(['replaceMe', 'module2'], function() {
....
})
will be replaced to
require.ensure(['module1', 'module2'], function() {
....
})
Replacement
You can write your own replacement function for Browserify or Webpack, just call .injectToFile()
,
var fs = require('fs');
var code = fs.readFileSync(filePath, 'utf8');
var replacedCode = rj.injectToFile(filePath, code);
fs.writeFileSync(filePath, replacedCode);
Solution for NodeJS and browser environment
-
For NodeJS, the injector kidnaps Node's native API Module.prototype.require()
, so that each require()
call goes to injector's control, it returns injecting value according to callee file's id (file path).
-
For browsers, if you are packing your code by any tool like Browsersify and Webpack, this module plays a role of tranform
, loader
or replacer
, parsing JS code and replacing require()
expression with stringified injecting value.
You can even create 2 instances, one for Node injection, another for browser side replacement, so they don't interfere each other.
var rj = require('require-injector');
var rjNode = rj();
rjNode.fromPackage('feature1')
.substitute('dependency1', 'module1');
var rjReplace = rj({noNode: true});
rjReplace.fromPackage('feature2')
.substitute('dependency2', 'module2');
ES6 Import syntax and import async syntax
Since v3.0.0, it also supports replacing import syntax to work with Webpack2, you no longer need to use babel to translate import syntax to require()
before require-injector kicks in, which is also for enabling Webpack tree-shaking function.
import defaultMember from "module-name";
import * as name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1 , member2 } from "module-name";
import { member1 , member2 as alias2 , [...] } from "module-name";
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
import "module-name";
Even asyc import syntax import("module-name")
Support as Webapck loader to replace Typescript file
You can either put this loader either behide or before babel loader, it can recoganize
Injection for server side Swig template
We also extend injection function to resource type other than Javascript, if you are using server side Swig template engine,
this injector can work with swig-package-tmpl-loader injection
Injector API
require('require-injector')( {object}
opts )
Must call this function at as beginning as possible of your entry script.
It kidnaps Node's native API Module.prototype.require()
, so every require()
call actually goes to its management.
Parameters
opts
: optional, global options:
opts.basedir
: {string}
default is process.cwd(), used to resolve relative path in .fromDir(path)
opts.resolve
: {function(id)}
, default isreolve.sync
, you may also use Node API require.resolve
or browser-resolve.sync
opts.resolveOpts
: {object}
set a global resolve options which is for .fromPackage(path, opts)
noNode
: {boolean}
default is false, if you only use it as a replacer like Browserify's transform or Webpack's loader, you don't want injection work on NodeJS side, no kidnapping on Module.prototype.require
, just set this property to true
. And this will turn default opts.resolve
to require('browser-resolve').sync
.opts.debug
: {boolean}
if true, log4js will be enabled to print out logs
fromPackage( {string|array}
nodePackageName, {basedir: string}
opts)
Adding one or multiple packages to injection setting, all files under this package's directory will be injectable. This function calls .fromDir()
internally.
Parameters
-
nodePackageName
: Node package's name or array of multiple package names
-
opts
: optional, {basedir}
default is current directory process.cwd()
, lookup package from basedir/node_modules/<pkg>, basedir/../node_modules/<pkg>, ...
,
Underneath, it uses resolve to locate package's root directory, which mean it could not only be a Node package, but also a Browser side package which has a "browser
" property instead of "main
" property in package.json, you may use browserResolve.sync instead of resolve.
returns chainable FactoryMap
fromDir( {string|array}
directory)
Adding one or multiple directories to injection setting, all files under this directory will be injectable.
The internal directory list are sorted and can be binary searched when Module.prototype.require()
is called against each file. So the performance of dynamic injection should be not bad
Parameters
directory
: if this is a relative path, you must call requireInjector({basedir: rootDir})
to tell a base directory
returns chainable FactoryMap
transform(filePath)
A Browserify JS file transform function to replace require()
expression with proper injection.
browserify.transform(rj.transform, {global: true});
returns through-stream
injectToFile({string}
filePath, {string}
code, {object}
ast)
Here "inject" is actually "replacement".
Parsing a matched file to Esprima AST tree, looking for matched require(module)
expression and replacing them with proper values, expression.
Parameters
filePath
: file pathcode
: content of fileast
: optional, if you have already parsed code toesrima AST tree with {range: true}
option, pass it to this function which helps to speed up process by skip parsing again.
returns replaced source code, if there is no injectable require()
, same source code will be returned.
cleanup()
Remove all packages and directories set by .fromDir()
and .fromPackage()
, also release Module.prototype.require()
, injection will stop working.
Events
require-injector extends Node Events
module.
"inject" event
Emitted when a Node module is matched to be injected with something.
rj.on('inject', moduleId => {});
"replace" event
Emitted when injectToFile
is called on injector.
rj.on('replace', (moduleName: string, replacement: string) => {});
"ast" event
In replacement mode, requir-injector use Acorn or Typescript engine to parse JS/JSX, TS/TSX file into AST object, if you want to reuse this AST object, add listerner to this event
FactoryMap API
substitute({string|RegExp}
requiredModule, {string|function}
newModule)
Or
alias(requiredModule, newModule)
Replacing a required module with requiring another module.
Also support npm://package
reference in Swig template tags include
and import
,
check this out swig-package-tmpl-loader injection
It works very like Webpack's resolve.alias
,
it also matches module name which is consist of node package name and specific path
e.g.
When injector is configured as
rj.fromDir('.').alias('moduleA', 'moduleB');
Then the file contains require('moduleA/foo/bar.js')
will be replaced with require('moduleB/foo/bar.js')
Parameters
-
requiredModule
: the original module name which is required for, it can't be relative file path, only supports absolute path, a package name or Regular Expression.
Package name like lodash/throttle
also works, as long as it can be resolved to same absolute path all the time.
-
newModule
: the new module name that is replaced with.
If newModule
is a function, it will be passed in 2 parameters: sourceFilePath
and regexpExecResult
, and must return string value of replaced module name.
returns chainable FactoryMap
factory({string|RegExp}
requiredModule, {function}
factory)
Replacing a required module with a function returned value.
Not work for require.ensure()
Parameters
-
requiredModule
: the original module name which is required for, it can't be a relative file path.
-
factory
: A function invoked with 1 argument: sourceFilePath
and returns a value which then will replace the original module of requiredModule
.
Note: In browser side replacement mode, it replaces entire require('requiredModule')
expression in source code with Immediately-Invoked Function Expression (IIFE) of the factory function.toString()
:
'(' + factory.toString() + ')(sourceFilePath, regexpExecResult)';
In replacement mode, parameter sourceFilePath
will be null by default, since this would expose
original source file path of your file system, if you still want to obtain sourceFilePath
, set option .enableFactoryParamFile
to true
The factory eventually stands in source code, not NodeJS runtime.
Thus you can not have any reference to any closure variable in factory function.
returns chainable FactoryMap
replaceCode({string|RegExp}
moduleName, {string | function}
jsCode)
Arbitrary JS code replacement
Only work in replacement mode, not NodeJs side
import Injector from 'require-injector';
var rjReplace = new Injector({noNode: true});
rjReplace.fromPackage([packageA...])
.replaceCode('foobar', JSON.stringify({foo: 'bar'}));
In which "var foobar = require('foobar');"
is replaced with:
var foobar = {"foo": "bar"};
value({string|RegExp}
requiredModule, {*|function}
value)
Replacing a required module with any object or primitive value.
Not work for require.ensure()
Parameters
-
requiredModule
: the original module name which is required for, it can't be a relative file path.
-
value
: the value to replace requiredModule
exports.
When .injectToFile()
is called or .transform
is used for Browserify, meaning it is not a Node environment, the solution is actually replacing entire require('requiredModule')
‘’ expression with result of JSON.stringify(value)
.
Sometimes, the value is variable reference,
you wouldn't want JSON.stringify
for it, you can use an object expression:
{string}
value.replacement
: The replaced string literal as variable expression, same as what .replaceCode()
does.{object}
value.value
: Node require injection value
rj.fromDir('dir1')
.value('replaceMe', {
replacement: 'window.jQuery',
value: cheerio
})
If value
is a function, it will be passed in 2 parameters: sourceFilePath
and regexpExecResult
, and must return some value.
returns chainable FactoryMap
swigTemplateDir({string}
packageName, {string}
dir)
Replace npm://package
reference in Swig template tags include
and import
,
check this out swig-package-tmpl-loader injection
Now you can require some cool fake module name in your code, and inject/replace them with the real package or value later.
All have been covered by tests.
Anyone willing to contribute for Webpack loader will be very welcome.