ts-mockgen
Version:
Generate mock objects from TypeScript types using the TypeScript AST (ts-morph).
99 lines (98 loc) • 3.28 kB
JavaScript
import path from "path";
import fs from "fs";
import { Project } from "ts-morph";
/**
* Generates a mock object based on a TypeScript type name defined in the current project.
*
* @param typeName - The name of the TypeScript type to mock (interface, type alias, or exported type).
* @param overrides - Optional object specifying values to override in the generated mock.
*
* @returns A mock object matching the specified type with default values filled in and overrides applied.
*
* @throws Error if the specified type is not found in the project’s source files.
*/
export function generateMock(typeName, overrides = {}) {
const tsconfigPath = findTsConfig(process.cwd()) || path.resolve("./tsconfig.json");
const project = new Project({
tsConfigFilePath: tsconfigPath,
skipAddingFilesFromTsConfig: false,
});
const sourceFiles = project.getSourceFiles();
let foundType;
for (const sf of sourceFiles) {
const typeAlias = sf.getTypeAlias(typeName) || sf.getInterface(typeName);
if (typeAlias) {
foundType = typeAlias.getType();
break;
}
for (const exportSymbol of sf.getExportSymbols()) {
if (exportSymbol.getName() === typeName) {
foundType = exportSymbol.getDeclaredType();
break;
}
}
if (foundType)
break;
}
if (!foundType) {
throw new Error(`Type ${typeName} not found in project`);
}
const mock = mockFromType(foundType);
return deepMerge(mock, overrides);
}
function findTsConfig(startDir) {
let currentDir = path.resolve(startDir);
while (true) {
const tsconfigPath = path.join(currentDir, "tsconfig.json");
if (fs.existsSync(tsconfigPath))
return tsconfigPath;
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir)
break;
currentDir = parentDir;
}
return null;
}
function mockFromType(type) {
if (type.isString())
return "example";
if (type.isNumber())
return 42;
if (type.isBoolean())
return true;
if (type.getText() === "Date")
return new Date();
const unionTypes = type.isUnion() ? type.getUnionTypes() : [];
if (unionTypes.length > 0) {
return unionTypes[0].getLiteralValue();
}
if (type.isObject()) {
const properties = type.getProperties();
const mock = {};
for (const prop of properties) {
const propType = prop.getTypeAtLocation(prop.getValueDeclarationOrThrow());
mock[prop.getName()] = mockFromType(propType);
}
return mock;
}
return null;
}
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
}
function deepMerge(target, source) {
if (typeof source !== "object" || source === null)
return source;
for (const key of Object.keys(source)) {
const srcVal = source[key];
const tgtVal = target[key];
if (isPlainObject(srcVal) &&
isPlainObject(tgtVal)) {
target[key] = deepMerge(tgtVal, srcVal);
}
else {
target[key] = srcVal;
}
}
return target;
}