@katis/weave
Version:
Simple functional dependency injection
191 lines (183 loc) • 5.28 kB
JavaScript
// src/Providers.ts
var isClassProvider = (provider) => "isClassProvider" in provider && provider.isClassProvider;
function Provider(base = Object) {
return class Injectable extends base {
static get isClassProvider() {
return true;
}
};
}
// src/mapValues.ts
var mapValues = (record, fn) => Object.fromEntries(
Object.entries(record).map(([key, value]) => [key, fn(value)])
);
// src/resolved.ts
var property = (provider) => {
let get;
if (isClassProvider(provider)) {
get = function() {
return new provider(this);
};
} else {
get = function() {
return provider(this);
};
}
return {
configurable: false,
enumerable: true,
get
};
};
var resolved = (providers) => Object.create(Object.prototype, mapValues(providers, property));
// src/assertNoCircularDeps.ts
var findCircularDependency = (graph) => {
const pathStack = [];
const nodeStates = new Map(
Object.keys(graph).map((key) => [key, 0 /* Unvisited */])
);
const dfs = (nodeName) => {
const nodeState = nodeStates.get(nodeName);
if (nodeState === 1 /* Visiting */) {
return pathStack.slice(pathStack.lastIndexOf(nodeName));
}
if (nodeState === 2 /* Visited */) {
return;
}
nodeStates.set(nodeName, 1 /* Visiting */);
pathStack.push(nodeName);
for (const dependency of graph[nodeName]) {
const cyclePath = dfs(dependency);
if (cyclePath) {
return cyclePath;
}
}
nodeStates.set(nodeName, 2 /* Visited */);
pathStack.pop();
};
for (const node of Object.keys(graph)) {
const cyclePath = dfs(node);
if (cyclePath) {
return cyclePath;
}
}
};
var assertNoCircularDeps = (graph) => {
const circular = findCircularDependency(graph);
if (circular) {
const message = [...circular, circular[0]].join(" -> ");
throw new Error(`Circular dependency detected: ${message}`);
}
};
// src/assertNoMissingProviders.ts
var assertNoMissingProviders = (providers) => {
for (const [provider, dependencies] of Object.entries(providers)) {
for (const dependency of dependencies) {
if (!(dependency in providers)) {
throw new Error(
`Provider "${dependency}" not defined, required by provider "${provider}"`
);
}
}
}
};
// src/parseDependencyNames.ts
var filterOutComments = (str) => {
const result = [];
let commentState = "none";
for (let i = 0; i < str.length; ++i) {
if (commentState === "singleline") {
if (str[i] === "\n")
commentState = "none";
} else if (commentState === "multiline") {
if (str[i - 1] === "*" && str[i] === "/")
commentState = "none";
} else if (commentState === "none") {
if (str[i] === "/" && str[i + 1] === "/") {
commentState = "singleline";
} else if (str[i] === "/" && str[i + 1] === "*") {
commentState = "multiline";
i += 2;
} else {
result.push(str[i]);
}
}
}
return result.join("");
};
var splitByComma = (str) => {
const result = [];
const stack = [];
let start = 0;
for (let i = 0; i < str.length; i++) {
if (str[i] === "{" || str[i] === "[") {
stack.push(str[i] === "{" ? "}" : "]");
} else if (str[i] === stack[stack.length - 1]) {
stack.pop();
} else if (!stack.length && str[i] === ",") {
const token = str.substring(start, i).trim();
if (token)
result.push(token);
start = i + 1;
}
}
const lastToken = str.substring(start).trim();
if (lastToken)
result.push(lastToken);
return result;
};
var extractConstructorParams = (classText) => {
const constructorMatch = classText.match(/constructor\s*\(([^)]*)\)/);
if (!constructorMatch)
throw Error("No constructor found in class");
return constructorMatch[1].trim();
};
var parseDependencyNames = (fn) => {
const isClass = isClassProvider(fn);
const text = filterOutComments(fn.toString());
let paramsText;
if (isClass) {
paramsText = extractConstructorParams(text);
} else {
const match = text.match(/(?:async)?(?:\s+function)?[^(]*\(([^)]*)/);
if (!match)
return [];
paramsText = match[1].trim();
}
if (!paramsText)
return [];
const [firstParam] = splitByComma(paramsText);
if (firstParam[0] !== "{" || firstParam[firstParam.length - 1] !== "}") {
throw Error(
"First argument must use the object destructuring pattern: " + firstParam
);
}
const props = splitByComma(
firstParam.substring(1, firstParam.length - 1)
).map((prop) => {
const colon = prop.indexOf(":");
return colon === -1 ? prop.trim() : prop.substring(0, colon).trim();
});
const restProperty = props.find((prop) => prop.startsWith("..."));
if (restProperty) {
throw Error(
`Rest property "${restProperty}" is not supported. List all used fixtures explicitly, separated by comma.`
);
}
return props;
};
// src/verifyDependencies.ts
var verifyDependencies = (providers) => {
const graph = mapValues(providers, parseDependencyNames);
assertNoCircularDeps(graph);
assertNoMissingProviders(graph);
};
// src/weave.ts
var weave = (providers) => {
verifyDependencies(providers);
return resolved(providers);
};
export {
Provider,
weave
};