react-magnetic-di
Advanced tools
Comparing version 2.3.4 to 3.0.0
@@ -6,3 +6,4 @@ "use strict"; | ||
PACKAGE_FUNCTION: 'di', | ||
INJECT_FUNCTION: 'injectable', | ||
HOC_FUNCTION: 'withDi' | ||
}; |
"use strict"; | ||
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } | ||
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } | ||
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } | ||
function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } | ||
var _require = require('./constants'), | ||
PACKAGE_NAME = _require.PACKAGE_NAME, | ||
INJECT_FUNCTION = _require.INJECT_FUNCTION, | ||
PACKAGE_FUNCTION = _require.PACKAGE_FUNCTION, | ||
HOC_FUNCTION = _require.HOC_FUNCTION; | ||
var processDIReference = require('./processor-di'); | ||
var processDiDeclaration = require('./processor-di'); | ||
var processHOCReference = require('./processor-hoc'); | ||
var _require2 = require('./utils'), | ||
isEnabledEnv = _require2.isEnabledEnv; | ||
assert = _require2.assert, | ||
createNamedImport = _require2.createNamedImport, | ||
collectDiReferencePaths = _require2.collectDiReferencePaths, | ||
collectDepsReferencePaths = _require2.collectDepsReferencePaths, | ||
isExcludedFile = _require2.isExcludedFile, | ||
isEnabledEnv = _require2.isEnabledEnv, | ||
hasDisableComment = _require2.hasDisableComment; | ||
var State = /*#__PURE__*/function () { | ||
function State(path) { | ||
var isExcluded = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; | ||
_classCallCheck(this, State); | ||
_defineProperty(this, "locations", new WeakMap()); | ||
_defineProperty(this, "aliases", new Map()); | ||
_defineProperty(this, "diIdentifier", null); | ||
_defineProperty(this, "programPath", null); | ||
this.programPath = path; | ||
this.isExcluded = isExcluded; | ||
} | ||
_createClass(State, [{ | ||
key: "findDiIndentifier", | ||
value: function findDiIndentifier(t, body, scope) { | ||
var diImportNode = body.find(function (n) { | ||
return t.isImportDeclaration(n) && n.source.value === PACKAGE_NAME; | ||
}); | ||
var diImportSpecifier = diImportNode === null || diImportNode === void 0 ? void 0 : diImportNode.specifiers.find(function (s) { | ||
return s.imported && s.imported.name === PACKAGE_FUNCTION; | ||
}); | ||
this.diIdentifier = (diImportSpecifier === null || diImportSpecifier === void 0 ? void 0 : diImportSpecifier.local) || scope.generateUidIdentifier(PACKAGE_FUNCTION); | ||
} | ||
}, { | ||
key: "getValueForPath", | ||
value: function getValueForPath(fnPath) { | ||
return this.locations.get(fnPath) || this.locations.get(fnPath.node); | ||
} | ||
}, { | ||
key: "setValueForPath", | ||
value: function setValueForPath(fnPath, value) { | ||
this.locations.set(fnPath, value); | ||
this.locations.set(fnPath.node, value); | ||
} | ||
}, { | ||
key: "getValueOrInit", | ||
value: function getValueOrInit(fnPath) { | ||
// we need both node and path as either might get replaced | ||
if (!this.locations.has(fnPath) && !this.locations.has(fnPath.node)) { | ||
this.setValueForPath(fnPath, { | ||
diRef: null, | ||
dependencyRefs: new Set() | ||
}); | ||
} | ||
return this.getValueForPath(fnPath); | ||
} | ||
}, { | ||
key: "moveValueForPath", | ||
value: function moveValueForPath(fnPath, newFnPath) { | ||
if (newFnPath && newFnPath.isFunction()) { | ||
this.setValueForPath(newFnPath, this.getValueForPath(fnPath)); | ||
} | ||
} | ||
}, { | ||
key: "removeValueForPath", | ||
value: function removeValueForPath(fnPath) { | ||
this.locations["delete"](fnPath); | ||
this.locations["delete"](fnPath.node); | ||
} | ||
}, { | ||
key: "getAlias", | ||
value: function getAlias(name, scope) { | ||
if (!this.aliases.has(name)) { | ||
this.aliases.set(name, scope.generateUid(name)); | ||
} | ||
return this.aliases.get(name); | ||
} | ||
}, { | ||
key: "addDi", | ||
value: function addDi(diRef) { | ||
var parentFnPath = diRef.getFunctionParent(); | ||
assert.isValidLocation(parentFnPath, diRef); | ||
var value = this.getValueOrInit(parentFnPath); | ||
value.diRef = diRef; | ||
} | ||
}, { | ||
key: "addDependency", | ||
value: function addDependency(depRef) { | ||
var _this = this; | ||
depRef.findParent(function (p) { | ||
var _p$parentPath, _p$parentPath$node, _p$parentPath$node$ca; | ||
if (p.isFunction() && ((_p$parentPath = p.parentPath) === null || _p$parentPath === void 0 ? void 0 : (_p$parentPath$node = _p$parentPath.node) === null || _p$parentPath$node === void 0 ? void 0 : (_p$parentPath$node$ca = _p$parentPath$node.callee) === null || _p$parentPath$node$ca === void 0 ? void 0 : _p$parentPath$node$ca.name) !== INJECT_FUNCTION) { | ||
// add ref for every function scope up to the root one | ||
_this.getValueOrInit(p).dependencyRefs.add(depRef); | ||
} | ||
}); | ||
} | ||
}, { | ||
key: "addDiImport", | ||
value: function addDiImport(t) { | ||
if (this.diIdentifier.loc) return; | ||
var statement = createNamedImport(t, PACKAGE_NAME, [PACKAGE_FUNCTION], [this.diIdentifier]); | ||
this.programPath.unshiftContainer('body', statement); | ||
// after adding, make this function a noop | ||
this.addDiImport = function () {}; | ||
} | ||
}]); | ||
return State; | ||
}(); | ||
module.exports = function (babel) { | ||
var t = babel.types; | ||
var stateCache = new WeakMap(); | ||
return { | ||
name: PACKAGE_NAME, | ||
visitor: { | ||
ImportDeclaration: function ImportDeclaration(path, _ref) { | ||
var _ref$opts = _ref.opts, | ||
opts = _ref$opts === void 0 ? {} : _ref$opts; | ||
Program: function Program(path, _ref) { | ||
var opts = _ref.opts, | ||
file = _ref.file; | ||
var isEnabled = isEnabledEnv(opts.enabledEnvs); | ||
var isExcluded = isExcludedFile(opts.exclude, file.opts.filename); | ||
var state = new State(path, isExcluded); | ||
state.findDiIndentifier(t, path.node.body, path.scope); | ||
collectDiReferencePaths(t, state.diIdentifier, path.scope).forEach(function (p, i, arr) { | ||
var _arr; | ||
var hasMulti = p.getFunctionParent() === ((_arr = arr[i + 1]) === null || _arr === void 0 ? void 0 : _arr.getFunctionParent()); | ||
if (isEnabled && !hasMulti) state.addDi(p);else p.parentPath.remove(); | ||
}); | ||
if (!isEnabled) return; | ||
collectDepsReferencePaths(t, path.get('body')).forEach(function (p) { | ||
return state.addDependency(p); | ||
}); | ||
// TODO | ||
// Should we add collection of globals to di via path.scope.globals? | ||
stateCache.set(file, state); | ||
}, | ||
Function: function Function(path, _ref2) { | ||
var file = _ref2.file; | ||
var state = stateCache.get(file); | ||
var locationValue = state === null || state === void 0 ? void 0 : state.getValueForPath(path); | ||
var shouldDi = !(state !== null && state !== void 0 && state.isExcluded) && !hasDisableComment(path) || (locationValue === null || locationValue === void 0 ? void 0 : locationValue.diRef); | ||
// process only if function is a candidate to host di | ||
if (!state || !locationValue || !shouldDi) return; | ||
// convert arrow function returns as di needs a block | ||
if (!t.isBlockStatement(path.node.body)) { | ||
var bodyPath = path.get('body'); | ||
// convert arrow function return to block | ||
bodyPath.replaceWith(t.blockStatement([t.returnStatement(path.node.body)])); | ||
// we make sure that if body was a function that needs di() | ||
// we update the reference as new function path has been created | ||
state.moveValueForPath(bodyPath, path.get('body.body.0.argument')); | ||
} | ||
// create di declaration | ||
processDiDeclaration(t, path, locationValue, state); | ||
// once done, remove from cache so if babel calls function again we do not reprocess | ||
state.removeValueForPath(path); | ||
}, | ||
ImportDeclaration: function ImportDeclaration(path) { | ||
// first we look at the imports: | ||
// if not our package and not the right function, ignore | ||
var importSource = path.node.source.value; | ||
var importDISpecifier = path.node.specifiers.find(function (s) { | ||
return s.imported && s.imported.name === PACKAGE_FUNCTION; | ||
}); | ||
if (path.node.source.value !== PACKAGE_NAME) return; | ||
var importHOCSpecifier = path.node.specifiers.find(function (s) { | ||
return s.imported && s.imported.name === HOC_FUNCTION; | ||
}); | ||
if (importSource !== PACKAGE_NAME) return; | ||
if (importDISpecifier) { | ||
// then we locate all usages of the method | ||
// ensuring we affect only locations where it is called | ||
var methodIdentifier = importDISpecifier.local.name; | ||
var binding = path.scope.getBinding(methodIdentifier); | ||
if (!binding) return; | ||
var references = binding.referencePaths.filter(function (ref) { | ||
return t.isCallExpression(ref.container); | ||
}); | ||
var isEnabled = isEnabledEnv() || Boolean(opts.forceEnable); | ||
if (!importHOCSpecifier) return; | ||
// for each of that location we apply a tranformation | ||
references.forEach(function (ref) { | ||
return processDIReference(t, ref, isEnabled); | ||
}); | ||
} | ||
if (importHOCSpecifier) { | ||
// then we locate all usages of the method | ||
// ensuring we affect only locations where it is called | ||
var _methodIdentifier = importHOCSpecifier.local.name; | ||
var _binding = path.scope.getBinding(_methodIdentifier); | ||
if (!_binding) return; | ||
var _references = _binding.referencePaths.filter(function (ref) { | ||
return t.isCallExpression(ref.container); | ||
}); | ||
// then we locate all usages of the method | ||
// ensuring we affect only locations where it is called | ||
var methodIdentifier = importHOCSpecifier.local.name; | ||
var binding = path.scope.getBinding(methodIdentifier); | ||
if (!binding) return; | ||
var references = binding.referencePaths.filter(function (ref) { | ||
return t.isCallExpression(ref.container); | ||
}); | ||
// for each of that location we apply a tranformation | ||
_references.forEach(function (ref) { | ||
return processHOCReference(t, ref); | ||
}); | ||
} | ||
// for each of that location we apply a tranformation | ||
references.forEach(function (ref) { | ||
return processHOCReference(t, ref); | ||
}); | ||
} | ||
@@ -60,0 +198,0 @@ } |
@@ -6,34 +6,66 @@ "use strict"; | ||
getComponentDeclaration = _require.getComponentDeclaration; | ||
function processReference(t, ref, isEnabled) { | ||
assert.isValidBlock(t, ref); | ||
assert.isValidCall(t, ref); | ||
function processReference(t, path, locationValue, state) { | ||
var _locationValue$diRef, _locationValue$diRef$, _locationValue$diRef$2; | ||
var self = getComponentDeclaration(t, path.scope); | ||
var bodyPath = path.get('body'); | ||
// from the arguments of the method we generate the list of dependency identifiers | ||
var args = ref.container.arguments; | ||
var dependencyIdentifiers = args.map(function (v) { | ||
return t.identifier(v.name); | ||
// Build list of dependencies | ||
// combining used imports/exports in this function block | ||
// with existing di expression (if any) | ||
var depNames = []; | ||
Array.from(locationValue.dependencyRefs).forEach(function (n) { | ||
var _n$node; | ||
var name = (_n$node = n.node) === null || _n$node === void 0 ? void 0 : _n$node.name; | ||
// quick check that the path is not detached | ||
if (!name || !n.parentPath) return; | ||
// Some babel plugins might rename imports (eg emotion) and references break | ||
// For now we skip, but ideally we would refresh the reference | ||
if (!bodyPath.scope.getBinding(name)) return; | ||
// Ensure we do not duplicate and di() self name | ||
if (depNames.includes(name) || name === (self === null || self === void 0 ? void 0 : self.name)) return; | ||
depNames.push(name); | ||
}); | ||
var statement = ref.getStatementParent(); | ||
(_locationValue$diRef = locationValue.diRef) === null || _locationValue$diRef === void 0 ? void 0 : (_locationValue$diRef$ = _locationValue$diRef.container) === null || _locationValue$diRef$ === void 0 ? void 0 : (_locationValue$diRef$2 = _locationValue$diRef$.arguments) === null || _locationValue$diRef$2 === void 0 ? void 0 : _locationValue$diRef$2.forEach(function (n) { | ||
assert.isValidArgument(t, n, locationValue.diRef, self); | ||
if (!depNames.includes(n.name)) depNames.push(n.name); | ||
}); | ||
depNames.sort(); | ||
// if should not be enabled, just remove the statement and exit | ||
if (!isEnabled) { | ||
statement.remove(); | ||
return; | ||
// if there are no valid candidates, exit | ||
if (!depNames.length) return; | ||
var elements = depNames.map(function (v) { | ||
return t.identifier(v); | ||
}); | ||
var args = depNames.map(function (v) { | ||
return t.identifier(v); | ||
}); | ||
// add di there | ||
var declaration = t.variableDeclaration('const', [t.variableDeclarator(t.arrayPattern(elements), t.callExpression(state.diIdentifier, [t.arrayExpression(args), self ? t.identifier(self.name) : t.nullLiteral()]))]); | ||
// We inject the new declaration either by replacing existing di | ||
// or by replacing and adding the statement at the top. | ||
// We need replacing to ensure we get a path so that registerDeclaration works | ||
var declarationPath; | ||
if (locationValue.diRef) { | ||
declarationPath = locationValue.diRef.getStatementParent(); | ||
declarationPath.replaceWith(declaration); | ||
} else { | ||
bodyPath.unshiftContainer('body', declaration); | ||
declarationPath = bodyPath.get('body.0'); | ||
} | ||
// generating variable declarations with array destructuring | ||
// assigning them the result of the method call, with arguments | ||
// now wrapped in an array | ||
statement.replaceWith(t.variableDeclaration('const', [t.variableDeclarator(t.arrayPattern(dependencyIdentifiers), t.callExpression(ref.node, [t.arrayExpression(args), getComponentDeclaration(t, ref.scope) || t.nullLiteral()]))])); | ||
ref.scope.registerDeclaration(statement); | ||
args.forEach(function (argIdentifier) { | ||
// for each argument we get the dependency variable name | ||
bodyPath.scope.registerDeclaration(declarationPath); | ||
var argsPaths = declarationPath.get('declarations.0.init.arguments.0.elements'); | ||
argsPaths.forEach(function (argPath) { | ||
// For each argument we get the dependency variable name | ||
// then we rename it locally so we get a new unique identifier. | ||
// Then we manually revert just the argument identifier name back, | ||
// so it still points to the original dependency identifier name | ||
var name = argIdentifier.name; | ||
ref.scope.rename(name); | ||
argIdentifier.name = name; | ||
var name = argPath.node.name; | ||
bodyPath.scope.rename(name, state.getAlias(name, bodyPath.scope)); | ||
argPath.replaceWith(t.identifier(name)); | ||
}); | ||
// ensure we add di import | ||
state.addDiImport(t); | ||
} | ||
module.exports = processReference; |
"use strict"; | ||
var processReference = function processReference(t, ref) { | ||
var _nextSibling$node$exp, _nextSibling$node$exp2, _nextSibling$node$exp3; | ||
var container = ref.parentPath.container; | ||
if (container.type !== 'VariableDeclarator') return; | ||
var containerID = container.id; | ||
// check if display name already set by someone else right after def | ||
var nextSibling = ref.getStatementParent().getNextSibling(); | ||
if (nextSibling.isExpressionStatement() && ((_nextSibling$node$exp = nextSibling.node.expression) === null || _nextSibling$node$exp === void 0 ? void 0 : (_nextSibling$node$exp2 = _nextSibling$node$exp.left) === null || _nextSibling$node$exp2 === void 0 ? void 0 : (_nextSibling$node$exp3 = _nextSibling$node$exp2.property) === null || _nextSibling$node$exp3 === void 0 ? void 0 : _nextSibling$node$exp3.name) == 'displayName') { | ||
return; | ||
} | ||
ref.getStatementParent().insertAfter(t.expressionStatement(t.assignmentExpression('=', t.memberExpression(containerID, t.identifier('displayName')), t.stringLiteral(containerID.name)))); | ||
}; | ||
module.exports = processReference; |
"use strict"; | ||
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } | ||
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } | ||
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } | ||
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } | ||
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } | ||
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } | ||
var _require = require('./constants'), | ||
PACKAGE_NAME = _require.PACKAGE_NAME; | ||
var getComponentDeclaration = function getComponentDeclaration(t, scope) { | ||
@@ -11,25 +19,17 @@ // function declarations | ||
if (scope.parentBlock.type.includes('Class')) return scope.parent.block.id; | ||
return null; | ||
}; | ||
var assert = { | ||
isValidBlock: function isValidBlock(t, ref) { | ||
var block = ref.scope.block; | ||
if (!t.isFunctionDeclaration(block) && !t.isFunctionExpression(block) && !t.isArrowFunctionExpression(block) && !t.isClassMethod(block)) { | ||
throw ref.buildCodeFrameError('Invalid di(...) call: must be inside a render function of a component. '); | ||
} | ||
}, | ||
isValidCall: function isValidCall(t, ref) { | ||
if (!ref.container.arguments.length) { | ||
throw ref.buildCodeFrameError('Invalid di(...) arguments: must be called with at least one argument. '); | ||
} | ||
if (!ref.container.arguments.every(function (node) { | ||
return t.isIdentifier(node); | ||
})) { | ||
isValidArgument: function isValidArgument(t, node, ref, self) { | ||
if (!t.isIdentifier(node)) { | ||
throw ref.buildCodeFrameError('Invalid di(...) arguments: must be called with plain identifiers. '); | ||
} | ||
var decl = getComponentDeclaration(t, ref.scope); | ||
if (decl && ref.container.arguments.some(function (v) { | ||
return v.name === decl.name; | ||
})) { | ||
if (node.name === (self === null || self === void 0 ? void 0 : self.name)) { | ||
throw ref.buildCodeFrameError('Invalid di(...) call: cannot inject self.'); | ||
} | ||
}, | ||
isValidLocation: function isValidLocation(path, ref) { | ||
if (!path) { | ||
throw ref.buildCodeFrameError('Invalid di(...) call: must be inside a render function of a component. '); | ||
} | ||
} | ||
@@ -44,10 +44,91 @@ }; | ||
}; | ||
function collectDepsReferencePaths(t, bodyPaths) { | ||
var references = []; | ||
function addRef(path) { | ||
var _ref = path.scope.getBinding(path) || {}, | ||
_ref$referencePaths = _ref.referencePaths, | ||
referencePaths = _ref$referencePaths === void 0 ? [] : _ref$referencePaths; | ||
references.push.apply(references, _toConsumableArray(referencePaths)); | ||
} | ||
// we could use scope.bindings to get all top level bindings | ||
// but it is hard to track local only vs later exported values | ||
bodyPaths.forEach(function (path) { | ||
if (path.isImportDeclaration()) { | ||
if (path.node.importKind === 'type') return; | ||
if (path.node.source.value === PACKAGE_NAME) return; | ||
path.get('specifiers').forEach(function (sp) { | ||
if (sp.node.importKind === 'type') return; | ||
if (sp.isImportDefaultSpecifier() || sp.isImportSpecifier()) { | ||
addRef(sp.get('local')); | ||
} | ||
}); | ||
} | ||
if (path.isExportNamedDeclaration()) { | ||
if (path.node.exportKind === 'type') return; | ||
if (path.node.declaration) { | ||
if (path.get('declaration.id').isIdentifier()) { | ||
addRef(path.get('declaration.id')); | ||
} else { | ||
path.get('declaration.declarations').forEach(function (dp) { | ||
if (dp.get('id').isIdentifier()) { | ||
addRef(dp.get('id')); | ||
} | ||
}); | ||
} | ||
} else { | ||
path.get('specifiers').forEach(function (sp) { | ||
if (sp.node.exportKind === 'type') return; | ||
if (sp.get('local').isIdentifier()) { | ||
addRef(sp.get('local')); | ||
} | ||
}); | ||
} | ||
} | ||
if (path.isExportDefaultDeclaration()) { | ||
if (path.node.exportKind === 'type') return; | ||
var ref = path.get('declaration').isIdentifier() ? path.get('declaration') : path.get('declaration.id'); | ||
addRef(ref); | ||
} | ||
}); | ||
return references; | ||
} | ||
function collectDiReferencePaths(t, identifier, scope) { | ||
// we locate all usages of the method | ||
var _ref2 = scope.getBinding(identifier.name) || {}, | ||
_ref2$referencePaths = _ref2.referencePaths, | ||
referencePaths = _ref2$referencePaths === void 0 ? [] : _ref2$referencePaths; | ||
return referencePaths.filter(function (ref) { | ||
return t.isCallExpression(ref.container); | ||
}); | ||
} | ||
var isExcludedFile = function isExcludedFile() { | ||
var exclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; | ||
var filename = arguments.length > 1 ? arguments[1] : undefined; | ||
var excludes = [].concat(exclude).map(function (v) { | ||
return v instanceof RegExp ? v : new RegExp(v.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')); | ||
}); | ||
return excludes.some(function (rx) { | ||
return rx.test(filename); | ||
}); | ||
}; | ||
var isEnabledEnv = function isEnabledEnv() { | ||
return ['development', 'test'].includes(process.env.BABEL_ENV) || ['development', 'test'].includes(process.env.NODE_ENV) || !process.env.BABEL_ENV && !process.env.NODE_ENV; | ||
var enabledEnvs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['development', 'test']; | ||
return enabledEnvs.includes(process.env.BABEL_ENV) || enabledEnvs.includes(process.env.NODE_ENV); | ||
}; | ||
var hasDisableComment = function hasDisableComment(path) { | ||
var _path$node, _path$node$body, _path$node2, _path$node2$body, _path$node2$body$body, _path$node2$body$body2; | ||
return [].concat(_toConsumableArray(((_path$node = path.node) === null || _path$node === void 0 ? void 0 : (_path$node$body = _path$node.body) === null || _path$node$body === void 0 ? void 0 : _path$node$body.leadingComments) || []), _toConsumableArray(((_path$node2 = path.node) === null || _path$node2 === void 0 ? void 0 : (_path$node2$body = _path$node2.body) === null || _path$node2$body === void 0 ? void 0 : (_path$node2$body$body = _path$node2$body.body) === null || _path$node2$body$body === void 0 ? void 0 : (_path$node2$body$body2 = _path$node2$body$body[0]) === null || _path$node2$body$body2 === void 0 ? void 0 : _path$node2$body$body2.leadingComments) || [])).some(function (c) { | ||
return c.value.includes('di-ignore'); | ||
}); | ||
}; | ||
module.exports = { | ||
getComponentDeclaration: getComponentDeclaration, | ||
assert: assert, | ||
createNamedImport: createNamedImport, | ||
isEnabledEnv: isEnabledEnv | ||
collectDiReferencePaths: collectDiReferencePaths, | ||
collectDepsReferencePaths: collectDepsReferencePaths, | ||
getComponentDeclaration: getComponentDeclaration, | ||
isEnabledEnv: isEnabledEnv, | ||
isExcludedFile: isExcludedFile, | ||
hasDisableComment: hasDisableComment | ||
}; |
"use strict"; | ||
var order = require('./rules/order'); | ||
var exhaustiveInject = require('./rules/exhaustive-inject'); | ||
var noDuplicate = require('./rules/no-duplicate'); | ||
var noExtraneous = require('./rules/no-extraneous'); | ||
var noRestrictedInjectable = require('./rules/no-restricted-injectable'); | ||
var sortDependencies = require('./rules/sort-dependencies'); | ||
@@ -11,7 +11,7 @@ module.exports = { | ||
order: order, | ||
'exhaustive-inject': exhaustiveInject, | ||
'no-duplicate': noDuplicate, | ||
'no-extraneous': noExtraneous, | ||
'no-restricted-injectable': noRestrictedInjectable, | ||
'sort-dependencies': sortDependencies | ||
} | ||
}; |
@@ -10,3 +10,3 @@ "use strict"; | ||
docs: { | ||
description: 'Enforce injectable definition at the top of the block', | ||
description: 'Enforce di() call expression at the top of the block', | ||
category: 'Possible Errors', | ||
@@ -18,3 +18,3 @@ recommended: true | ||
messages: { | ||
wrongOrder: 'Injectables should be defined at the top of their scope ' + 'to avoid partial replacements and variables clashing' | ||
wrongOrder: 'di() calls should be defined at the top of their scope ' + 'to avoid partial replacements and variables clashing' | ||
} | ||
@@ -21,0 +21,0 @@ }, |
"use strict"; | ||
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } | ||
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } | ||
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } | ||
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } | ||
function _iterableToArrayLimit(arr, i) { var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; if (null != _i) { var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; try { if (_x = (_i = _i.call(arr)).next, 0 === i) { if (Object(_i) !== _i) return; _n = !1; } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); } catch (err) { _d = !0, _e = err; } finally { try { if (!_n && null != _i["return"] && (_r = _i["return"](), Object(_r) !== _r)) return; } finally { if (_d) throw _e; } } return _arr; } } | ||
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } | ||
var PACKAGE_NAME = 'react-magnetic-di'; | ||
var PACKAGE_FUNCTION = 'di'; | ||
var INJECT_FUNCTION = 'injectable'; | ||
var isDiStatement = function isDiStatement(stm, spec) { | ||
return stm.type === 'ExpressionStatement' && stm.expression && stm.expression.callee && stm.expression.callee.name === spec.name; | ||
}; | ||
var isHookName = function isHookName(node) { | ||
return /^use[A-Z0-9].*$/.test(node.name); | ||
var calcImportSource = function calcImportSource(src) { | ||
var _src$split = src.split('/'), | ||
_src$split2 = _slicedToArray(_src$split, 2), | ||
ns = _src$split2[0], | ||
_src$split2$ = _src$split2[1], | ||
value = _src$split2$ === void 0 ? '' : _src$split2$; | ||
return ns.startsWith('@') ? ns + '/' + value : ns; | ||
}; | ||
var isComponentName = function isComponentName(node) { | ||
return !/^[a-z]/.test(node.name); | ||
}; | ||
var isLocalVariable = function isLocalVariable(node, scope) { | ||
do { | ||
var _scope; | ||
// if we reach module/global scope then is not local | ||
if (scope.type === 'module' || scope.type === 'global') return false; | ||
var isLocal = (_scope = scope) === null || _scope === void 0 ? void 0 : _scope.variables.some(function (v) { | ||
return v.name === node.name; | ||
var getImportIdentifiers = function getImportIdentifiers(node, pkgName, impNames) { | ||
var importSource = calcImportSource(node.source.value); | ||
var importSpecifiers = node.specifiers.filter(function (s) { | ||
return s.imported && (!impNames || impNames.includes(s.imported.name)); | ||
}); | ||
if (importSource === pkgName && importSpecifiers.length) { | ||
return importSpecifiers.map(function (s) { | ||
return s.local; | ||
}); | ||
if (isLocal) return true; | ||
// eslint-disable-next-line no-cond-assign | ||
} while (scope = scope.upper); | ||
return false; | ||
}; | ||
var getDiIdentifier = function getDiIdentifier(node) { | ||
var importSource = node.source.value; | ||
var importSpecifier = node.specifiers.find(function (s) { | ||
return s.imported && s.imported.name === PACKAGE_FUNCTION; | ||
}); | ||
if (importSource.startsWith(PACKAGE_NAME) && importSpecifier) { | ||
return importSpecifier.local; | ||
} | ||
return null; | ||
}; | ||
var getDiIdentifier = function getDiIdentifier(n) { | ||
var _getImportIdentifiers; | ||
return (_getImportIdentifiers = getImportIdentifiers(n, PACKAGE_NAME, [PACKAGE_FUNCTION])) === null || _getImportIdentifiers === void 0 ? void 0 : _getImportIdentifiers[0]; | ||
}; | ||
var getInjectIdentifier = function getInjectIdentifier(n) { | ||
var _getImportIdentifiers2; | ||
return (_getImportIdentifiers2 = getImportIdentifiers(n, PACKAGE_NAME, [INJECT_FUNCTION])) === null || _getImportIdentifiers2 === void 0 ? void 0 : _getImportIdentifiers2[0]; | ||
}; | ||
var getDiStatements = function getDiStatements(node, diIdentifier) { | ||
@@ -51,7 +57,2 @@ return (node.body || []).reduce(function (acc, statement) { | ||
}; | ||
var getParentDiStatements = function getParentDiStatements(node, diIdentifier) { | ||
var parentBlock = getParentDiBlock(node, diIdentifier); | ||
if (parentBlock) return getDiStatements(parentBlock, diIdentifier); | ||
return []; | ||
}; | ||
var getDiVars = function getDiVars(statements) { | ||
@@ -64,10 +65,8 @@ return statements.reduce(function (acc, s) { | ||
isDiStatement: isDiStatement, | ||
isHookName: isHookName, | ||
isComponentName: isComponentName, | ||
isLocalVariable: isLocalVariable, | ||
getDiIdentifier: getDiIdentifier, | ||
getImportIdentifiers: getImportIdentifiers, | ||
getInjectIdentifier: getInjectIdentifier, | ||
getDiStatements: getDiStatements, | ||
getParentDiBlock: getParentDiBlock, | ||
getParentDiStatements: getParentDiStatements, | ||
getDiVars: getDiVars | ||
}; |
@@ -12,18 +12,18 @@ "use strict"; | ||
}); | ||
Object.defineProperty(exports, "di", { | ||
Object.defineProperty(exports, "debug", { | ||
enumerable: true, | ||
get: function get() { | ||
return _consumer.di; | ||
return _utils.debug; | ||
} | ||
}); | ||
Object.defineProperty(exports, "injectable", { | ||
Object.defineProperty(exports, "di", { | ||
enumerable: true, | ||
get: function get() { | ||
return _utils.injectable; | ||
return _consumer.di; | ||
} | ||
}); | ||
Object.defineProperty(exports, "mock", { | ||
Object.defineProperty(exports, "injectable", { | ||
enumerable: true, | ||
get: function get() { | ||
return _utils.mock; | ||
return _injectable.injectable; | ||
} | ||
@@ -52,3 +52,4 @@ }); | ||
var _provider = require("./react/provider"); | ||
var _injectable = require("./react/injectable"); | ||
var _utils = require("./react/utils"); | ||
var _stats = require("./react/stats"); |
@@ -6,6 +6,6 @@ "use strict"; | ||
}); | ||
exports.PACKAGE_NAME = exports.KEY = void 0; | ||
var KEY = Symbol["for"]('di'); | ||
exports.KEY = KEY; | ||
exports.diRegistry = exports.PACKAGE_NAME = void 0; | ||
var diRegistry = new WeakMap(); | ||
exports.diRegistry = diRegistry; | ||
var PACKAGE_NAME = 'react-magnetic-di'; | ||
exports.PACKAGE_NAME = PACKAGE_NAME; |
@@ -7,8 +7,6 @@ "use strict"; | ||
exports.di = di; | ||
var _constants = require("./constants"); | ||
var _context = require("./context"); | ||
var _global = require("./global"); | ||
var _utils = require("./utils"); | ||
function di(deps, target) { | ||
// check if babel plugin has been added | ||
// check if babel plugin has been added othrewise this is a noop | ||
if (Array.isArray(deps)) { | ||
@@ -23,8 +21,3 @@ // Read context and grab all the dependencies override Providers in the tree | ||
return getDependencies(deps, target); | ||
} else { | ||
(0, _utils.warnOnce)("Seems like you are using ".concat(_constants.PACKAGE_NAME, " without Babel plugin. ") + "Please add '".concat(_constants.PACKAGE_NAME, "/babel-plugin' to your Babel config ") + "or import from '".concat(_constants.PACKAGE_NAME, "/macro' if your are using 'babel-plugin-macros'. ") + 'di(...) run as a no-op.'); | ||
} | ||
} | ||
/** @deprecated use injectable instead */ | ||
di.mock = _utils.mock; | ||
} |
@@ -9,3 +9,2 @@ "use strict"; | ||
var _constants = require("./constants"); | ||
var _stats = require("./stats"); | ||
var _utils = require("./utils"); | ||
@@ -15,17 +14,14 @@ function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } | ||
var globalDi = { | ||
getDependencies: function getDependencies(realDeps) { | ||
getDependencies: function getDependencies(realDeps, targetChild) { | ||
return realDeps.map(function (dep) { | ||
var replacedDep = replacementMap.get(dep); | ||
_stats.stats.track(replacedDep, dep); | ||
return replacedDep || dep; | ||
var replacedInj = (0, _utils.findInjectable)(replacementMap, dep, targetChild); | ||
return replacedInj ? replacedInj.value : dep; | ||
}); | ||
}, | ||
use: function use(deps) { | ||
use: function use(injs) { | ||
if (replacementMap.size) { | ||
throw new Error("".concat(_constants.PACKAGE_NAME, " has replacements configured already. ") + "Implicit merging is not supported, so please concatenate injectables. " + "If this is not expected, please file a bug report"); | ||
} | ||
deps.forEach(function (d) { | ||
(0, _utils.assertValidInjectable)(d); | ||
if (d[_constants.KEY].track) _stats.stats.set(d); | ||
replacementMap.set(d[_constants.KEY].from, d); | ||
injs.forEach(function (inj) { | ||
return (0, _utils.addInjectableToMap)(replacementMap, inj); | ||
}); | ||
@@ -32,0 +28,0 @@ }, |
@@ -13,3 +13,2 @@ "use strict"; | ||
var _context = require("./context"); | ||
var _stats = require("./stats"); | ||
var _utils = require("./utils"); | ||
@@ -29,8 +28,4 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } | ||
var value = (0, _react.useMemo)(function () { | ||
// create a map of dependency real -> replacement for fast lookup | ||
var replacementMap = use.reduce(function (m, d) { | ||
(0, _utils.assertValidInjectable)(d); | ||
if (d[_constants.KEY].track) _stats.stats.set(d); | ||
return m.set(d[_constants.KEY].from, d); | ||
}, new Map()); | ||
// create a map of dependency real -> replacements for fast lookup | ||
var replacementMap = use.reduce(_utils.addInjectableToMap, new Map()); | ||
// support single or multiple targets | ||
@@ -43,5 +38,4 @@ var targets = target && (Array.isArray(target) ? target : [target]); | ||
// If no target or target is in the array of targets, map use | ||
if (!targetChild || !targets || targets.includes(targetChild)) { | ||
if (!targets || targets.includes(targetChild)) { | ||
return dependencies.map(function (dep) { | ||
var _dep$KEY; | ||
// dep can be either the original or a replacement | ||
@@ -51,6 +45,5 @@ // if another provider at the top has already swapped it | ||
// or return the original / parent replacement | ||
var real = ((_dep$KEY = dep[_constants.KEY]) === null || _dep$KEY === void 0 ? void 0 : _dep$KEY.from) || dep; | ||
var replacedDep = replacementMap.get(real); | ||
_stats.stats.track(replacedDep, dep); | ||
return replacedDep || dep; | ||
var real = _constants.diRegistry.has(dep) ? _constants.diRegistry.get(dep).from : dep; | ||
var replacedInj = (0, _utils.findInjectable)(replacementMap, real, targetChild); | ||
return replacedInj ? replacedInj.value : dep; | ||
}); | ||
@@ -57,0 +50,0 @@ } |
@@ -7,3 +7,2 @@ "use strict"; | ||
exports.stats = void 0; | ||
var _constants = require("./constants"); | ||
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } | ||
@@ -25,3 +24,4 @@ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } | ||
state: createState(), | ||
set: function set(replacedDep) { | ||
set: function set(injObj) { | ||
var _injObj$value; | ||
// allow injectable override without flagging as unused | ||
@@ -32,4 +32,4 @@ var _iterator = _createForOfIteratorHelper(this.state.unused.keys()), | ||
for (_iterator.s(); !(_step = _iterator.n()).done;) { | ||
var injectable = _step.value; | ||
if (injectable[_constants.KEY].from === replacedDep[_constants.KEY].from) this.state.unused["delete"](injectable); | ||
var unusedInj = _step.value; | ||
if (unusedInj.from === injObj.from) this.state.unused["delete"](unusedInj); | ||
} | ||
@@ -41,12 +41,11 @@ } catch (err) { | ||
} | ||
this.state.unused.set(replacedDep, new Error("Unused \"di\" injectable: ".concat(replacedDep.displayName || replacedDep, "."), { | ||
cause: replacedDep[_constants.KEY].cause | ||
this.state.unused.set(injObj, new Error("Unused \"di\" injectable: ".concat(((_injObj$value = injObj.value) === null || _injObj$value === void 0 ? void 0 : _injObj$value.displayName) || injObj.value, "."), { | ||
cause: injObj.cause | ||
})); | ||
}, | ||
track: function track(replacedDep, dep) { | ||
if (replacedDep) { | ||
this.state.unused["delete"](replacedDep); | ||
this.state.used.add(replacedDep); | ||
this.state.provided.add(dep); | ||
} | ||
track: function track(inj) { | ||
if (!inj) return; | ||
this.state.unused["delete"](inj); | ||
this.state.used.add(inj); | ||
this.state.provided.add(inj.from); | ||
}, | ||
@@ -59,7 +58,7 @@ reset: function reset() { | ||
var _ref2 = _slicedToArray(_ref, 2), | ||
injectable = _ref2[0], | ||
inj = _ref2[0], | ||
_error = _ref2[1]; | ||
return { | ||
get: function get() { | ||
return injectable; | ||
return inj.value; | ||
}, | ||
@@ -66,0 +65,0 @@ error: function error() { |
@@ -6,8 +6,16 @@ "use strict"; | ||
}); | ||
exports.assertValidInjectable = assertValidInjectable; | ||
exports.addInjectableToMap = addInjectableToMap; | ||
exports.debug = debug; | ||
exports.findInjectable = findInjectable; | ||
exports.getDisplayName = getDisplayName; | ||
exports.injectable = injectable; | ||
exports.mock = void 0; | ||
exports.warnOnce = warnOnce; | ||
var _constants = require("./constants"); | ||
var _stats = require("./stats"); | ||
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } | ||
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } | ||
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } | ||
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } | ||
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } | ||
function _iterableToArrayLimit(arr, i) { var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; if (null != _i) { var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; try { if (_x = (_i = _i.call(arr)).next, 0 === i) { if (Object(_i) !== _i) return; _n = !1; } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); } catch (err) { _d = !0, _e = err; } finally { try { if (!_n && null != _i["return"] && (_r = _i["return"](), Object(_r) !== _r)) return; } finally { if (_d) throw _e; } } return _arr; } } | ||
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } | ||
var hasWarned = false; | ||
@@ -17,8 +25,18 @@ function warnOnce(message) { | ||
// eslint-disable-next-line no-console | ||
console.error('Warning:', message); | ||
console.warn('Warning:', message); | ||
hasWarned = true; | ||
} | ||
} | ||
function assertValidInjectable(dep) { | ||
if (!dep[_constants.KEY]) throw new Error("Seems like you are trying to use \"".concat(dep, "\" as injectable, but magnetic-di needs the return value of \"injectable()\"")); | ||
function addInjectableToMap(replacementMap, inj) { | ||
var injObj = _constants.diRegistry.get(inj); | ||
if (!injObj) { | ||
throw new Error("Seems like you are trying to use \"".concat(inj, "\" as injectable, but magnetic-di needs the return value of \"injectable()\"")); | ||
} | ||
if (injObj.track) _stats.stats.set(injObj); | ||
if (replacementMap.has(injObj.from)) { | ||
replacementMap.get(injObj.from).unshift(injObj); | ||
} else { | ||
replacementMap.set(injObj.from, [injObj]); | ||
} | ||
return replacementMap; | ||
} | ||
@@ -30,26 +48,29 @@ function getDisplayName(Comp) { | ||
} | ||
function injectable(from, implementation) { | ||
var _implementation$KEY, _implementation$KEY2; | ||
var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, | ||
displayName = _ref.displayName, | ||
_ref$track = _ref.track, | ||
track = _ref$track === void 0 ? true : _ref$track; | ||
implementation.displayName = displayName || getDisplayName(implementation) || getDisplayName(from, 'di'); | ||
if ((_implementation$KEY = implementation[_constants.KEY]) !== null && _implementation$KEY !== void 0 && _implementation$KEY.from && ((_implementation$KEY2 = implementation[_constants.KEY]) === null || _implementation$KEY2 === void 0 ? void 0 : _implementation$KEY2.from) !== from) { | ||
warnOnce("You are trying to use replacement \"".concat(implementation.displayName, "\" on multiple injectables. ") + "That will override only the last dependency, as each replacement is uniquely linked."); | ||
function debug(fn) { | ||
var source = fn.toString(); | ||
var _ref = source.match(/const \[[^\]]+\] = .*di.*\(\[([^\]]+)/) || [], | ||
_ref2 = _slicedToArray(_ref, 2), | ||
args = _ref2[1]; | ||
return args; | ||
} | ||
function findInjectable(replacementMap, dep, targetChild) { | ||
var injectables = replacementMap.get(dep) || []; | ||
var candidates = []; | ||
// loop all injectables for the dep, ranking targeted ones higher | ||
var _iterator = _createForOfIteratorHelper(injectables), | ||
_step; | ||
try { | ||
for (_iterator.s(); !(_step = _iterator.n()).done;) { | ||
var _inj$targets; | ||
var inj = _step.value; | ||
if (!inj.targets) candidates.push(inj); | ||
if ((_inj$targets = inj.targets) !== null && _inj$targets !== void 0 && _inj$targets.includes(targetChild)) candidates.unshift(inj); | ||
} | ||
} catch (err) { | ||
_iterator.e(err); | ||
} finally { | ||
_iterator.f(); | ||
} | ||
Object.defineProperty(implementation, _constants.KEY, { | ||
writable: true, | ||
// ideally this should be false, but sometimes devs reuse mocks | ||
value: { | ||
from: from, | ||
track: track, | ||
cause: new Error('Injectable created but not used. If this is on purpose, add "{track: false}"') | ||
} | ||
}); | ||
return implementation; | ||
} | ||
/** @deprecated use injectable instead */ | ||
var mock = injectable; | ||
exports.mock = mock; | ||
_stats.stats.track(candidates[0]); | ||
return candidates[0] || null; | ||
} |
module.exports = { | ||
PACKAGE_NAME: 'react-magnetic-di', | ||
PACKAGE_FUNCTION: 'di', | ||
INJECT_FUNCTION: 'injectable', | ||
HOC_FUNCTION: 'withDi' | ||
}; |
@@ -0,11 +1,90 @@ | ||
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } | ||
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } | ||
const { | ||
PACKAGE_NAME, | ||
INJECT_FUNCTION, | ||
PACKAGE_FUNCTION, | ||
HOC_FUNCTION | ||
} = require('./constants'); | ||
const processDIReference = require('./processor-di'); | ||
const processDiDeclaration = require('./processor-di'); | ||
const processHOCReference = require('./processor-hoc'); | ||
const { | ||
isEnabledEnv | ||
assert, | ||
createNamedImport, | ||
collectDiReferencePaths, | ||
collectDepsReferencePaths, | ||
isExcludedFile, | ||
isEnabledEnv, | ||
hasDisableComment | ||
} = require('./utils'); | ||
class State { | ||
constructor(path, isExcluded = false) { | ||
_defineProperty(this, "locations", new WeakMap()); | ||
_defineProperty(this, "aliases", new Map()); | ||
_defineProperty(this, "diIdentifier", null); | ||
_defineProperty(this, "programPath", null); | ||
this.programPath = path; | ||
this.isExcluded = isExcluded; | ||
} | ||
findDiIndentifier(t, body, scope) { | ||
const diImportNode = body.find(n => t.isImportDeclaration(n) && n.source.value === PACKAGE_NAME); | ||
const diImportSpecifier = diImportNode == null ? void 0 : diImportNode.specifiers.find(s => s.imported && s.imported.name === PACKAGE_FUNCTION); | ||
this.diIdentifier = (diImportSpecifier == null ? void 0 : diImportSpecifier.local) || scope.generateUidIdentifier(PACKAGE_FUNCTION); | ||
} | ||
getValueForPath(fnPath) { | ||
return this.locations.get(fnPath) || this.locations.get(fnPath.node); | ||
} | ||
setValueForPath(fnPath, value) { | ||
this.locations.set(fnPath, value); | ||
this.locations.set(fnPath.node, value); | ||
} | ||
getValueOrInit(fnPath) { | ||
// we need both node and path as either might get replaced | ||
if (!this.locations.has(fnPath) && !this.locations.has(fnPath.node)) { | ||
this.setValueForPath(fnPath, { | ||
diRef: null, | ||
dependencyRefs: new Set() | ||
}); | ||
} | ||
return this.getValueForPath(fnPath); | ||
} | ||
moveValueForPath(fnPath, newFnPath) { | ||
if (newFnPath && newFnPath.isFunction()) { | ||
this.setValueForPath(newFnPath, this.getValueForPath(fnPath)); | ||
} | ||
} | ||
removeValueForPath(fnPath) { | ||
this.locations.delete(fnPath); | ||
this.locations.delete(fnPath.node); | ||
} | ||
getAlias(name, scope) { | ||
if (!this.aliases.has(name)) { | ||
this.aliases.set(name, scope.generateUid(name)); | ||
} | ||
return this.aliases.get(name); | ||
} | ||
addDi(diRef) { | ||
const parentFnPath = diRef.getFunctionParent(); | ||
assert.isValidLocation(parentFnPath, diRef); | ||
const value = this.getValueOrInit(parentFnPath); | ||
value.diRef = diRef; | ||
} | ||
addDependency(depRef) { | ||
depRef.findParent(p => { | ||
var _p$parentPath, _p$parentPath$node, _p$parentPath$node$ca; | ||
if (p.isFunction() && ((_p$parentPath = p.parentPath) == null ? void 0 : (_p$parentPath$node = _p$parentPath.node) == null ? void 0 : (_p$parentPath$node$ca = _p$parentPath$node.callee) == null ? void 0 : _p$parentPath$node$ca.name) !== INJECT_FUNCTION) { | ||
// add ref for every function scope up to the root one | ||
this.getValueOrInit(p).dependencyRefs.add(depRef); | ||
} | ||
}); | ||
} | ||
addDiImport(t) { | ||
if (this.diIdentifier.loc) return; | ||
const statement = createNamedImport(t, PACKAGE_NAME, [PACKAGE_FUNCTION], [this.diIdentifier]); | ||
this.programPath.unshiftContainer('body', statement); | ||
// after adding, make this function a noop | ||
this.addDiImport = () => {}; | ||
} | ||
} | ||
module.exports = function (babel) { | ||
@@ -15,37 +94,68 @@ const { | ||
} = babel; | ||
let stateCache = new WeakMap(); | ||
return { | ||
name: PACKAGE_NAME, | ||
visitor: { | ||
ImportDeclaration(path, _ref) { | ||
let { | ||
opts = {} | ||
} = _ref; | ||
Program(path, { | ||
opts, | ||
file | ||
}) { | ||
const isEnabled = isEnabledEnv(opts.enabledEnvs); | ||
const isExcluded = isExcludedFile(opts.exclude, file.opts.filename); | ||
const state = new State(path, isExcluded); | ||
state.findDiIndentifier(t, path.node.body, path.scope); | ||
collectDiReferencePaths(t, state.diIdentifier, path.scope).forEach((p, i, arr) => { | ||
var _arr; | ||
const hasMulti = p.getFunctionParent() === ((_arr = arr[i + 1]) == null ? void 0 : _arr.getFunctionParent()); | ||
if (isEnabled && !hasMulti) state.addDi(p);else p.parentPath.remove(); | ||
}); | ||
if (!isEnabled) return; | ||
collectDepsReferencePaths(t, path.get('body')).forEach(p => state.addDependency(p)); | ||
// TODO | ||
// Should we add collection of globals to di via path.scope.globals? | ||
stateCache.set(file, state); | ||
}, | ||
Function(path, { | ||
file | ||
}) { | ||
const state = stateCache.get(file); | ||
const locationValue = state == null ? void 0 : state.getValueForPath(path); | ||
const shouldDi = !(state != null && state.isExcluded) && !hasDisableComment(path) || (locationValue == null ? void 0 : locationValue.diRef); | ||
// process only if function is a candidate to host di | ||
if (!state || !locationValue || !shouldDi) return; | ||
// convert arrow function returns as di needs a block | ||
if (!t.isBlockStatement(path.node.body)) { | ||
const bodyPath = path.get('body'); | ||
// convert arrow function return to block | ||
bodyPath.replaceWith(t.blockStatement([t.returnStatement(path.node.body)])); | ||
// we make sure that if body was a function that needs di() | ||
// we update the reference as new function path has been created | ||
state.moveValueForPath(bodyPath, path.get('body.body.0.argument')); | ||
} | ||
// create di declaration | ||
processDiDeclaration(t, path, locationValue, state); | ||
// once done, remove from cache so if babel calls function again we do not reprocess | ||
state.removeValueForPath(path); | ||
}, | ||
ImportDeclaration(path) { | ||
// first we look at the imports: | ||
// if not our package and not the right function, ignore | ||
const importSource = path.node.source.value; | ||
const importDISpecifier = path.node.specifiers.find(s => s.imported && s.imported.name === PACKAGE_FUNCTION); | ||
if (path.node.source.value !== PACKAGE_NAME) return; | ||
const importHOCSpecifier = path.node.specifiers.find(s => s.imported && s.imported.name === HOC_FUNCTION); | ||
if (importSource !== PACKAGE_NAME) return; | ||
if (importDISpecifier) { | ||
// then we locate all usages of the method | ||
// ensuring we affect only locations where it is called | ||
const methodIdentifier = importDISpecifier.local.name; | ||
const binding = path.scope.getBinding(methodIdentifier); | ||
if (!binding) return; | ||
const references = binding.referencePaths.filter(ref => t.isCallExpression(ref.container)); | ||
const isEnabled = isEnabledEnv() || Boolean(opts.forceEnable); | ||
if (!importHOCSpecifier) return; | ||
// for each of that location we apply a tranformation | ||
references.forEach(ref => processDIReference(t, ref, isEnabled)); | ||
} | ||
if (importHOCSpecifier) { | ||
// then we locate all usages of the method | ||
// ensuring we affect only locations where it is called | ||
const methodIdentifier = importHOCSpecifier.local.name; | ||
const binding = path.scope.getBinding(methodIdentifier); | ||
if (!binding) return; | ||
const references = binding.referencePaths.filter(ref => t.isCallExpression(ref.container)); | ||
// then we locate all usages of the method | ||
// ensuring we affect only locations where it is called | ||
const methodIdentifier = importHOCSpecifier.local.name; | ||
const binding = path.scope.getBinding(methodIdentifier); | ||
if (!binding) return; | ||
const references = binding.referencePaths.filter(ref => t.isCallExpression(ref.container)); | ||
// for each of that location we apply a tranformation | ||
references.forEach(ref => processHOCReference(t, ref)); | ||
} | ||
// for each of that location we apply a tranformation | ||
references.forEach(ref => processHOCReference(t, ref)); | ||
} | ||
@@ -52,0 +162,0 @@ } |
@@ -5,32 +5,62 @@ const { | ||
} = require('./utils'); | ||
function processReference(t, ref, isEnabled) { | ||
assert.isValidBlock(t, ref); | ||
assert.isValidCall(t, ref); | ||
function processReference(t, path, locationValue, state) { | ||
var _locationValue$diRef, _locationValue$diRef$, _locationValue$diRef$2; | ||
const self = getComponentDeclaration(t, path.scope); | ||
const bodyPath = path.get('body'); | ||
// from the arguments of the method we generate the list of dependency identifiers | ||
const args = ref.container.arguments; | ||
const dependencyIdentifiers = args.map(v => t.identifier(v.name)); | ||
const statement = ref.getStatementParent(); | ||
// Build list of dependencies | ||
// combining used imports/exports in this function block | ||
// with existing di expression (if any) | ||
const depNames = []; | ||
Array.from(locationValue.dependencyRefs).forEach(n => { | ||
var _n$node; | ||
const name = (_n$node = n.node) == null ? void 0 : _n$node.name; | ||
// quick check that the path is not detached | ||
if (!name || !n.parentPath) return; | ||
// Some babel plugins might rename imports (eg emotion) and references break | ||
// For now we skip, but ideally we would refresh the reference | ||
if (!bodyPath.scope.getBinding(name)) return; | ||
// Ensure we do not duplicate and di() self name | ||
if (depNames.includes(name) || name === (self == null ? void 0 : self.name)) return; | ||
depNames.push(name); | ||
}); | ||
(_locationValue$diRef = locationValue.diRef) == null ? void 0 : (_locationValue$diRef$ = _locationValue$diRef.container) == null ? void 0 : (_locationValue$diRef$2 = _locationValue$diRef$.arguments) == null ? void 0 : _locationValue$diRef$2.forEach(n => { | ||
assert.isValidArgument(t, n, locationValue.diRef, self); | ||
if (!depNames.includes(n.name)) depNames.push(n.name); | ||
}); | ||
depNames.sort(); | ||
// if should not be enabled, just remove the statement and exit | ||
if (!isEnabled) { | ||
statement.remove(); | ||
return; | ||
// if there are no valid candidates, exit | ||
if (!depNames.length) return; | ||
const elements = depNames.map(v => t.identifier(v)); | ||
const args = depNames.map(v => t.identifier(v)); | ||
// add di there | ||
const declaration = t.variableDeclaration('const', [t.variableDeclarator(t.arrayPattern(elements), t.callExpression(state.diIdentifier, [t.arrayExpression(args), self ? t.identifier(self.name) : t.nullLiteral()]))]); | ||
// We inject the new declaration either by replacing existing di | ||
// or by replacing and adding the statement at the top. | ||
// We need replacing to ensure we get a path so that registerDeclaration works | ||
let declarationPath; | ||
if (locationValue.diRef) { | ||
declarationPath = locationValue.diRef.getStatementParent(); | ||
declarationPath.replaceWith(declaration); | ||
} else { | ||
bodyPath.unshiftContainer('body', declaration); | ||
declarationPath = bodyPath.get('body.0'); | ||
} | ||
// generating variable declarations with array destructuring | ||
// assigning them the result of the method call, with arguments | ||
// now wrapped in an array | ||
statement.replaceWith(t.variableDeclaration('const', [t.variableDeclarator(t.arrayPattern(dependencyIdentifiers), t.callExpression(ref.node, [t.arrayExpression(args), getComponentDeclaration(t, ref.scope) || t.nullLiteral()]))])); | ||
ref.scope.registerDeclaration(statement); | ||
args.forEach(argIdentifier => { | ||
// for each argument we get the dependency variable name | ||
bodyPath.scope.registerDeclaration(declarationPath); | ||
const argsPaths = declarationPath.get('declarations.0.init.arguments.0.elements'); | ||
argsPaths.forEach(argPath => { | ||
// For each argument we get the dependency variable name | ||
// then we rename it locally so we get a new unique identifier. | ||
// Then we manually revert just the argument identifier name back, | ||
// so it still points to the original dependency identifier name | ||
const name = argIdentifier.name; | ||
ref.scope.rename(name); | ||
argIdentifier.name = name; | ||
const name = argPath.node.name; | ||
bodyPath.scope.rename(name, state.getAlias(name, bodyPath.scope)); | ||
argPath.replaceWith(t.identifier(name)); | ||
}); | ||
// ensure we add di import | ||
state.addDiImport(t); | ||
} | ||
module.exports = processReference; |
const processReference = (t, ref) => { | ||
var _nextSibling$node$exp, _nextSibling$node$exp2, _nextSibling$node$exp3; | ||
const container = ref.parentPath.container; | ||
if (container.type !== 'VariableDeclarator') return; | ||
const containerID = container.id; | ||
// check if display name already set by someone else right after def | ||
const nextSibling = ref.getStatementParent().getNextSibling(); | ||
if (nextSibling.isExpressionStatement() && ((_nextSibling$node$exp = nextSibling.node.expression) == null ? void 0 : (_nextSibling$node$exp2 = _nextSibling$node$exp.left) == null ? void 0 : (_nextSibling$node$exp3 = _nextSibling$node$exp2.property) == null ? void 0 : _nextSibling$node$exp3.name) == 'displayName') { | ||
return; | ||
} | ||
ref.getStatementParent().insertAfter(t.expressionStatement(t.assignmentExpression('=', t.memberExpression(containerID, t.identifier('displayName')), t.stringLiteral(containerID.name)))); | ||
}; | ||
module.exports = processReference; |
@@ -0,1 +1,4 @@ | ||
const { | ||
PACKAGE_NAME | ||
} = require('./constants'); | ||
const getComponentDeclaration = (t, scope) => { | ||
@@ -9,23 +12,17 @@ // function declarations | ||
if (scope.parentBlock.type.includes('Class')) return scope.parent.block.id; | ||
return null; | ||
}; | ||
const assert = { | ||
isValidBlock(t, ref) { | ||
const { | ||
block | ||
} = ref.scope; | ||
if (!t.isFunctionDeclaration(block) && !t.isFunctionExpression(block) && !t.isArrowFunctionExpression(block) && !t.isClassMethod(block)) { | ||
throw ref.buildCodeFrameError('Invalid di(...) call: must be inside a render function of a component. '); | ||
} | ||
}, | ||
isValidCall(t, ref) { | ||
if (!ref.container.arguments.length) { | ||
throw ref.buildCodeFrameError('Invalid di(...) arguments: must be called with at least one argument. '); | ||
} | ||
if (!ref.container.arguments.every(node => t.isIdentifier(node))) { | ||
isValidArgument(t, node, ref, self) { | ||
if (!t.isIdentifier(node)) { | ||
throw ref.buildCodeFrameError('Invalid di(...) arguments: must be called with plain identifiers. '); | ||
} | ||
const decl = getComponentDeclaration(t, ref.scope); | ||
if (decl && ref.container.arguments.some(v => v.name === decl.name)) { | ||
if (node.name === (self == null ? void 0 : self.name)) { | ||
throw ref.buildCodeFrameError('Invalid di(...) call: cannot inject self.'); | ||
} | ||
}, | ||
isValidLocation(path, ref) { | ||
if (!path) { | ||
throw ref.buildCodeFrameError('Invalid di(...) call: must be inside a render function of a component. '); | ||
} | ||
} | ||
@@ -38,10 +35,80 @@ }; | ||
}; | ||
const isEnabledEnv = () => { | ||
return ['development', 'test'].includes(process.env.BABEL_ENV) || ['development', 'test'].includes(process.env.NODE_ENV) || !process.env.BABEL_ENV && !process.env.NODE_ENV; | ||
function collectDepsReferencePaths(t, bodyPaths) { | ||
const references = []; | ||
function addRef(path) { | ||
const { | ||
referencePaths = [] | ||
} = path.scope.getBinding(path) || {}; | ||
references.push(...referencePaths); | ||
} | ||
// we could use scope.bindings to get all top level bindings | ||
// but it is hard to track local only vs later exported values | ||
bodyPaths.forEach(path => { | ||
if (path.isImportDeclaration()) { | ||
if (path.node.importKind === 'type') return; | ||
if (path.node.source.value === PACKAGE_NAME) return; | ||
path.get('specifiers').forEach(sp => { | ||
if (sp.node.importKind === 'type') return; | ||
if (sp.isImportDefaultSpecifier() || sp.isImportSpecifier()) { | ||
addRef(sp.get('local')); | ||
} | ||
}); | ||
} | ||
if (path.isExportNamedDeclaration()) { | ||
if (path.node.exportKind === 'type') return; | ||
if (path.node.declaration) { | ||
if (path.get('declaration.id').isIdentifier()) { | ||
addRef(path.get('declaration.id')); | ||
} else { | ||
path.get('declaration.declarations').forEach(dp => { | ||
if (dp.get('id').isIdentifier()) { | ||
addRef(dp.get('id')); | ||
} | ||
}); | ||
} | ||
} else { | ||
path.get('specifiers').forEach(sp => { | ||
if (sp.node.exportKind === 'type') return; | ||
if (sp.get('local').isIdentifier()) { | ||
addRef(sp.get('local')); | ||
} | ||
}); | ||
} | ||
} | ||
if (path.isExportDefaultDeclaration()) { | ||
if (path.node.exportKind === 'type') return; | ||
const ref = path.get('declaration').isIdentifier() ? path.get('declaration') : path.get('declaration.id'); | ||
addRef(ref); | ||
} | ||
}); | ||
return references; | ||
} | ||
function collectDiReferencePaths(t, identifier, scope) { | ||
// we locate all usages of the method | ||
const { | ||
referencePaths = [] | ||
} = scope.getBinding(identifier.name) || {}; | ||
return referencePaths.filter(ref => t.isCallExpression(ref.container)); | ||
} | ||
const isExcludedFile = (exclude = [], filename) => { | ||
const excludes = [].concat(exclude).map(v => v instanceof RegExp ? v : new RegExp(v.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'))); | ||
return excludes.some(rx => rx.test(filename)); | ||
}; | ||
const isEnabledEnv = (enabledEnvs = ['development', 'test']) => { | ||
return enabledEnvs.includes(process.env.BABEL_ENV) || enabledEnvs.includes(process.env.NODE_ENV); | ||
}; | ||
const hasDisableComment = path => { | ||
var _path$node, _path$node$body, _path$node2, _path$node2$body, _path$node2$body$body, _path$node2$body$body2; | ||
return [...(((_path$node = path.node) == null ? void 0 : (_path$node$body = _path$node.body) == null ? void 0 : _path$node$body.leadingComments) || []), ...(((_path$node2 = path.node) == null ? void 0 : (_path$node2$body = _path$node2.body) == null ? void 0 : (_path$node2$body$body = _path$node2$body.body) == null ? void 0 : (_path$node2$body$body2 = _path$node2$body$body[0]) == null ? void 0 : _path$node2$body$body2.leadingComments) || [])].some(c => c.value.includes('di-ignore')); | ||
}; | ||
module.exports = { | ||
getComponentDeclaration, | ||
assert, | ||
createNamedImport, | ||
isEnabledEnv | ||
collectDiReferencePaths, | ||
collectDepsReferencePaths, | ||
getComponentDeclaration, | ||
isEnabledEnv, | ||
isExcludedFile, | ||
hasDisableComment | ||
}; |
const order = require('./rules/order'); | ||
const exhaustiveInject = require('./rules/exhaustive-inject'); | ||
const noDuplicate = require('./rules/no-duplicate'); | ||
const noExtraneous = require('./rules/no-extraneous'); | ||
const noRestrictedInjectable = require('./rules/no-restricted-injectable'); | ||
const sortDependencies = require('./rules/sort-dependencies'); | ||
@@ -9,7 +9,7 @@ module.exports = { | ||
order: order, | ||
'exhaustive-inject': exhaustiveInject, | ||
'no-duplicate': noDuplicate, | ||
'no-extraneous': noExtraneous, | ||
'no-restricted-injectable': noRestrictedInjectable, | ||
'sort-dependencies': sortDependencies | ||
} | ||
}; |
@@ -20,3 +20,3 @@ const { | ||
}, | ||
create: function create(context) { | ||
create: function (context) { | ||
let diIdentifier; | ||
@@ -23,0 +23,0 @@ const report = node => context.report({ |
@@ -32,3 +32,3 @@ const { | ||
}, | ||
create: function create(context) { | ||
create: function (context) { | ||
let diIdentifier; | ||
@@ -35,0 +35,0 @@ const blockReferences = new WeakMap(); |
@@ -9,3 +9,3 @@ const { | ||
docs: { | ||
description: 'Enforce injectable definition at the top of the block', | ||
description: 'Enforce di() call expression at the top of the block', | ||
category: 'Possible Errors', | ||
@@ -17,6 +17,6 @@ recommended: true | ||
messages: { | ||
wrongOrder: 'Injectables should be defined at the top of their scope ' + 'to avoid partial replacements and variables clashing' | ||
wrongOrder: 'di() calls should be defined at the top of their scope ' + 'to avoid partial replacements and variables clashing' | ||
} | ||
}, | ||
create: function create(context) { | ||
create: function (context) { | ||
let diIdentifier = null; | ||
@@ -23,0 +23,0 @@ return { |
@@ -22,3 +22,3 @@ const { | ||
}, | ||
create: function create(context) { | ||
create: function (context) { | ||
let diIdentifier; | ||
@@ -25,0 +25,0 @@ const report = (node, prevNode, args, sortedArgs) => context.report({ |
const PACKAGE_NAME = 'react-magnetic-di'; | ||
const PACKAGE_FUNCTION = 'di'; | ||
const INJECT_FUNCTION = 'injectable'; | ||
const isDiStatement = (stm, spec) => stm.type === 'ExpressionStatement' && stm.expression && stm.expression.callee && stm.expression.callee.name === spec.name; | ||
const isHookName = node => /^use[A-Z0-9].*$/.test(node.name); | ||
const isComponentName = node => !/^[a-z]/.test(node.name); | ||
const isLocalVariable = (node, scope) => { | ||
do { | ||
var _scope; | ||
// if we reach module/global scope then is not local | ||
if (scope.type === 'module' || scope.type === 'global') return false; | ||
const isLocal = (_scope = scope) == null ? void 0 : _scope.variables.some(v => v.name === node.name); | ||
if (isLocal) return true; | ||
// eslint-disable-next-line no-cond-assign | ||
} while (scope = scope.upper); | ||
return false; | ||
const calcImportSource = src => { | ||
const [ns, value = ''] = src.split('/'); | ||
return ns.startsWith('@') ? ns + '/' + value : ns; | ||
}; | ||
const getDiIdentifier = node => { | ||
const importSource = node.source.value; | ||
const importSpecifier = node.specifiers.find(s => s.imported && s.imported.name === PACKAGE_FUNCTION); | ||
if (importSource.startsWith(PACKAGE_NAME) && importSpecifier) { | ||
return importSpecifier.local; | ||
const getImportIdentifiers = (node, pkgName, impNames) => { | ||
const importSource = calcImportSource(node.source.value); | ||
const importSpecifiers = node.specifiers.filter(s => s.imported && (!impNames || impNames.includes(s.imported.name))); | ||
if (importSource === pkgName && importSpecifiers.length) { | ||
return importSpecifiers.map(s => s.local); | ||
} | ||
return null; | ||
}; | ||
const getDiIdentifier = n => { | ||
var _getImportIdentifiers; | ||
return (_getImportIdentifiers = getImportIdentifiers(n, PACKAGE_NAME, [PACKAGE_FUNCTION])) == null ? void 0 : _getImportIdentifiers[0]; | ||
}; | ||
const getInjectIdentifier = n => { | ||
var _getImportIdentifiers2; | ||
return (_getImportIdentifiers2 = getImportIdentifiers(n, PACKAGE_NAME, [INJECT_FUNCTION])) == null ? void 0 : _getImportIdentifiers2[0]; | ||
}; | ||
const getDiStatements = (node, diIdentifier) => (node.body || []).reduce((acc, statement) => isDiStatement(statement, diIdentifier) ? acc.concat(statement) : acc, []); | ||
@@ -35,18 +35,11 @@ const getParentDiBlock = (node, diIdentifier) => { | ||
}; | ||
const getParentDiStatements = (node, diIdentifier) => { | ||
const parentBlock = getParentDiBlock(node, diIdentifier); | ||
if (parentBlock) return getDiStatements(parentBlock, diIdentifier); | ||
return []; | ||
}; | ||
const getDiVars = statements => statements.reduce((acc, s) => acc.concat(s.expression.arguments), []); | ||
module.exports = { | ||
isDiStatement, | ||
isHookName, | ||
isComponentName, | ||
isLocalVariable, | ||
getDiIdentifier, | ||
getImportIdentifiers, | ||
getInjectIdentifier, | ||
getDiStatements, | ||
getParentDiBlock, | ||
getParentDiStatements, | ||
getDiVars | ||
}; |
export { di } from './react/consumer'; | ||
export { runWithDi } from './react/global'; | ||
export { DiProvider, withDi } from './react/provider'; | ||
export { mock, injectable } from './react/utils'; | ||
export { injectable } from './react/injectable'; | ||
export { debug } from './react/utils'; | ||
export { stats } from './react/stats'; |
@@ -1,2 +0,2 @@ | ||
export const KEY = Symbol.for('di'); | ||
export const diRegistry = new WeakMap(); | ||
export const PACKAGE_NAME = 'react-magnetic-di'; |
@@ -1,7 +0,5 @@ | ||
import { PACKAGE_NAME } from './constants'; | ||
import { Context } from './context'; | ||
import { globalDi } from './global'; | ||
import { warnOnce, mock } from './utils'; | ||
function di(deps, target) { | ||
// check if babel plugin has been added | ||
export function di(deps, target) { | ||
// check if babel plugin has been added othrewise this is a noop | ||
if (Array.isArray(deps)) { | ||
@@ -17,9 +15,3 @@ // Read context and grab all the dependencies override Providers in the tree | ||
return getDependencies(deps, target); | ||
} else { | ||
warnOnce(`Seems like you are using ${PACKAGE_NAME} without Babel plugin. ` + `Please add '${PACKAGE_NAME}/babel-plugin' to your Babel config ` + `or import from '${PACKAGE_NAME}/macro' if your are using 'babel-plugin-macros'. ` + 'di(...) run as a no-op.'); | ||
} | ||
} | ||
/** @deprecated use injectable instead */ | ||
di.mock = mock; | ||
export { di }; | ||
} |
@@ -1,22 +0,16 @@ | ||
import { KEY, PACKAGE_NAME } from './constants'; | ||
import { stats } from './stats'; | ||
import { assertValidInjectable } from './utils'; | ||
import { PACKAGE_NAME } from './constants'; | ||
import { addInjectableToMap, findInjectable } from './utils'; | ||
const replacementMap = new Map(); | ||
export const globalDi = { | ||
getDependencies(realDeps) { | ||
getDependencies(realDeps, targetChild) { | ||
return realDeps.map(dep => { | ||
const replacedDep = replacementMap.get(dep); | ||
stats.track(replacedDep, dep); | ||
return replacedDep || dep; | ||
const replacedInj = findInjectable(replacementMap, dep, targetChild); | ||
return replacedInj ? replacedInj.value : dep; | ||
}); | ||
}, | ||
use(deps) { | ||
use(injs) { | ||
if (replacementMap.size) { | ||
throw new Error(`${PACKAGE_NAME} has replacements configured already. ` + `Implicit merging is not supported, so please concatenate injectables. ` + `If this is not expected, please file a bug report`); | ||
} | ||
deps.forEach(d => { | ||
assertValidInjectable(d); | ||
if (d[KEY].track) stats.set(d); | ||
replacementMap.set(d[KEY].from, d); | ||
}); | ||
injs.forEach(inj => addInjectableToMap(replacementMap, inj)); | ||
}, | ||
@@ -23,0 +17,0 @@ clear() { |
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } | ||
import React, { useContext, useMemo, forwardRef } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { KEY } from './constants'; | ||
import { diRegistry } from './constants'; | ||
import { Context } from './context'; | ||
import { stats } from './stats'; | ||
import { assertValidInjectable, getDisplayName } from './utils'; | ||
export const DiProvider = _ref => { | ||
let { | ||
children, | ||
use, | ||
target | ||
} = _ref; | ||
import { addInjectableToMap, getDisplayName, findInjectable } from './utils'; | ||
export const DiProvider = ({ | ||
children, | ||
use, | ||
target | ||
}) => { | ||
const { | ||
@@ -20,8 +18,4 @@ getDependencies | ||
const value = useMemo(() => { | ||
// create a map of dependency real -> replacement for fast lookup | ||
const replacementMap = use.reduce((m, d) => { | ||
assertValidInjectable(d); | ||
if (d[KEY].track) stats.set(d); | ||
return m.set(d[KEY].from, d); | ||
}, new Map()); | ||
// create a map of dependency real -> replacements for fast lookup | ||
const replacementMap = use.reduce(addInjectableToMap, new Map()); | ||
// support single or multiple targets | ||
@@ -34,5 +28,4 @@ const targets = target && (Array.isArray(target) ? target : [target]); | ||
// If no target or target is in the array of targets, map use | ||
if (!targetChild || !targets || targets.includes(targetChild)) { | ||
if (!targets || targets.includes(targetChild)) { | ||
return dependencies.map(dep => { | ||
var _dep$KEY; | ||
// dep can be either the original or a replacement | ||
@@ -42,6 +35,5 @@ // if another provider at the top has already swapped it | ||
// or return the original / parent replacement | ||
const real = ((_dep$KEY = dep[KEY]) == null ? void 0 : _dep$KEY.from) || dep; | ||
const replacedDep = replacementMap.get(real); | ||
stats.track(replacedDep, dep); | ||
return replacedDep || dep; | ||
const real = diRegistry.has(dep) ? diRegistry.get(dep).from : dep; | ||
const replacedInj = findInjectable(replacementMap, real, targetChild); | ||
return replacedInj ? replacedInj.value : dep; | ||
}); | ||
@@ -63,6 +55,3 @@ } | ||
}; | ||
export function withDi(Comp, deps, target) { | ||
if (target === void 0) { | ||
target = null; | ||
} | ||
export function withDi(Comp, deps, target = null) { | ||
const WrappedComponent = /*#__PURE__*/forwardRef((props, ref) => /*#__PURE__*/React.createElement(DiProvider, { | ||
@@ -69,0 +58,0 @@ use: deps, |
@@ -1,2 +0,1 @@ | ||
import { KEY } from './constants'; | ||
const createState = () => ({ | ||
@@ -9,15 +8,15 @@ unused: new Map(), | ||
state: createState(), | ||
set(replacedDep) { | ||
set(injObj) { | ||
var _injObj$value; | ||
// allow injectable override without flagging as unused | ||
for (let injectable of this.state.unused.keys()) if (injectable[KEY].from === replacedDep[KEY].from) this.state.unused.delete(injectable); | ||
this.state.unused.set(replacedDep, new Error(`Unused "di" injectable: ${replacedDep.displayName || replacedDep}.`, { | ||
cause: replacedDep[KEY].cause | ||
for (let unusedInj of this.state.unused.keys()) if (unusedInj.from === injObj.from) this.state.unused.delete(unusedInj); | ||
this.state.unused.set(injObj, new Error(`Unused "di" injectable: ${((_injObj$value = injObj.value) == null ? void 0 : _injObj$value.displayName) || injObj.value}.`, { | ||
cause: injObj.cause | ||
})); | ||
}, | ||
track(replacedDep, dep) { | ||
if (replacedDep) { | ||
this.state.unused.delete(replacedDep); | ||
this.state.used.add(replacedDep); | ||
this.state.provided.add(dep); | ||
} | ||
track(inj) { | ||
if (!inj) return; | ||
this.state.unused.delete(inj); | ||
this.state.used.add(inj); | ||
this.state.provided.add(inj.from); | ||
}, | ||
@@ -28,10 +27,7 @@ reset() { | ||
unused() { | ||
return Array.from(this.state.unused.entries()).map(_ref => { | ||
let [injectable, _error] = _ref; | ||
return { | ||
get: () => injectable, | ||
error: () => _error | ||
}; | ||
}); | ||
return Array.from(this.state.unused.entries()).map(([inj, error]) => ({ | ||
get: () => inj.value, | ||
error: () => error | ||
})); | ||
} | ||
}; |
@@ -1,2 +0,3 @@ | ||
import { KEY } from './constants'; | ||
import { diRegistry } from './constants'; | ||
import { stats } from './stats'; | ||
let hasWarned = false; | ||
@@ -6,39 +7,39 @@ export function warnOnce(message) { | ||
// eslint-disable-next-line no-console | ||
console.error('Warning:', message); | ||
console.warn('Warning:', message); | ||
hasWarned = true; | ||
} | ||
} | ||
export function assertValidInjectable(dep) { | ||
if (!dep[KEY]) throw new Error(`Seems like you are trying to use "${dep}" as injectable, but magnetic-di needs the return value of "injectable()"`); | ||
export function addInjectableToMap(replacementMap, inj) { | ||
const injObj = diRegistry.get(inj); | ||
if (!injObj) { | ||
throw new Error(`Seems like you are trying to use "${inj}" as injectable, but magnetic-di needs the return value of "injectable()"`); | ||
} | ||
if (injObj.track) stats.set(injObj); | ||
if (replacementMap.has(injObj.from)) { | ||
replacementMap.get(injObj.from).unshift(injObj); | ||
} else { | ||
replacementMap.set(injObj.from, [injObj]); | ||
} | ||
return replacementMap; | ||
} | ||
export function getDisplayName(Comp, wrapper) { | ||
if (wrapper === void 0) { | ||
wrapper = ''; | ||
} | ||
export function getDisplayName(Comp, wrapper = '') { | ||
const name = Comp.displayName || Comp.name; | ||
return !name || !wrapper ? name : `${wrapper}(${name})`; | ||
} | ||
export function injectable(from, implementation, _temp) { | ||
var _implementation$KEY, _implementation$KEY2; | ||
let { | ||
displayName, | ||
track = true | ||
} = _temp === void 0 ? {} : _temp; | ||
implementation.displayName = displayName || getDisplayName(implementation) || getDisplayName(from, 'di'); | ||
if ((_implementation$KEY = implementation[KEY]) != null && _implementation$KEY.from && ((_implementation$KEY2 = implementation[KEY]) == null ? void 0 : _implementation$KEY2.from) !== from) { | ||
warnOnce(`You are trying to use replacement "${implementation.displayName}" on multiple injectables. ` + `That will override only the last dependency, as each replacement is uniquely linked.`); | ||
export function debug(fn) { | ||
const source = fn.toString(); | ||
const [, args] = source.match(/const \[[^\]]+\] = .*di.*\(\[([^\]]+)/) || []; | ||
return args; | ||
} | ||
export function findInjectable(replacementMap, dep, targetChild) { | ||
const injectables = replacementMap.get(dep) || []; | ||
const candidates = []; | ||
// loop all injectables for the dep, ranking targeted ones higher | ||
for (const inj of injectables) { | ||
var _inj$targets; | ||
if (!inj.targets) candidates.push(inj); | ||
if ((_inj$targets = inj.targets) != null && _inj$targets.includes(targetChild)) candidates.unshift(inj); | ||
} | ||
Object.defineProperty(implementation, KEY, { | ||
writable: true, | ||
// ideally this should be false, but sometimes devs reuse mocks | ||
value: { | ||
from, | ||
track, | ||
cause: new Error('Injectable created but not used. If this is on purpose, add "{track: false}"') | ||
} | ||
}); | ||
return implementation; | ||
} | ||
/** @deprecated use injectable instead */ | ||
export const mock = injectable; | ||
stats.track(candidates[0]); | ||
return candidates[0] || null; | ||
} |
{ | ||
"name": "react-magnetic-di", | ||
"version": "2.3.4", | ||
"version": "3.0.0", | ||
"description": "Context driven dependency injection", | ||
"keywords": [ | ||
"React", | ||
"Dependency injection" | ||
"Dependency injection", | ||
"Dependency replacement" | ||
], | ||
@@ -41,5 +42,2 @@ "main": "lib/cjs/index.js", | ||
"peerDependenciesMeta": { | ||
"babel-plugin-macros": { | ||
"optional": true | ||
}, | ||
"eslint": { | ||
@@ -54,2 +52,4 @@ "optional": true | ||
"@babel/plugin-proposal-class-properties": "^7.18.6", | ||
"@babel/plugin-proposal-decorators": "^7.22.7", | ||
"@babel/plugin-transform-modules-commonjs": "^7.22.5", | ||
"@babel/plugin-transform-runtime": "^7.21.4", | ||
@@ -59,2 +59,3 @@ "@babel/preset-env": "^7.21.4", | ||
"@babel/preset-react": "^7.18.6", | ||
"@babel/preset-typescript": "^7.22.5", | ||
"@babel/runtime": "^7.21.0", | ||
@@ -67,3 +68,2 @@ "@testing-library/react": "^14.0.0", | ||
"babel-loader": "^9.1.2", | ||
"babel-plugin-macros": "^3.1.0", | ||
"babel-plugin-module-resolver": "^5.0.0", | ||
@@ -70,0 +70,0 @@ "dtslint": "^4.2.1", |
207
README.md
@@ -17,11 +17,12 @@ <p align="center"> | ||
- **Zero** performance overhead on production (code gets stripped unless told otherwise) | ||
- Works with any kind of functions/classes (not only React components) and in both class and functional React components | ||
- Replaces dependencies at any depth of the React tree / call chain | ||
- Allows selective injection (React only) | ||
- Promotes type safety for mocks | ||
- Works with any kind of value (funcitons, objects, strings) and in all closures / React components | ||
- Replaces dependencies at any depth of the call chain / React tree | ||
- Allows selective injection | ||
- Enforces separation of concerns, keeps your component API clean | ||
- Just uses smart variable assignments, it does not mess up with React internals or modules/require | ||
- Proper ES Modules support, as it does not mess up with modules/require or React internals | ||
## Philosophy | ||
Dependency injection and component injection is not a new topic. Especially the ability to provide a custom implementation of a component/hook while testing or writing storybooks and examples it is extremely valuable. `react-magnetic-di` takes inspiration from decorators, and with a touch of Babel magic and React Context / globals allows you to optionally override "marked" dependencies inside your components so you can swap implementations only when needed. | ||
Dependency injection and component injection is not a new topic. Especially the ability to provide a custom implementation of a component/hook while testing or writing storybooks and examples it is extremely valuable. `magnetic-di` takes inspiration from decorators, and with a touch of Babel magic allows you to optionally override imported/exported values in your code so you can swap implementations only when needed. | ||
@@ -36,3 +37,3 @@ ## Usage | ||
### Adding babel plugin (or using macro) | ||
### Adding babel plugin | ||
@@ -49,46 +50,41 @@ Edit your Babel config file (`.babelrc` / `babel.config.js` / ...) and add: | ||
If you are using Create React App or babel macros, you don't need the babel plugin: just import the methods from `react-magnetic-di/macro` (see next example). | ||
This is where the magic happens: we safely rewrite the code to prepend `di(...)` in every function scope, so that the dependency value can be swapped. We recommend to only add the plugin in development/test enviroments to avoid useless const assignment in production. You can either do that via multiple babel environment configs or by using `enabledEnvs` option. | ||
### Using injection replacement in your components | ||
### Using dependency replacement | ||
Given a component with complex UI interaction or data dependencies, like a Modal or an Apollo Query, we want to easily be able to integration test it. To achieve that, we "mark" such dependencies in the `render` function of the class component: | ||
Once babel is configured, in your tests you can create type safe replacements via `injectable` and then use `runWithDi` , which will setup and clear the replacements for you after function execution is terminated. Such util also handles async code, but might require you to wrap the entire test to work effectively with scheduled code paths, or event driven implementations. | ||
```jsx | ||
import React, { Component } from 'react'; | ||
import { di } from 'react-magnetic-di'; | ||
// or | ||
import { di } from 'react-magnetic-di/macro'; | ||
Assuming your source is: | ||
import { Modal } from 'material-ui'; | ||
import { Query } from 'react-apollo'; | ||
```js | ||
import { fetchApi } from './fetch'; | ||
class MyComponent extends Component { | ||
render() { | ||
// that's all is needed to "mark" these variables as injectable | ||
di(Modal, Query); | ||
return ( | ||
<Modal> | ||
<Query>{({ data }) => data && 'Done!'}</Query> | ||
</Modal> | ||
); | ||
} | ||
export async function myApiFetcher() { | ||
const { data } = await fetchApi(); | ||
return data; | ||
} | ||
``` | ||
Or on our functional component with hooks: | ||
Then in the test you can write: | ||
```jsx | ||
function MyComponent() { | ||
// "mark" any type of function/class as injectable | ||
di(Modal, useQuery); | ||
```js | ||
import { injectable, runWithDi } from 'react-magnetic-di'; | ||
import { fetchApi } from './fetch'; | ||
import { myApiFetcher } from '.'; | ||
const { data } = useQuery(); | ||
return <Modal>{data && 'Done!'}</Modal>; | ||
} | ||
it('should call the API', async () => { | ||
// injectable() needs the original implementation as first argument | ||
// and the replacement implementation as second | ||
const fetchApiDi = injectable(fetchApi, jest.fn().mockResolvedValue('mock')); | ||
const result = await runWithDi(() => myApiFetcher(), [fetchApiDi]); | ||
expect(fetchApiDi).toHaveBeenCalled(); | ||
expect(result).toEqual('mock'); | ||
}); | ||
``` | ||
### Leveraging dependency replacement in tests and storybooks | ||
### Usin dependency replacement in React tests and storybooks | ||
In the unit/integration tests or storybooks we can create a new `injectable` implementation and wrap the component with `DiProvider` to override such dependency: | ||
For React, we provide a specific `DiProvider` to enable replacements across the entire tree. Given a component with complex UI interaction or data dependencies, like a Modal or an Apollo Query, we want to easily be able to integration test it: | ||
@@ -106,11 +102,2 @@ ```jsx | ||
// test-enzyme.js | ||
it('should render with enzyme', () => { | ||
const container = mount(<MyComponent />, { | ||
wrappingComponent: DiProvider, | ||
wrappingComponentProps: { use: [ModalOpenDi, useQueryDi] }, | ||
}); | ||
expect(container.html()).toMatchSnapshot(); | ||
}); | ||
// test-testing-library.js | ||
@@ -147,3 +134,3 @@ it('should render with react-testing-library', () => { | ||
In the example above `MyComponent` will have both `ModalOpen` and `useQuery` replaced while `MyOtherComponent` only `ModalOpen`. Be aware that `target` needs an **actual component** declaration to work, so will not work in cases where the component is fully anonymous (eg: `export default () => ...` or `forwardRef(() => ...)`). | ||
Here `MyComponent` will have both `ModalOpen` and `useQuery` replaced while `MyOtherComponent` only `ModalOpen`. Be aware that `target` needs an **actual component** declaration to work, so will not work in cases where the component is fully anonymous (eg: `export default () => ...` or `forwardRef(() => ...)`). | ||
@@ -170,14 +157,15 @@ The library also provides a `withDi` HOC in case you want to export components with dependencies already injected: | ||
### Using injection replacement outside of React | ||
### Other replacement patterns | ||
The usage outside React is not much different, aside from the different way of clearing the replacements. | ||
#### Allowing globals replacement | ||
Currently the library does not enable automatic replacement of globals. To do that, you need to manually "tag" a global for replacement with `di(myGlobal)` in the function scope. For instance: | ||
```js | ||
import { fetchApi } from './fetch'; | ||
import { di } from 'react-magnetic-di'; | ||
export async function myApiFetcher() { | ||
// "mark" any type of function/class as injectable | ||
di(fetchApi); | ||
const { data } = await fetchApi(); | ||
// explicitly allow fetch global to be injected | ||
di(fetch); | ||
const { data } = await fetch(); | ||
return data; | ||
@@ -187,31 +175,28 @@ } | ||
In the tests, you can use `runWithDi`, which will setup and clear the replacements for you after function execution is terminated. Such util also handles async code, but might require you to wrap the entire test to work effectively with scheduled code paths, or event driven implementations. | ||
Alternatively, you can create a "getter" so that the library will pick it up: | ||
```js | ||
import { injectable, runWithDi } from 'react-magnetic-di'; | ||
import { myApiFetcher, fetchApi } from '.'; | ||
export const fetchApi = (...args) => fetch(...args); | ||
it('should call the API', async () => { | ||
const fetchApiDi = injectable(fetchApi, jest.fn().mockResolvedValue('mock')); | ||
export async function myApiFetcher() { | ||
// now injection will automatically work | ||
const { data } = await fetchApi(); | ||
return data; | ||
} | ||
``` | ||
const result = await runWithDi(() => myApiFetcher(), [fetchApiDi]); | ||
#### Ignoring a function scope | ||
expect(fetchApiDi).toHaveBeenCalled(); | ||
expect(result).toEqual('mock'); | ||
}); | ||
``` | ||
Other times, there might be places in code where auto injection is problematic and might cause infine loops. It might be the case if you are creating an injectable that then imports the replacement source itself. | ||
### injectables configuration | ||
For those scenarios, you can add a comment at the top of the function scope to tell the Babel plugin to skip that scope: | ||
When creating injectables you can provide a configuration object to customise some of its behaviour. | ||
For instance, you can provide a custom `displayName` to make debugging easier: | ||
```js | ||
const fetchApiDi = injectable(fetchApi, jest.fn(), { displayName: 'fetchApi' }); | ||
``` | ||
import { fetchApi } from './fetch'; | ||
Or you can skip reporting it in `stats.unused()` (handy if you provide default injectables across tests): | ||
```js | ||
const fetchApiDi = injectable(fetchApi, jest.fn(), { track: false }); | ||
export async function myApiFetcher() { | ||
// di-ignore | ||
const { data } = await fetchApi(); | ||
return data; | ||
} | ||
``` | ||
@@ -244,17 +229,42 @@ | ||
#### Enable dependency replacement on production (or custom env) | ||
#### Babel plugin options | ||
By default dependency replacement is enabled on `development` and `test` environments only, which means `di(...)` is removed on production builds. If you want to allow injection on production too (or on a custom env) you can use the `forceEnable` option: | ||
The plugin provides a couple of options to explicitly disable auto injection for certain paths, and overall enable/disable replacements on specific environments: | ||
```js | ||
// In your .babelrc / babel.config.js | ||
// In your .babelrc / babel.config.js | ||
// ... other stuff like presets | ||
plugins: [ | ||
// ... other plugins | ||
['react-magnetic-di/babel-plugin', { forceEnable: true }], | ||
['react-magnetic-di/babel-plugin', { | ||
// List of paths to ignore for auto injection. Recommended for mocks/tests | ||
exclude: ['mocks', /test\.tsx?/], | ||
// List of Babel or Node environment names where the plugin should be enabled | ||
enabledEnvs: ['development', 'test'], | ||
}], | ||
], | ||
``` | ||
In case of babel macro (eg for use with CRA), the `configName` key is `reactMagneticDi`. | ||
#### injectables options | ||
When creating injectables you can provide a configuration object to customise some of its behaviour. | ||
• `displayName`: provide a custom name to make debugging easier: | ||
```js | ||
const fetchApiDi = injectable(fetchApi, jest.fn(), { displayName: 'fetchApi' }); | ||
``` | ||
• `target`: allows a replacement to only apply to specific function(s): | ||
```js | ||
const fetchApiDi = injectable(fetchApi, jest.fn(), { target: fetchProjects }); | ||
``` | ||
• `track`: skip reporting it in `stats.unused()` (handy if you provide default injectables across tests): | ||
```js | ||
const fetchApiDi = injectable(fetchApi, jest.fn(), { track: false }); | ||
``` | ||
## ESLint plugin and rules | ||
@@ -264,9 +274,9 @@ | ||
| rule | description | options | | ||
| ------------------- | ---------------------------------------------------------------------------------------- | ------------------------ | | ||
| `order` | enforces `di(...)` to be the top of the block, to reduce chances of partial replacements | - | | ||
| `exhaustive-inject` | enforces all external components/hooks being used to be marked as injectable. | `ignore`: array of names | | ||
| `no-duplicate` | prohibits marking the same dependency as injectable more than once in the same block | - | | ||
| `no-extraneous` | enforces dependencies to be consumed in the scope, to prevent unused variables | - | | ||
| `sort-dependencies` | require injectable dependencies to be sorted | - | | ||
| rule | description | | ||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------- | | ||
| `order` | enforces `di(...)` to be the top of the block, to reduce chances of partial replacements | | ||
| `no-duplicate` | prohibits marking the same dependency as injectable more than once in the same scope | | ||
| `no-extraneous` | enforces dependencies to be consumed in the scope, to prevent unused variables | | ||
| `no-restricted-injectable` | prohibits certains values from being injected: `paths: [{ name: string, importNames?: string[], message?: string }]` | | ||
| `sort-dependencies` | require injectable dependencies to be sorted | | ||
@@ -277,25 +287,18 @@ The rules are exported from `react-magnetic-di/eslint-plugin`. Unfortunately ESLint does not allow plugins that are not npm packages, so rules needs to be imported via other means for now. | ||
- Does not support Enzyme shallow ([due to shallow not fully supporting context](https://github.com/enzymejs/enzyme/issues/2176)). If you wish to shallow anyway, you could mock `di` and manually return the array of mocked dependencies, but it is not recommended. | ||
- Does not support dynamic `use` and `target` props (changes are ignored) | ||
- Officially supports injecting only functions/classes. If you need to inject some other data types, create a simple getter and use that as dependency. | ||
- `DiProvider` does not support dynamic `use` and `target` props (changes are ignored) | ||
- Does not replace default props (or default parameters in general): so dependencies provided as default parameters (eg `function MyComponent ({ modal = Modal }) { ... }`) will be ignored. If you accept the dependency as prop/argument you should inject it via prop/argument, as having a double injection strategy is just confusing. | ||
- Injecting primitive values (strings, booleans, numbers, ...) can be unreliable as we only have the actual value as reference, and so the library might not exactly know what to replace. In cases where multiple values might be replaced, a warning will be logged and we recommend you declare an inject a getter instead of the value itself. | ||
- Targeting only works on named functions/classes, so it won't work on anonymous scopes (eg `export default () => { ... }` or `memo(() => { ... })`) | ||
## FAQ | ||
#### Can it be used without Babel plugin? | ||
#### Cannot seem to make injectable work | ||
Yes, but you will have to handle variable assignment yourself, which is a bit verbose. In this mode `di` needs an array of dependencies as first argument and the component, or `null`, as second (to make `target` behaviour work). Moreover, `di` won't be removed on prod builds and ESLint rules are not currently compatible with this mode. | ||
A way to check if some dependency has been tagged for injection is to use the `debug` util, as it will print all values that are available for injection: | ||
```js | ||
import React, { Component } from 'react'; | ||
import { di } from 'react-magnetic-di'; | ||
import { Modal as ModalInj } from 'material-ui'; | ||
import { useQuery as useQueryInj } from 'react-apollo'; | ||
function MyComponent() { | ||
const [Modal, useQuery] = di([ModalInj, useQueryInj], MyComponent); | ||
const { data } = useQuery(); | ||
return <Modal>{data && 'Done!'}</Modal>; | ||
} | ||
import { debug } from 'react-magnetic-di'; | ||
// ... | ||
console.log(debug(myApiFetcher)); | ||
// It will print ['fetchApi'] | ||
``` | ||
@@ -302,0 +305,0 @@ |
declare module 'react-magnetic-di' { | ||
import { ComponentType, ReactNode, Component, ComponentProps } from 'react'; | ||
type Dependency = Function; | ||
type Dependency = unknown; | ||
@@ -35,3 +35,3 @@ type Injectable<T = Dependency> = T & { | ||
use: Injectable[]; | ||
target?: ComponentType<any> | ComponentType<any>[]; | ||
target?: Function | Function[]; | ||
children?: ReactNode; | ||
@@ -45,7 +45,10 @@ }, | ||
dependencies: Injectable[], | ||
target?: ComponentType<any> | ComponentType<any>[] | ||
target?: Function | Function[] | ||
): T; | ||
/** @deprecated use injectable instead */ | ||
function mock<T extends Dependency>(original: T, mock: T): T; | ||
type InjectableOptions = { | ||
displayName?: string; | ||
target?: Function | Function[]; | ||
track?: boolean; | ||
}; | ||
@@ -55,3 +58,3 @@ function injectable<T extends Dependency>( | ||
implementation: ComponentOrFunction<T>, | ||
options?: { displayName?: string; track?: boolean } | ||
options?: InjectableOptions | ||
): Injectable<T>; | ||
@@ -61,3 +64,3 @@ function injectable<T extends Dependency>( | ||
implementation: T, | ||
options?: { displayName?: string; track?: boolean } | ||
options?: InjectableOptions | ||
): Injectable<T>; | ||
@@ -77,2 +80,4 @@ | ||
function debug<T extends Function>(fn: T): string; | ||
const stats: { | ||
@@ -84,11 +89,2 @@ /** Returns unused injectables */ | ||
}; | ||
class di { | ||
/** @deprecated use injectable instead */ | ||
static mock: typeof mock; | ||
} | ||
} | ||
declare module 'react-magnetic-di/macro' { | ||
export * from 'react-magnetic-di'; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
126836
2456
300
39
52
4