humpty
Version:
Makes sure your changelogs mention your breaking changes
180 lines (164 loc) • 4.23 kB
JavaScript
// @flow
import _ from "lodash";
import path from "path";
import simpleGit from "simple-git";
const babylon = require("babylon");
const reactDocs = require("react-docgen");
import babylonOptions from "./util/babylonOptions";
async function showFile(repo, apiRef, filePath) {
return new Promise((resolve, reject) => {
repo.show([`${apiRef}:${filePath}`], (err, results) => {
if (err) {
reject(err);
return;
}
resolve(results);
});
});
}
async function loadFile(
repo,
apiRef,
filePath,
extensionsToTry = ["", ".js", ".jsx"]
) {
for (let i = 0; i < extensionsToTry.length; i++) {
const ext = extensionsToTry[i];
try {
const contents = await showFile(repo, apiRef, `${filePath}${ext}`);
return contents;
} catch (error) {
// trying next extension
}
}
}
function getDefaultExportSourceFilePath(ast) {
const defaultExportAsNamed = ast.find(
node =>
node.type === "ExportNamedDeclaration" &&
node.specifiers[0].exported.name === "default"
);
if (defaultExportAsNamed) {
return defaultExportAsNamed.source.value;
}
}
async function getPropsOfDeepestFile({
repo,
apiRef,
sourceFile,
currentDir = ""
}) {
const fullFilePath = path.join(currentDir, sourceFile);
const source = await loadFile(repo, apiRef, fullFilePath);
if (!source) {
throw new Error(`Could not load file for named export ${sourceFile}`);
}
const indexAst = getAST(source);
const nextSourceFile = getDefaultExportSourceFilePath(indexAst);
if (nextSourceFile) {
return await getPropsOfDeepestFile({
repo,
apiRef,
sourceFile: nextSourceFile,
currentDir: path.dirname(fullFilePath)
});
}
return await detectProps(source);
}
async function detectNamedExports({ repo, apiRef, source: indexSource }) {
const indexAst = getAST(indexSource);
if (!indexAst) {
throw new Error("Could not parse entry file");
}
const exportsThatNeedLookup = indexAst
.filter(
({ type }) => ["ExportNamedDeclaration"].includes(type)
// "ExportDefaultDeclaration"
)
.filter(({ source }) => Boolean(source));
const results = await Promise.all(
exportsThatNeedLookup.map(async toImport => {
const publicExportName = toImport.specifiers[0].exported.name;
const sourceFile = toImport.source.value;
const foundProps = await getPropsOfDeepestFile({
repo,
apiRef,
sourceFile
});
return generateExportDefinition({
isDefaultExport: false,
name: publicExportName,
api: { props: foundProps }
});
})
);
return results;
}
async function detectProps(sourceToParse) {
const defaultExportApi = reactDocs.parse(sourceToParse);
return _(defaultExportApi.props)
.mapValues((value, name) => _.merge({}, value, { name }))
.values()
.value();
}
function generateExportDefinition({
isDefaultExport,
name,
sourceContents,
api
}) {
return {
isDefaultExport,
sourceContents,
name,
api
};
}
async function loadExport(repo, apiRef) {
const source = await loadFile(repo, apiRef, "index.js");
const namedExports = await detectNamedExports({ repo, apiRef, source });
const results = [
generateExportDefinition({
isDefaultExport: true,
sourceContents: source,
api: {
props: await detectProps(source)
}
}),
...namedExports
];
return results;
}
function getGitLog(repo, oldApi, newApi) {
return new Promise((resolve, reject) => {
repo.log(
{
from: oldApi,
to: newApi
},
(err, results) => {
if (err) {
reject(err);
return;
}
resolve(results.all.map(log => log.message));
}
);
});
}
function getAST(source) {
const ast = babylon.parse(source, babylonOptions);
return ast ? ast.program.body : null;
}
export default async function getExportsAndCommits({
oldApi,
newApi,
gitRepo
}) {
const repo = simpleGit(gitRepo).silent(true);
return {
commitMessages: await getGitLog(repo, oldApi, newApi),
newExports: await loadExport(repo, newApi),
oldExports: await loadExport(repo, oldApi)
};
}