Comparing version 0.0.1 to 0.0.2
@@ -13,3 +13,7 @@ let storedCommits; | ||
const [sha, fileName] = showRequest.split(":"); | ||
callback(undefined, storedFiles[sha][fileName]); | ||
if (storedFiles[sha] && storedFiles[sha][fileName]) { | ||
callback(undefined, storedFiles[sha][fileName]); | ||
} else { | ||
callback(`Fatal error: could not show ${fileName}`); | ||
} | ||
}; | ||
@@ -16,0 +20,0 @@ } |
@@ -19,6 +19,10 @@ "use strict"; | ||
var _printUnmentionedChanges = require("./printUnmentionedChanges"); | ||
var _printResults = require("./printResults"); | ||
var _printUnmentionedChanges2 = _interopRequireDefault(_printUnmentionedChanges); | ||
var _printResults2 = _interopRequireDefault(_printResults); | ||
var _exitWithCode = require("./exitWithCode"); | ||
var _exitWithCode2 = _interopRequireDefault(_exitWithCode); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -29,3 +33,8 @@ | ||
exports.default = (() => { | ||
var _ref = _asyncToGenerator(function* ({ oldApi, newApi, gitRepo, json }) { | ||
var _ref = _asyncToGenerator(function* ({ | ||
oldApi, | ||
newApi, | ||
gitRepo, | ||
json: wantsJson | ||
}) { | ||
if (!oldApi) { | ||
@@ -45,3 +54,2 @@ console.error("Error: --oldApi flag required. See --help for more info."); | ||
} | ||
const { | ||
@@ -59,13 +67,9 @@ commitMessages, | ||
if (json) { | ||
console.log(JSON.stringify(unmentionedChanges, null, 2)); | ||
} else { | ||
(0, _printUnmentionedChanges2.default)({ | ||
unmentionedChanges, | ||
unmentionedChangeCount, | ||
oldApi, | ||
newApi | ||
}); | ||
} | ||
process.exit(unmentionedChangeCount === 0 ? 0 : 1); | ||
(0, _printResults2.default)({ | ||
unmentionedChanges, | ||
unmentionedChangeCount, | ||
oldApi, | ||
newApi | ||
}, wantsJson); | ||
(0, _exitWithCode2.default)(unmentionedChangeCount === 0 ? 0 : 1); | ||
}); | ||
@@ -72,0 +76,0 @@ |
@@ -12,4 +12,12 @@ "use strict"; | ||
var _findReactApiChanges = require("./findReactApiChanges"); | ||
var _findReactApiChanges2 = _interopRequireDefault(_findReactApiChanges); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } | ||
// @flow | ||
function findApiChanges({ oldExports, newExports }) { | ||
@@ -27,44 +35,8 @@ return oldExports.map(oldExport => { | ||
}); | ||
return newExport; | ||
} else { | ||
exportResult.apiChanges.push(...(0, _findReactApiChanges2.default)(oldExport, newExport)); | ||
} | ||
const { api: newApi } = newExport; | ||
oldExport.api.props.forEach(oldProp => { | ||
const newProp = newApi.props.find(p => p.name === oldProp.name); | ||
if (!newProp) { | ||
exportResult.apiChanges.push({ | ||
propName: oldProp.name, | ||
changeType: changeTypes.PROP_REMOVED | ||
}); | ||
return; | ||
} | ||
if (oldProp.type.name !== newProp.type.name) { | ||
exportResult.apiChanges.push({ | ||
propName: oldProp.name, | ||
changeType: changeTypes.PROP_TYPE_CHANGED, | ||
oldType: oldProp.type.name, | ||
newType: newProp.type.name | ||
}); | ||
} | ||
if (!oldProp.required && newProp.required) { | ||
exportResult.apiChanges.push({ | ||
propName: oldProp.name, | ||
changeType: changeTypes.PROP_MADE_REQUIRED | ||
}); | ||
} | ||
if (oldProp.required && !newProp.required) { | ||
exportResult.apiChanges.push({ | ||
propName: oldProp.name, | ||
changeType: changeTypes.PROP_MADE_OPTIONAL | ||
}); | ||
} | ||
}); | ||
if (exportResult.apiChanges.length) { | ||
return exportResult; | ||
} | ||
return exportResult.apiChanges.length ? exportResult : null; | ||
}).filter(exportChange => !!exportChange); | ||
} // @flow | ||
} |
@@ -7,5 +7,3 @@ "use strict"; | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | ||
let loadFile = (() => { | ||
let showFile = (() => { | ||
var _ref = _asyncToGenerator(function* (repo, apiRef, filePath) { | ||
@@ -23,3 +21,3 @@ return new Promise(function (resolve, reject) { | ||
return function loadFile(_x, _x2, _x3) { | ||
return function showFile(_x, _x2, _x3) { | ||
return _ref.apply(this, arguments); | ||
@@ -29,13 +27,122 @@ }; | ||
let loadFile = (() => { | ||
var _ref2 = _asyncToGenerator(function* (repo, apiRef, filePath, extensionsToTry = ["", ".js", ".jsx"]) { | ||
for (let i = 0; i < extensionsToTry.length; i++) { | ||
const ext = extensionsToTry[i]; | ||
try { | ||
const contents = yield showFile(repo, apiRef, `${filePath}${ext}`); | ||
return contents; | ||
} catch (error) { | ||
// trying next extension | ||
} | ||
} | ||
}); | ||
return function loadFile(_x4, _x5, _x6) { | ||
return _ref2.apply(this, arguments); | ||
}; | ||
})(); | ||
let getPropsOfDeepestFile = (() => { | ||
var _ref3 = _asyncToGenerator(function* ({ | ||
repo, | ||
apiRef, | ||
sourceFile, | ||
currentDir = "" | ||
}) { | ||
const fullFilePath = _path2.default.join(currentDir, sourceFile); | ||
const source = yield 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 yield getPropsOfDeepestFile({ | ||
repo, | ||
apiRef, | ||
sourceFile: nextSourceFile, | ||
currentDir: _path2.default.dirname(fullFilePath) | ||
}); | ||
} | ||
return yield detectProps(source); | ||
}); | ||
return function getPropsOfDeepestFile(_x7) { | ||
return _ref3.apply(this, arguments); | ||
}; | ||
})(); | ||
let detectNamedExports = (() => { | ||
var _ref4 = _asyncToGenerator(function* ({ repo, apiRef, source: indexSource }) { | ||
const indexAst = getAST(indexSource); | ||
if (!indexAst) { | ||
throw new Error("Could not parse entry file"); | ||
} | ||
const exportsThatNeedLookup = indexAst.filter(function ({ type }) { | ||
return ["ExportNamedDeclaration"].includes(type); | ||
} | ||
// "ExportDefaultDeclaration" | ||
).filter(function ({ source }) { | ||
return Boolean(source); | ||
}); | ||
const results = yield Promise.all(exportsThatNeedLookup.map((() => { | ||
var _ref5 = _asyncToGenerator(function* (toImport) { | ||
const publicExportName = toImport.specifiers[0].exported.name; | ||
const sourceFile = toImport.source.value; | ||
const foundProps = yield getPropsOfDeepestFile({ | ||
repo, | ||
apiRef, | ||
sourceFile | ||
}); | ||
return generateExportDefinition({ | ||
isDefaultExport: false, | ||
name: publicExportName, | ||
api: { props: foundProps } | ||
}); | ||
}); | ||
return function (_x9) { | ||
return _ref5.apply(this, arguments); | ||
}; | ||
})())); | ||
return results; | ||
}); | ||
return function detectNamedExports(_x8) { | ||
return _ref4.apply(this, arguments); | ||
}; | ||
})(); | ||
let detectProps = (() => { | ||
var _ref6 = _asyncToGenerator(function* (sourceToParse) { | ||
const defaultExportApi = reactDocs.parse(sourceToParse); | ||
return (0, _lodash2.default)(defaultExportApi.props).mapValues(function (value, name) { | ||
return _lodash2.default.merge({}, value, { name }); | ||
}).values().value(); | ||
}); | ||
return function detectProps(_x10) { | ||
return _ref6.apply(this, arguments); | ||
}; | ||
})(); | ||
let loadExport = (() => { | ||
var _ref2 = _asyncToGenerator(function* (repo, apiRef) { | ||
var _ref7 = _asyncToGenerator(function* (repo, apiRef) { | ||
const source = yield loadFile(repo, apiRef, "index.js"); | ||
return { | ||
const namedExports = yield detectNamedExports({ repo, apiRef, source }); | ||
const results = [generateExportDefinition({ | ||
isDefaultExport: true, | ||
sourceContents: source, | ||
api: detectProps(source) | ||
}; | ||
api: { | ||
props: yield detectProps(source) | ||
} | ||
}), ...namedExports]; | ||
return results; | ||
}); | ||
return function loadExport(_x4, _x5) { | ||
return _ref2.apply(this, arguments); | ||
return function loadExport(_x11, _x12) { | ||
return _ref7.apply(this, arguments); | ||
}; | ||
@@ -48,2 +155,6 @@ })(); | ||
var _path = require("path"); | ||
var _path2 = _interopRequireDefault(_path); | ||
var _simpleGit = require("simple-git"); | ||
@@ -53,2 +164,6 @@ | ||
var _babylonOptions = require("./util/babylonOptions"); | ||
var _babylonOptions2 = _interopRequireDefault(_babylonOptions); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -58,10 +173,27 @@ | ||
const babylon = require("babylon"); | ||
const reactDocs = require("react-docgen"); | ||
const detectProps = source => { | ||
const { props } = reactDocs.parse(source); | ||
function getDefaultExportSourceFilePath(ast) { | ||
const defaultExportAsNamed = ast.find(node => node.type === "ExportNamedDeclaration" && node.specifiers[0].exported.name === "default"); | ||
if (defaultExportAsNamed) { | ||
return defaultExportAsNamed.source.value; | ||
} | ||
} | ||
function generateExportDefinition({ | ||
isDefaultExport, | ||
name, | ||
sourceContents, | ||
api | ||
}) { | ||
return { | ||
props: (0, _lodash2.default)(props).mapValues((value, name) => _lodash2.default.merge({}, value, { name })).values().value() | ||
isDefaultExport, | ||
sourceContents, | ||
name, | ||
api | ||
}; | ||
}; | ||
} | ||
@@ -83,4 +215,9 @@ function getGitLog(repo, oldApi, newApi) { | ||
function getAST(source) { | ||
const ast = babylon.parse(source, _babylonOptions2.default); | ||
return ast ? ast.program.body : null; | ||
} | ||
exports.default = (() => { | ||
var _ref3 = _asyncToGenerator(function* ({ | ||
var _ref8 = _asyncToGenerator(function* ({ | ||
oldApi, | ||
@@ -90,16 +227,12 @@ newApi, | ||
}) { | ||
const repo = (0, _simpleGit2.default)(gitRepo); | ||
const repo = (0, _simpleGit2.default)(gitRepo).silent(true); | ||
return { | ||
commitMessages: yield getGitLog(repo, oldApi, newApi), | ||
oldExports: [_extends({ | ||
isDefaultExport: true | ||
}, (yield loadExport(repo, oldApi)))], | ||
newExports: [_extends({ | ||
isDefaultExport: true | ||
}, (yield loadExport(repo, newApi)))] | ||
newExports: yield loadExport(repo, newApi), | ||
oldExports: yield loadExport(repo, oldApi) | ||
}; | ||
}); | ||
function getExportsAndCommits(_x6) { | ||
return _ref3.apply(this, arguments); | ||
function getExportsAndCommits(_x13) { | ||
return _ref8.apply(this, arguments); | ||
} | ||
@@ -106,0 +239,0 @@ |
@@ -7,4 +7,9 @@ "use strict"; | ||
exports.default = printUnmentionedChanges; | ||
// @flow | ||
var _changeTypes = require("./changeTypes"); | ||
var changeTypes = _interopRequireWildcard(_changeTypes); | ||
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } | ||
function condensedGitRef(gitRef) { | ||
@@ -16,3 +21,3 @@ const isSha = gitRef.match(/^[0-9a-f]{6,}$/); | ||
return gitRef; | ||
} | ||
} // @flow | ||
@@ -29,15 +34,29 @@ function printUnmentionedChanges({ | ||
console.log(`🚨 Found ${unmentionedChangeCount} breaking API change${unmentionedChangeCount === 1 ? "" : "s"} not mentioned in git commits (${condensedGitRef(oldApi)}..${condensedGitRef(newApi)}):`); | ||
unmentionedChanges.forEach(changedExport => { | ||
console.log(""); | ||
if (changedExport.isDefaultExport) { | ||
console.log("Default export"); | ||
} else { | ||
console.log(changedExport.name); | ||
} | ||
const plural = unmentionedChangeCount === 1 ? "" : "s"; | ||
const intro = `🚨 Found ${unmentionedChangeCount} breaking API change${plural} not mentioned in git commits (${condensedGitRef(oldApi)}..${condensedGitRef(newApi)}):`; | ||
const fullChanges = unmentionedChanges.map(changedExport => { | ||
const exportName = changedExport.isDefaultExport ? "Default export" : changedExport.name; | ||
changedExport.apiChanges.forEach(apiChange => { | ||
console.log(` - ${apiChange.changeType}: "${apiChange.propName}" was ${apiChange.oldType}, now ${apiChange.newType}`); | ||
}); | ||
}); | ||
const changeList = changedExport.apiChanges.map(apiChange => { | ||
switch (apiChange.changeType) { | ||
case changeTypes.PROP_TYPE_CHANGED: | ||
return ` - ${apiChange.changeType}: "${apiChange.propName}" was ${apiChange.oldType}, now ${apiChange.newType}.`; | ||
case changeTypes.PROP_REMOVED: | ||
return ` - ${apiChange.changeType}: "${apiChange.propName}".`; | ||
case changeTypes.PROP_MADE_REQUIRED: | ||
return ` - ${apiChange.changeType}: "${apiChange.propName}" was optional, now required.`; | ||
case changeTypes.PROP_MADE_OPTIONAL: | ||
return ` - ${apiChange.changeType}: "${apiChange.propName}" was required, now optional.`; | ||
case !changedExport.isDefaultExport && changeTypes.EXPORT_REMOVED: | ||
return ` - ${apiChange.changeType}: ${changedExport.name} is no longer exported from the entry file.`; | ||
case changedExport.isDefaultExport && changeTypes.EXPORT_REMOVED: | ||
return ` - ${apiChange.changeType}: There is no longer a default export.`; | ||
default: | ||
break; | ||
} | ||
}).join("\n"); | ||
return `${exportName}\n${changeList}`; | ||
}).join("\n"); | ||
return `${intro}\n\n${fullChanges}`; | ||
} |
{ | ||
"name": "humpty", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "Makes sure your changelogs mention your breaking changes", | ||
@@ -14,7 +14,3 @@ "main": "dist/index.js", | ||
}, | ||
"keywords": [ | ||
"semver", | ||
"breaking", | ||
"change" | ||
], | ||
"keywords": ["semver", "breaking", "change"], | ||
"author": "Ben Gummer <bengummer@gmail.com>", | ||
@@ -26,2 +22,3 @@ "license": "ISC", | ||
"dependencies": { | ||
"babylon": "^6.18.0", | ||
"lodash": "^4.17.4", | ||
@@ -28,0 +25,0 @@ "meow": "^3.7.0", |
@@ -5,2 +5,4 @@ # Humpty 🍳 | ||
[![Build Status](https://travis-ci.org/bengummer/humpty.svg?branch=master)](https://travis-ci.org/bengummer/humpty) [![npm](https://img.shields.io/npm/v/humpty.svg)]() | ||
## Intro | ||
@@ -25,3 +27,3 @@ | ||
### APIs | ||
### Supported API detection | ||
@@ -36,3 +38,3 @@ * React components | ||
### Changelogs | ||
### Supported changelogs | ||
@@ -42,2 +44,26 @@ * React components | ||
## Installation | ||
If you're using humpty in CI you can install it globally | ||
```sh | ||
$ yarn global add humpty | ||
``` | ||
Or add it as a devDependency and call it via an npm script | ||
```json | ||
{ | ||
"name": "my-component", | ||
"scripts": { | ||
"humpty": "humpty ..." | ||
} | ||
} | ||
``` | ||
```sh | ||
$ yarn add --dev humpty | ||
$ yarn run humpty | ||
``` | ||
## Detailed usage | ||
@@ -61,2 +87,2 @@ | ||
[trello-board]: https://trello.com/b/kO2SRotn/humpty | ||
[trello-board]: https://trello.com/b/kO2SRotn/humpty |
@@ -6,5 +6,11 @@ // @flow | ||
import findUnmentionedChanges from "./findUnmentionedChanges"; | ||
import printUnmentionedChanges from "./printUnmentionedChanges"; | ||
import printResults from "./printResults"; | ||
import exitWithCode from "./exitWithCode"; | ||
export default async function App({ oldApi, newApi, gitRepo, json }) { | ||
export default async function App({ | ||
oldApi, | ||
newApi, | ||
gitRepo, | ||
json: wantsJson | ||
}) { | ||
if (!oldApi) { | ||
@@ -24,3 +30,2 @@ console.error("Error: --oldApi flag required. See --help for more info."); | ||
} | ||
const { | ||
@@ -41,6 +46,4 @@ commitMessages, | ||
if (json) { | ||
console.log(JSON.stringify(unmentionedChanges, null, 2)); | ||
} else { | ||
printUnmentionedChanges({ | ||
printResults( | ||
{ | ||
unmentionedChanges, | ||
@@ -50,5 +53,6 @@ unmentionedChangeCount, | ||
newApi | ||
}); | ||
} | ||
process.exit(unmentionedChangeCount === 0 ? 0 : 1); | ||
}, | ||
wantsJson | ||
); | ||
exitWithCode(unmentionedChangeCount === 0 ? 0 : 1); | ||
} |
// @flow | ||
import * as changeTypes from "./changeTypes"; | ||
import findReactApiChanges from "./findReactApiChanges"; | ||
@@ -20,45 +21,11 @@ export default function findApiChanges({ oldExports, newExports }) { | ||
}); | ||
return newExport; | ||
} else { | ||
exportResult.apiChanges.push( | ||
...findReactApiChanges(oldExport, newExport) | ||
); | ||
} | ||
const { api: newApi } = newExport; | ||
oldExport.api.props.forEach(oldProp => { | ||
const newProp = newApi.props.find(p => p.name === oldProp.name); | ||
if (!newProp) { | ||
exportResult.apiChanges.push({ | ||
propName: oldProp.name, | ||
changeType: changeTypes.PROP_REMOVED | ||
}); | ||
return; | ||
} | ||
if (oldProp.type.name !== newProp.type.name) { | ||
exportResult.apiChanges.push({ | ||
propName: oldProp.name, | ||
changeType: changeTypes.PROP_TYPE_CHANGED, | ||
oldType: oldProp.type.name, | ||
newType: newProp.type.name | ||
}); | ||
} | ||
if (!oldProp.required && newProp.required) { | ||
exportResult.apiChanges.push({ | ||
propName: oldProp.name, | ||
changeType: changeTypes.PROP_MADE_REQUIRED | ||
}); | ||
} | ||
if (oldProp.required && !newProp.required) { | ||
exportResult.apiChanges.push({ | ||
propName: oldProp.name, | ||
changeType: changeTypes.PROP_MADE_OPTIONAL | ||
}); | ||
} | ||
}); | ||
if (exportResult.apiChanges.length) { | ||
return exportResult; | ||
} | ||
return exportResult.apiChanges.length ? exportResult : null; | ||
}) | ||
.filter(exportChange => !!exportChange); | ||
} |
// @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 loadFile(repo, apiRef, filePath) { | ||
async function showFile(repo, apiRef, filePath) { | ||
return new Promise((resolve, reject) => { | ||
@@ -19,18 +22,123 @@ repo.show([`${apiRef}:${filePath}`], (err, results) => { | ||
const detectProps = source => { | ||
const { props } = reactDocs.parse(source); | ||
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 { | ||
props: _(props) | ||
.mapValues((value, name) => _.merge({}, value, { name })) | ||
.values() | ||
.value() | ||
isDefaultExport, | ||
sourceContents, | ||
name, | ||
api | ||
}; | ||
}; | ||
} | ||
async function loadExport(repo, apiRef) { | ||
const source = await loadFile(repo, apiRef, "index.js"); | ||
return { | ||
sourceContents: source, | ||
api: detectProps(source) | ||
}; | ||
const namedExports = await detectNamedExports({ repo, apiRef, source }); | ||
const results = [ | ||
generateExportDefinition({ | ||
isDefaultExport: true, | ||
sourceContents: source, | ||
api: { | ||
props: await detectProps(source) | ||
} | ||
}), | ||
...namedExports | ||
]; | ||
return results; | ||
} | ||
@@ -56,2 +164,7 @@ | ||
function getAST(source) { | ||
const ast = babylon.parse(source, babylonOptions); | ||
return ast ? ast.program.body : null; | ||
} | ||
export default async function getExportsAndCommits({ | ||
@@ -62,18 +175,8 @@ oldApi, | ||
}) { | ||
const repo = simpleGit(gitRepo); | ||
const repo = simpleGit(gitRepo).silent(true); | ||
return { | ||
commitMessages: await getGitLog(repo, oldApi, newApi), | ||
oldExports: [ | ||
{ | ||
isDefaultExport: true, | ||
...(await loadExport(repo, oldApi)) | ||
} | ||
], | ||
newExports: [ | ||
{ | ||
isDefaultExport: true, | ||
...(await loadExport(repo, newApi)) | ||
} | ||
] | ||
newExports: await loadExport(repo, newApi), | ||
oldExports: await loadExport(repo, oldApi) | ||
}; | ||
} |
// @flow | ||
import * as changeTypes from "./changeTypes"; | ||
function condensedGitRef(gitRef) { | ||
@@ -21,24 +23,37 @@ const isSha = gitRef.match(/^[0-9a-f]{6,}$/); | ||
console.log( | ||
`🚨 Found ${unmentionedChangeCount} breaking API change${unmentionedChangeCount === | ||
1 | ||
? "" | ||
: "s"} not mentioned in git commits (${condensedGitRef( | ||
oldApi | ||
)}..${condensedGitRef(newApi)}):` | ||
); | ||
unmentionedChanges.forEach(changedExport => { | ||
console.log(""); | ||
if (changedExport.isDefaultExport) { | ||
console.log("Default export"); | ||
} else { | ||
console.log(changedExport.name); | ||
} | ||
const plural = unmentionedChangeCount === 1 ? "" : "s"; | ||
const intro = `🚨 Found ${unmentionedChangeCount} breaking API change${plural} not mentioned in git commits (${condensedGitRef( | ||
oldApi | ||
)}..${condensedGitRef(newApi)}):`; | ||
const fullChanges = unmentionedChanges | ||
.map(changedExport => { | ||
const exportName = changedExport.isDefaultExport | ||
? "Default export" | ||
: changedExport.name; | ||
changedExport.apiChanges.forEach(apiChange => { | ||
console.log( | ||
` - ${apiChange.changeType}: "${apiChange.propName}" was ${apiChange.oldType}, now ${apiChange.newType}` | ||
); | ||
}); | ||
}); | ||
const changeList = changedExport.apiChanges | ||
.map(apiChange => { | ||
switch (apiChange.changeType) { | ||
case changeTypes.PROP_TYPE_CHANGED: | ||
return ` - ${apiChange.changeType}: "${apiChange.propName}" was ${apiChange.oldType}, now ${apiChange.newType}.`; | ||
case changeTypes.PROP_REMOVED: | ||
return ` - ${apiChange.changeType}: "${apiChange.propName}".`; | ||
case changeTypes.PROP_MADE_REQUIRED: | ||
return ` - ${apiChange.changeType}: "${apiChange.propName}" was optional, now required.`; | ||
case changeTypes.PROP_MADE_OPTIONAL: | ||
return ` - ${apiChange.changeType}: "${apiChange.propName}" was required, now optional.`; | ||
case !changedExport.isDefaultExport && changeTypes.EXPORT_REMOVED: | ||
return ` - ${apiChange.changeType}: ${changedExport.name} is no longer exported from the entry file.`; | ||
case changedExport.isDefaultExport && changeTypes.EXPORT_REMOVED: | ||
return ` - ${apiChange.changeType}: There is no longer a default export.`; | ||
default: | ||
break; | ||
} | ||
}) | ||
.join("\n"); | ||
return `${exportName}\n${changeList}`; | ||
}) | ||
.join("\n"); | ||
return `${intro}\n\n${fullChanges}`; | ||
} |
import simpleGit from "simple-git"; | ||
import basicComponentOld from "./dummy_exports/basic/old"; | ||
import basicComponentNew from "./dummy_exports/basic/old"; | ||
import { | ||
entryOld, | ||
entryNew, | ||
namedOld, | ||
namedNew | ||
} from "./inputs/getExportsAndCommits/defaultAndNamed"; | ||
import * as defaultAndNamedDeep from "./inputs/getExportsAndCommits/defaultAndNamedDeep"; | ||
import * as defaultAndNamedDeep2 from "./inputs/getExportsAndCommits/defaultAndNamedDeep2"; | ||
@@ -8,3 +16,3 @@ import getExportsAndCommits from "../src/getExportsAndCommits"; | ||
describe("getExportsAndCommits()", () => { | ||
test("return results", () => { | ||
test("single component in root index.js", () => { | ||
simpleGit.__setCommits__(["commit 1", "commit 2"]); | ||
@@ -27,2 +35,99 @@ simpleGit.__setFiles__({ | ||
}); | ||
test("default component export from index.js + named export from separate file", () => { | ||
simpleGit.__setCommits__(["commit 1", "commit 2"]); | ||
simpleGit.__setFiles__({ | ||
aaaaaa: { | ||
"index.js": entryOld, | ||
"Dog.js": namedOld | ||
}, | ||
ffffff: { | ||
"index.js": entryNew, | ||
"Dog.js": namedNew | ||
} | ||
}); | ||
return getExportsAndCommits({ | ||
oldApi: "aaaaaa", | ||
newApi: "ffffff" | ||
}).then(result => { | ||
expect(result.commitMessages).toEqual(["commit 1", "commit 2"]); | ||
expect(result.newExports.length).toBe(2); | ||
expect(result.newExports[0].isDefaultExport).toBe(true); | ||
expect(result.newExports[0].name).toBe(undefined); | ||
expect(result.newExports[0].api.props.length).toBe(2); | ||
expect(result.newExports[1].isDefaultExport).toBe(false); | ||
expect(result.newExports[1].name).toBe("Dog"); | ||
expect(result.newExports[1].api.props.length).toBe(1); | ||
expect(result.oldExports.length).toBe(2); | ||
}); | ||
}); | ||
test("default component export from index.js > intermediary file > named export from separate file", () => { | ||
simpleGit.__setCommits__(["commit 1", "commit 2"]); | ||
simpleGit.__setFiles__({ | ||
aaaaaa: { | ||
"index.js": defaultAndNamedDeep.entryOld, | ||
"middle/Middle.jsx": defaultAndNamedDeep.middleOld, | ||
"middle/components/Dog.js": defaultAndNamedDeep.deepOld | ||
}, | ||
ffffff: { | ||
"index.js": defaultAndNamedDeep.entryNew, | ||
"middle/Middle.jsx": defaultAndNamedDeep.middleNew, | ||
"middle/components/Dog.js": defaultAndNamedDeep.deepNew | ||
} | ||
}); | ||
return getExportsAndCommits({ | ||
oldApi: "aaaaaa", | ||
newApi: "ffffff" | ||
}).then(result => { | ||
expect(result.commitMessages).toEqual(["commit 1", "commit 2"]); | ||
expect(result.newExports.length).toBe(2); | ||
expect(result.newExports[0].isDefaultExport).toBe(true); | ||
expect(result.newExports[0].name).toBe(undefined); | ||
expect(result.newExports[0].api.props.length).toBe(2); | ||
expect(result.newExports[1].isDefaultExport).toBe(false); | ||
expect(result.newExports[1].name).toBe("Dog"); | ||
expect(result.newExports[1].api.props.length).toBe(1); | ||
expect(result.oldExports.length).toBe(2); | ||
}); | ||
}); | ||
test("default component export from index.js > intermediary file > named export from separate file (variant)", () => { | ||
simpleGit.__setCommits__(["commit 1", "commit 2"]); | ||
simpleGit.__setFiles__({ | ||
aaaaaa: { | ||
"index.js": defaultAndNamedDeep2.entryOld, | ||
"middle/Middle.jsx": defaultAndNamedDeep2.middleOld, | ||
"middle/components/Dog.js": defaultAndNamedDeep2.deepOld | ||
}, | ||
ffffff: { | ||
"index.js": defaultAndNamedDeep2.entryNew, | ||
"middle/Middle.jsx": defaultAndNamedDeep2.middleNew, | ||
"middle/components/Dog.js": defaultAndNamedDeep2.deepNew | ||
} | ||
}); | ||
return getExportsAndCommits({ | ||
oldApi: "aaaaaa", | ||
newApi: "ffffff" | ||
}).then(result => { | ||
expect(result.commitMessages).toEqual(["commit 1", "commit 2"]); | ||
expect(result.newExports.length).toBe(2); | ||
expect(result.newExports[0].isDefaultExport).toBe(true); | ||
expect(result.newExports[0].name).toBe(undefined); | ||
expect(result.newExports[0].api.props.length).toBe(2); | ||
expect(result.newExports[1].isDefaultExport).toBe(false); | ||
expect(result.newExports[1].name).toBe("Dog"); | ||
expect(result.newExports[1].api.props.length).toBe(1); | ||
expect(result.oldExports.length).toBe(2); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
57654
53
1720
84
5
1
+ Addedbabylon@^6.18.0
+ Addedbabylon@6.18.0(transitive)