react-magnetic-di
Version:
Context driven dependency injection
238 lines (229 loc) • 11.5 kB
JavaScript
;
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
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 processDiDeclaration = require('./processor-di');
var processHOCReference = require('./processor-hoc');
var processInjectable = require('./processor-inj');
var _require2 = require('./utils'),
assert = _require2.assert,
createNamedImport = _require2.createNamedImport,
collectReferencePaths = _require2.collectReferencePaths,
collectDepsReferencePaths = _require2.collectDepsReferencePaths,
isMatchingAny = _require2.isMatchingAny,
isEnabledEnv = _require2.isEnabledEnv,
hasDisableComment = _require2.hasDisableComment,
parseOptions = _require2.parseOptions;
var State = /*#__PURE__*/function () {
function State(path) {
var isExcluded = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
_classCallCheck(this, State);
this.locations = new WeakMap();
this.imports = null;
this.diIdentifier = null;
this.injectIdentifier = null;
this.programPath = null;
this.programPath = path;
this.isExcluded = isExcluded;
}
return _createClass(State, [{
key: "findPkgIndentifiers",
value: function findPkgIndentifiers(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);
var injectImportSpecifier = diImportNode === null || diImportNode === void 0 ? void 0 : diImportNode.specifiers.find(function (s) {
return s.imported && s.imported.name === INJECT_FUNCTION;
});
this.injectIdentifier = injectImportSpecifier === null || injectImportSpecifier === void 0 ? void 0 : injectImportSpecifier.local;
}
}, {
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) {
return scope.generateUid(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$node, _depRef$parentPath, _p$parentPath;
// avoid dynamyc object getters/setters `get [dep]() {}` to be marked as dependencies
// on their created scope (odd behaviour of scope.getBinding)
var isComputedSelfPath = ((_p$node = p.node) === null || _p$node === void 0 ? void 0 : _p$node.computed) && (depRef.parentPath === p || ((_depRef$parentPath = depRef.parentPath) === null || _depRef$parentPath === void 0 ? void 0 : _depRef$parentPath.parentPath) === p);
if (p.isFunction() && ((_p$parentPath = p.parentPath) === null || _p$parentPath === void 0 || (_p$parentPath = _p$parentPath.node) === null || _p$parentPath === void 0 || (_p$parentPath = _p$parentPath.callee) === null || _p$parentPath === void 0 ? void 0 : _p$parentPath.name) !== INJECT_FUNCTION && !isComputedSelfPath) {
// add ref for every function scope up to the root one
_this.getValueOrInit(p).dependencyRefs.add(depRef);
}
});
}
}, {
key: "addImports",
value: function addImports(imports) {
this.imports = imports;
}
}, {
key: "prependDiImport",
value: function prependDiImport(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.prependDiImport = function () {};
}
}]);
}();
module.exports = function (babel) {
var t = babel.types;
var stateCache = new WeakMap();
var parsedOpts;
return {
name: PACKAGE_NAME,
pre: function pre(_ref) {
var opts = _ref.opts;
if (!parsedOpts) {
var pluginOpts = opts.plugins.find(function (p) {
return p.key === PACKAGE_NAME;
}).options;
parsedOpts = parseOptions(pluginOpts);
}
},
visitor: {
Program: function Program(path, _ref2) {
var file = _ref2.file;
var isEnabled = isEnabledEnv(parsedOpts.enabledEnvs);
var isExcluded = isMatchingAny(parsedOpts.exclude, file.opts.filename);
var state = new State(path, isExcluded);
state.findPkgIndentifiers(t, path.node.body, path.scope);
// Find all di() calls and store the arguments (to allow di custom vars)
// and then remove the di call as it's quicker than trying to manipilate
var diRefPaths = collectReferencePaths(t, state.diIdentifier, path.scope);
diRefPaths.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();
});
var alreadyProcessed = diRefPaths.some(function (p) {
var _p$parentPath2;
return (_p$parentPath2 = p.parentPath) === null || _p$parentPath2 === void 0 || (_p$parentPath2 = _p$parentPath2.parentPath) === null || _p$parentPath2 === void 0 ? void 0 : _p$parentPath2.isVariableDeclarator();
});
if (!isEnabled || alreadyProcessed) return;
var _collectDepsReference = collectDepsReferencePaths(t, path.get('body')),
references = _collectDepsReference.references,
imports = _collectDepsReference.imports;
references.forEach(function (p) {
return state.addDependency(p);
});
state.addImports(imports);
// TODO
// Should we add collection of globals to di via path.scope.globals?
// If we have injectables and we should mock modules, let's find them all
// and add relevat jest.mock() calls, before jest babel plugin looks for them
if (parsedOpts.mockModules && state.injectIdentifier) {
collectReferencePaths(t, state.injectIdentifier, path.scope).forEach(function (p) {
return processInjectable(t, p.parentPath, state, parsedOpts);
});
}
stateCache.set(file, state);
},
Function: function Function(path, _ref3) {
var file = _ref3.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
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 (!importHOCSpecifier) return;
// 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);
});
}
}
};
};