UNPKG

@katis/weave

Version:

Simple functional dependency injection

191 lines (183 loc) 5.28 kB
// 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 };