@hero-design/snowflake-guard
Version:
A hero-design bot detecting snowflake usage
176 lines (175 loc) • 9.95 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const parseMobileSource_1 = __importDefault(require("./parseMobileSource"));
const parseSource_1 = __importDefault(require("./parseSource"));
const constants_1 = require("./reports/constants");
const fetchGraphql_1 = __importDefault(require("./graphql/fetchGraphql"));
const queryGenerators_1 = require("./graphql/queryGenerators");
const getDiffLocs_1 = require("./utils/getDiffLocs");
const WEB_REGEX = /\.tsx$/;
const MOBILE_REGEX = /\.(tsx|jsx|js)$/;
const TEST_REGEX = /__tests__/;
const SNOWFLAKE_COMMENTS_WEB = {
style: 'Snowflake detected! A component is customized using inline styles. Make sure to not use [prohibited CSS properties](https://docs.google.com/spreadsheets/d/1Dj8vqLdFaf-CSaSVoYqyYZIkGqF6OoyP7K4G1_9L62U/edit?usp=sharing).',
sx: 'Snowflake detected! A component is customized via sx prop. Make sure to not use [prohibited CSS properties](https://docs.google.com/spreadsheets/d/1Dj8vqLdFaf-CSaSVoYqyYZIkGqF6OoyP7K4G1_9L62U/edit?usp=sharing).',
'styled-component': 'Please do not use styled-component to customize this component, use sx prop or inline style instead.',
className: `Please make sure that this className is not used as a CSS classname for component customization purposes, use sx prop or inline style instead. In case this is none-css classname, please flag it with this comment \`${constants_1.APPROVED_CLASSNAME_COMMENT}\`.`,
};
const SNOWFLAKE_COMMENTS_MOBILE = {
style: 'Snowflake detected! A component is customized using inline styles. Make sure to not use [prohibited CSS properties](https://docs.google.com/spreadsheets/d/1Dj8vqLdFaf-CSaSVoYqyYZIkGqF6OoyP7K4G1_9L62U/edit?gid=1761479144#gid=1761479144).',
'styled-component': 'Please do not use styled-component to customize this component.',
};
const MOBILE_REPO_NAMES = JSON.parse(process.env.MOBILE_REPO_NAMES || '[]');
const APPROVED_USER_LIST = JSON.parse(process.env.APPROVED_USER_LIST || '[]');
const checkIfDetectedSnowflakesInDiff = (diffLocs, locToComment) => {
const locIdx = diffLocs.findIndex(([start, end]) => {
return locToComment >= start && locToComment <= end;
});
return locIdx !== -1;
};
module.exports = (app) => {
app.on(['pull_request.opened', 'pull_request.synchronize'], (context) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
// Get PR info
const prNumber = context.payload.number;
const repoInfo = {
repo: context.payload.repository.name,
owner: context.payload.repository.owner.login,
};
const prBranch = context.payload.pull_request.head.ref;
// List all changed files
const prFiles = yield context.octokit.pulls.listFiles(Object.assign(Object.assign({}, repoInfo), { pull_number: prNumber }));
const isMobile = MOBILE_REPO_NAMES.includes(repoInfo.repo);
const parseSource = isMobile ? parseMobileSource_1.default : parseSource_1.default;
const SOURCE_REGEX = isMobile ? MOBILE_REGEX : WEB_REGEX;
const sourceFiles = prFiles.data.filter((file) => SOURCE_REGEX.test(file.filename) &&
!TEST_REGEX.test(file.filename) &&
file.status !== 'removed');
// Saving file patches to get diff locations
const prFilePatches = sourceFiles.reduce((acc, file) => {
acc[file.filename] = file.patch || '';
return acc;
}, {});
// Get file contents
const prFileContentPromises = sourceFiles.map((file) => context.octokit.repos.getContent(Object.assign(Object.assign({}, repoInfo), { path: file.filename, ref: prBranch })));
const prFileContents = yield Promise.all(prFileContentPromises);
const snowflakeComments = [];
const approvedSnowflakeLocs = [];
prFileContents.forEach((file) => __awaiter(void 0, void 0, void 0, function* () {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const filePath = file.data.path;
const diffLocs = (0, getDiffLocs_1.getDiffLocs)(prFilePatches[filePath]);
const stringContent = Buffer.from(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
file.data.content, 'base64').toString();
// Parse file content to check for snowflakes
const snowflakeReport = parseSource(stringContent);
snowflakeReport.styleLocs.forEach((loc) => {
if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) {
snowflakeComments.push({
path: filePath,
body: isMobile
? SNOWFLAKE_COMMENTS_MOBILE['style']
: SNOWFLAKE_COMMENTS_WEB['style'],
line: loc,
});
}
});
snowflakeReport.styledComponentLocs.forEach((loc) => {
if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) {
snowflakeComments.push({
path: filePath,
body: isMobile
? SNOWFLAKE_COMMENTS_MOBILE['styled-component']
: SNOWFLAKE_COMMENTS_WEB['styled-component'],
line: loc,
});
}
});
snowflakeReport.approvedLocs.forEach((loc) => {
if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) {
approvedSnowflakeLocs.push(loc);
}
});
if (!isMobile) {
snowflakeReport.sxLocs.forEach((loc) => {
if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) {
snowflakeComments.push({
path: filePath,
body: SNOWFLAKE_COMMENTS_WEB['sx'],
line: loc,
});
}
});
snowflakeReport.classNameLocs.forEach((loc) => {
if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) {
snowflakeComments.push({
path: filePath,
body: SNOWFLAKE_COMMENTS_WEB['className'],
line: loc,
});
}
});
}
}));
// Saving report
const snowflakeCount = snowflakeComments.length;
const report = (yield (0, fetchGraphql_1.default)((0, queryGenerators_1.generateFetchReportQuery)({ repoName: repoInfo.repo, prNumber })));
const reportData = (_a = report.data) === null || _a === void 0 ? void 0 : _a.fetchHdSnowflakeGuardReport;
if (reportData) {
yield (0, fetchGraphql_1.default)((0, queryGenerators_1.generateUpdateReportQuery)({
id: reportData.id,
latestCount: snowflakeCount,
approvedCount: approvedSnowflakeLocs.length,
}));
}
else {
yield (0, fetchGraphql_1.default)((0, queryGenerators_1.generateCreateReportQuery)({
repoName: repoInfo.repo,
prNumber,
owner: repoInfo.owner,
originalCount: snowflakeCount,
latestCount: snowflakeCount,
approvedCount: approvedSnowflakeLocs.length,
}));
}
// No snowflakes detected
// Create success check-run
if (snowflakeCount === 0) {
return context.octokit.checks.create(Object.assign(Object.assign({}, repoInfo), { name: 'SnowflakeGuard/Check', head_sha: context.payload.pull_request.head.sha, status: 'completed', conclusion: 'success' }));
}
// Snowflakes detected
// Create failed check-run & comment
yield context.octokit.checks.create(Object.assign(Object.assign({}, repoInfo), { name: 'SnowflakeGuard/Check', head_sha: context.payload.pull_request.head.sha, status: 'completed', conclusion: 'failure' }));
return context.octokit.pulls.createReview(Object.assign(Object.assign({}, repoInfo), { pull_number: prNumber, commit_id: context.payload.pull_request.head.sha, event: 'COMMENT', body: 'Snowflake Guard Bot has detected some snowflakes in this PR. Please review the following comments.', comments: snowflakeComments }));
}));
app.on('pull_request_review.submitted', (context) => {
const submittedType = context.payload.review.state;
const submittedUser = context.payload.review.user.login;
if (submittedType !== 'approved' ||
!APPROVED_USER_LIST.includes(submittedUser)) {
return;
}
return context.octokit.checks.create({
repo: context.payload.repository.name,
owner: context.payload.repository.owner.login,
name: 'SnowflakeGuard/Check',
head_sha: context.payload.pull_request.head.sha,
status: 'completed',
conclusion: 'success',
});
});
};