// ==UserScript==
// @name GitHub Changesets
// @license MIT
// @homepageURL https://github.com/bluwy/github-changesets-userscript
// @supportURL https://github.com/bluwy/github-changesets-userscript
// @namespace https://gf.qytechs.cn/
// @version 0.1.3
// @description Improve the Changesets experience in GitHub PRs
// @author Bjorn Lu
// @match https://github.com/**
// @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant none
// ==/UserScript==
// Options
const shouldRemoveChangesetBotComment = true
const shouldSkipCache = false
;
(() => {
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// node_modules/human-id/dist/index.js
var require_dist = __commonJS({
"node_modules/human-id/dist/index.js"(exports) {
"use strict";
var __spreadArray = exports && exports.__spreadArray || function(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.minLength = exports.maxLength = exports.poolSize = exports.humanId = exports.adverbs = exports.verbs = exports.nouns = exports.adjectives = void 0;
exports.adjectives = ["afraid", "all", "angry", "beige", "big", "better", "bitter", "blue", "brave", "breezy", "bright", "brown", "bumpy", "busy", "calm", "chatty", "chilly", "chubby", "clean", "clear", "clever", "cold", "crazy", "cruel", "cuddly", "curly", "curvy", "cute", "common", "cold", "cool", "cyan", "dark", "deep", "dirty", "dry", "dull", "eager", "early", "easy", "eight", "eighty", "eleven", "empty", "every", "evil", "fair", "famous", "fast", "fancy", "few", "fine", "fifty", "five", "flat", "fluffy", "floppy", "forty", "four", "free", "fresh", "fruity", "full", "funny", "fuzzy", "gentle", "giant", "gold", "good", "great", "green", "grumpy", "happy", "heavy", "hip", "honest", "hot", "huge", "hungry", "icy", "itchy", "khaki", "kind", "large", "late", "lazy", "lemon", "legal", "light", "little", "long", "loose", "loud", "lovely", "lucky", "major", "many", "mean", "metal", "mighty", "modern", "moody", "nasty", "neat", "new", "nice", "nine", "ninety", "odd", "old", "olive", "open", "orange", "pink", "plain", "plenty", "polite", "poor", "pretty", "proud", "public", "puny", "petite", "purple", "quick", "quiet", "rare", "real", "ready", "red", "rich", "ripe", "rotten", "rude", "sad", "salty", "seven", "shaggy", "shaky", "sharp", "shiny", "short", "shy", "silent", "silly", "silver", "six", "sixty", "slick", "slimy", "slow", "small", "smart", "smooth", "social", "soft", "solid", "some", "sour", "spicy", "spotty", "stale", "strong", "stupid", "sweet", "swift", "tall", "tame", "tangy", "tasty", "ten", "tender", "thick", "thin", "thirty", "three", "tidy", "tiny", "tired", "tough", "tricky", "true", "twelve", "twenty", "two", "upset", "vast", "violet", "warm", "weak", "wet", "whole", "wicked", "wide", "wild", "wise", "witty", "yellow", "young", "yummy"];
exports.nouns = ["apes", "animals", "areas", "bars", "banks", "baths", "breads", "bushes", "cloths", "clowns", "clubs", "hoops", "loops", "memes", "papers", "parks", "paths", "showers", "sides", "signs", "sites", "streets", "teeth", "tires", "webs", "actors", "ads", "adults", "aliens", "ants", "apples", "baboons", "badgers", "bags", "bananas", "bats", "beans", "bears", "beds", "beers", "bees", "berries", "bikes", "birds", "boats", "bobcats", "books", "bottles", "boxes", "brooms", "buckets", "bugs", "buses", "buttons", "camels", "cases", "cameras", "candies", "candles", "carpets", "carrots", "carrots", "cars", "cats", "chairs", "chefs", "chicken", "clocks", "clouds", "coats", "cobras", "coins", "corners", "colts", "comics", "cooks", "cougars", "regions", "results", "cows", "crabs", "crabs", "crews", "cups", "cities", "cycles", "dancers", "days", "deer", "dingos", "dodos", "dogs", "dolls", "donkeys", "donuts", "doodles", "doors", "dots", "dragons", "drinks", "dryers", "ducks", "ducks", "eagles", "ears", "eels", "eggs", "ends", "mammals", "emus", "experts", "eyes", "facts", "falcons", "fans", "feet", "files", "flies", "flowers", "forks", "foxes", "friends", "frogs", "games", "garlics", "geckos", "geese", "ghosts", "ghosts", "gifts", "glasses", "goats", "grapes", "groups", "guests", "hairs", "hands", "hats", "heads", "hornets", "horses", "hotels", "hounds", "houses", "humans", "icons", "ideas", "impalas", "insects", "islands", "items", "jars", "jeans", "jobs", "jokes", "keys", "kids", "kings", "kiwis", "knives", "lamps", "lands", "laws", "lemons", "lies", "lights", "lines", "lions", "lizards", "llamas", "mails", "mangos", "maps", "masks", "meals", "melons", "mice", "mirrors", "moments", "moles", "monkeys", "months", "moons", "moose", "mugs", "nails", "needles", "news", "nights", "numbers", "olives", "onions", "oranges", "otters", "owls", "pandas", "pans", "pants", "papayas", "parents", "parts", "parrots", "paws", "peaches", "pears", "peas", "pens", "pets", "phones", "pianos", "pigs", "pillows", "places", "planes", "planets", "plants", "plums", "poems", "poets", "points", "pots", "pugs", "pumas", "queens", "rabbits", "radios", "rats", "ravens", "readers", "rice", "rings", "rivers", "rockets", "rocks", "rooms", "roses", "rules", "schools", "bats", "seals", "seas", "sheep", "shirts", "shoes", "shrimps", "singers", "sloths", "snails", "snakes", "socks", "spiders", "spies", "spoons", "squids", "stars", "states", "steaks", "wings", "suits", "suns", "swans", "symbols", "tables", "taxes", "taxis", "teams", "terms", "things", "ties", "tigers", "times", "tips", "toes", "towns", "tools", "toys", "trains", "trams", "trees", "turkeys", "turtles", "vans", "views", "walls", "walls", "wasps", "waves", "ways", "weeks", "windows", "wolves", "women", "wombats", "words", "worlds", "worms", "yaks", "years", "zebras", "zoos"];
exports.verbs = ["accept", "act", "add", "admire", "agree", "allow", "appear", "argue", "arrive", "ask", "attack", "attend", "bake", "bathe", "battle", "beam", "beg", "begin", "behave", "bet", "boil", "bow", "brake", "brush", "build", "burn", "buy", "call", "camp", "care", "carry", "change", "cheat", "check", "cheer", "chew", "clap", "clean", "cough", "count", "cover", "crash", "create", "cross", "cry", "cut", "dance", "decide", "deny", "design", "dig", "divide", "do", "double", "doubt", "draw", "dream", "dress", "drive", "drop", "drum", "eat", "end", "enter", "enjoy", "exist", "fail", "fall", "feel", "fetch", "film", "find", "fix", "flash", "float", "flow", "fly", "fold", "follow", "fry", "give", "glow", "go", "grab", "greet", "grin", "grow", "guess", "hammer", "hang", "happen", "heal", "hear", "help", "hide", "hope", "hug", "hunt", "invent", "invite", "itch", "jam", "jog", "join", "joke", "judge", "juggle", "jump", "kick", "kiss", "kneel", "knock", "know", "laugh", "lay", "lead", "learn", "leave", "lick", "like", "lie", "listen", "live", "look", "lose", "love", "make", "march", "marry", "mate", "matter", "melt", "mix", "move", "nail", "notice", "obey", "occur", "open", "own", "pay", "peel", "play", "poke", "post", "press", "prove", "pull", "pump", "pick", "punch", "push", "raise", "read", "refuse", "relate", "relax", "remain", "repair", "repeat", "reply", "report", "rescue", "rest", "retire", "return", "rhyme", "ring", "roll", "rule", "run", "rush", "say", "scream", "see", "search", "sell", "send", "serve", "shake", "share", "shave", "shine", "show", "shop", "shout", "sin", "sink", "sing", "sip", "sit", "sleep", "slide", "smash", "smell", "smile", "smoke", "sneeze", "sniff", "sort", "speak", "spend", "stand", "start", "stay", "stick", "stop", "stare", "study", "strive", "swim", "switch", "take", "talk", "tan", "tap", "taste", "teach", "tease", "tell", "thank", "think", "throw", "tickle", "tie", "trade", "train", "travel", "try", "turn", "type", "unite", "vanish", "visit", "wait", "walk", "warn", "wash", "watch", "wave", "wear", "win", "wink", "wish", "wonder", "work", "worry", "write", "yawn", "yell"];
exports.adverbs = ["bravely", "brightly", "busily", "daily", "freely", "hungrily", "joyously", "knowlingly", "lazily", "oddly", "mysteriously", "noisily", "politely", "quickly", "quietly", "rapidly", "safely", "sleepily", "slowly", "truly", "yearly"];
function random(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function longest(arr) {
return arr.reduce(function(a, b) {
return a.length > b.length ? a : b;
});
}
function shortest(arr) {
return arr.reduce(function(a, b) {
return a.length < b.length ? a : b;
});
}
function humanId(options) {
if (options === void 0) {
options = {};
}
if (typeof options === "string")
options = { separator: options };
if (typeof options === "boolean")
options = { capitalize: options };
var _a = options.separator, separator = _a === void 0 ? "" : _a, _b = options.capitalize, capitalize = _b === void 0 ? true : _b, _c = options.adjectiveCount, adjectiveCount = _c === void 0 ? 1 : _c, _d = options.addAdverb, addAdverb = _d === void 0 ? false : _d;
var res = __spreadArray(__spreadArray(__spreadArray([], __spreadArray([], Array(adjectiveCount), true).map(function(_) {
return random(exports.adjectives);
}), true), [
random(exports.nouns),
random(exports.verbs)
], false), addAdverb ? [random(exports.adverbs)] : [], true);
if (capitalize)
res = res.map(function(r) {
return r.charAt(0).toUpperCase() + r.substr(1);
});
return res.join(separator);
}
exports.humanId = humanId;
function poolSize(options) {
if (options === void 0) {
options = {};
}
var _a = options.adjectiveCount, adjectiveCount = _a === void 0 ? 1 : _a, _b = options.addAdverb, addAdverb = _b === void 0 ? false : _b;
return exports.adjectives.length * adjectiveCount * exports.nouns.length * exports.verbs.length * (addAdverb ? exports.adverbs.length : 1);
}
exports.poolSize = poolSize;
function maxLength(options) {
if (options === void 0) {
options = {};
}
var _a = options.adjectiveCount, adjectiveCount = _a === void 0 ? 1 : _a, _b = options.addAdverb, addAdverb = _b === void 0 ? false : _b, _c = options.separator, separator = _c === void 0 ? "" : _c;
return longest(exports.adjectives).length * adjectiveCount + adjectiveCount * separator.length + longest(exports.nouns).length + separator.length + longest(exports.verbs).length + (addAdverb ? longest(exports.adverbs).length + separator.length : 0);
}
exports.maxLength = maxLength;
function minLength(options) {
if (options === void 0) {
options = {};
}
var _a = options.adjectiveCount, adjectiveCount = _a === void 0 ? 1 : _a, _b = options.addAdverb, addAdverb = _b === void 0 ? false : _b, _c = options.separator, separator = _c === void 0 ? "" : _c;
return shortest(exports.adjectives).length * adjectiveCount + adjectiveCount * separator.length + shortest(exports.nouns).length + separator.length + shortest(exports.verbs).length + (addAdverb ? shortest(exports.adverbs).length + separator.length : 0);
}
exports.minLength = minLength;
exports.default = humanId;
}
});
// src/index.js
run();
document.addEventListener("pjax:end", () => run());
document.addEventListener("turbo:render", () => run());
async function run() {
if (/^\/.+?\/.+?\/pull\/.+$/.exec(location.pathname) && // Skip if sidebar is already added
!document.querySelector(".sidebar-changesets") && await repoHasChangesetsSetup()) {
if (shouldRemoveChangesetBotComment) {
removeChangesetBotComment();
}
const updatedPackages = await prHasChangesetFiles();
await addChangesetSideSection(updatedPackages);
}
}
async function repoHasChangesetsSetup() {
const orgRepo = window.location.pathname.split("/").slice(1, 3).join("/");
const baseBranch = document.querySelector(".commit-ref").title.split(":")[1].trim();
const cacheKey = `github-changesets-userscript:repoHasChangesetsSetup-${orgRepo}-${baseBranch}`;
const cacheValue = sessionStorage.getItem(cacheKey);
if (!shouldSkipCache && cacheValue) return cacheValue === "true";
const changesetsFolderUrl = `https://github.com/${orgRepo}/tree/${baseBranch}/.changeset`;
const response = await fetch(changesetsFolderUrl, { method: "HEAD" });
const result = response.status === 200;
sessionStorage.setItem(cacheKey, result);
return result;
}
async function prHasChangesetFiles() {
const orgRepo = window.location.pathname.split("/").slice(1, 3).join("/");
const prNumber = window.location.pathname.split("/").pop();
const allCommitTimeline = document.querySelectorAll(
".js-timeline-item:has(svg.octicon-git-commit) a.markdown-title"
);
const prCommitSha = allCommitTimeline[allCommitTimeline.length - 1].href.split("/").slice(-1).join("").slice(0, 7);
const cacheKey = `github-changesets-userscript:prHasChangesetFiles-${orgRepo}-${prNumber}-${prCommitSha}`;
const cacheValue = sessionStorage.getItem(cacheKey);
if (!shouldSkipCache && cacheValue) return JSON.parse(cacheValue);
const filesUrl = `https://api.github.com/repos/${orgRepo}/pulls/${prNumber}/files`;
const response = await fetch(filesUrl);
const files = await response.json();
const hasChangesetFiles = files.some(
(file) => file.filename.startsWith(".changeset/")
);
if (hasChangesetFiles) {
const updatedPackages = await getUpdatedPackagesFromAddedChangedFiles(files);
sessionStorage.setItem(cacheKey, JSON.stringify(updatedPackages));
return updatedPackages;
} else {
sessionStorage.setItem(cacheKey, "{}");
return {};
}
}
async function addChangesetSideSection(updatedPackages) {
const { humanId } = await Promise.resolve().then(() => __toESM(require_dist(), 1));
updatedPackages = sortUpdatedPackages(updatedPackages);
const headRef = document.querySelector(".commit-ref.head-ref > a").title;
const orgRepo = headRef.split(":")[0].trim();
const branch = headRef.split(":")[1].trim();
const prTitle = document.querySelector(".js-issue-title").textContent.trim();
const changesetFileName = `.changeset/${humanId({
separator: "-",
capitalize: false
})}.md`;
const changesetFileContent = `---
"package": patch
---
${prTitle}
`;
const canEditPr = !!document.querySelector("button.js-title-edit-button");
const isPrOpen = !!document.querySelector(".gh-header .State.State--open");
const notificationsSideSection = document.querySelector(
".discussion-sidebar-item.sidebar-notifications"
);
const plusIcon = `<svg class="octicon octicon-plus" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"></path></svg>`;
let html = canEditPr && isPrOpen ? `<a class="d-block text-bold discussion-sidebar-heading discussion-sidebar-toggle" href="https://github.com/${orgRepo}/new/${branch}?filename=${changesetFileName}&value=${encodeURIComponent(
changesetFileContent
)}">Changesets
${plusIcon}</a>` : `<div class="d-block text-bold discussion-sidebar-heading">Changesets</div>`;
if (Object.keys(updatedPackages).length) {
html += `<table style="width: 100%; max-width: 400px;">
<tbody>
${Object.entries(updatedPackages).map(([pkg, bumpInfos]) => {
const bumpElements = bumpInfos.map((info) => {
if (info.diff) {
return `<a class="Link--muted" href="${location.origin + location.pathname}/files#diff-${info.diff}">${info.type}</a>`;
} else {
return info.type;
}
});
return `<tr>
<td style="width: 1px; white-space: nowrap; padding-right: 8px; vertical-align: top;">${pkg}</td>
<td class="color-fg-muted">${bumpElements.join(", ")}</td>
</tr>`;
}).join("")}
</tbody>
</table>`;
}
const changesetSideSection = document.createElement("div");
changesetSideSection.className = "discussion-sidebar-item sidebar-changesets";
changesetSideSection.innerHTML = html;
notificationsSideSection.before(changesetSideSection);
}
function removeChangesetBotComment() {
const changesetBotComment = document.querySelector(
'.js-timeline-item:has(a.author[href="/apps/changeset-bot"])'
);
if (changesetBotComment) {
changesetBotComment.remove();
}
}
async function getUpdatedPackagesFromAddedChangedFiles(changedFiles) {
const map = {};
for (const file of changedFiles) {
if (file.filename.startsWith(".changeset/") && file.status === "added") {
const lines = parseAddedPatchStringAsLines(file.patch);
let isInYaml = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line === "---") {
isInYaml = !isInYaml;
if (isInYaml) continue;
else break;
}
const match = /^['"](.+?)['"]:\s*(major|minor|patch)\s*$/.exec(line);
if (!match) continue;
const pkg = match[1];
const type = match[2];
const diff = await getAddedDiff(file.filename, i + 1);
const packages = map[pkg] || [];
packages.push({ type, diff });
map[pkg] = packages;
}
}
}
return map;
}
function parseAddedPatchStringAsLines(patch) {
return patch.replace(/^@@.*?@@$\n/m, "").replace(/^\+/gm, "").split("\n");
}
async function getAddedDiff(filename, line) {
if (window.isSecureContext && window.crypto && window.crypto.subtle) {
const filenameSha256 = await sha256(filename);
return `${filenameSha256}R${line}`;
}
}
async function sha256(message) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
return hashHex;
}
function sortUpdatedPackages(map) {
const newMap = {};
for (const key of Object.keys(map).sort()) {
const order = { major: 1, minor: 2, patch: 3 };
const value = [...map[key]].sort((a, b) => {
return order[a.type] - order[b.type];
});
newMap[key] = value;
}
return newMap;
}
})();