mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge pull request #14309 from meteor/oxc/phase-1
[phase1] Applying linter, formatter and new tests for a few pkgs
This commit is contained in:
@@ -31,3 +31,7 @@ packages/**/.npm/
|
||||
packages/*
|
||||
|
||||
!packages/facts-base/
|
||||
!packages/facts-ui/
|
||||
!packages/non-core/bundle-visualizer/
|
||||
!packages/non-core/mongo-decimal/
|
||||
!packages/non-core/xmlbuilder/
|
||||
|
||||
@@ -28,3 +28,7 @@ packages/**/.npm/
|
||||
packages/*
|
||||
|
||||
!packages/facts-base/
|
||||
!packages/facts-ui/
|
||||
!packages/non-core/bundle-visualizer/
|
||||
!packages/non-core/mongo-decimal/
|
||||
!packages/non-core/xmlbuilder/
|
||||
|
||||
@@ -171,3 +171,74 @@ Tinytest.add("facts-base - setUserIdFilter replaces the filter", (test) => {
|
||||
return !!Package.autopublish;
|
||||
});
|
||||
});
|
||||
|
||||
// -- resetServerFacts with active subscriptions --
|
||||
|
||||
Tinytest.add("facts-base - resetServerFacts does not notify subscriptions", (test) => {
|
||||
Facts.resetServerFacts();
|
||||
const sub = mockSub();
|
||||
Facts._setActiveSubscriptions([sub]);
|
||||
|
||||
Facts.incrementServerFact("pkg", "val", 5);
|
||||
Facts.resetServerFacts();
|
||||
|
||||
// added was called once for the increment, but reset should not trigger notifications
|
||||
test.equal(sub.calls.added.length, 1);
|
||||
test.equal(sub.calls.changed.length, 0);
|
||||
test.equal(Facts._factsByPackage, {});
|
||||
|
||||
Facts._setActiveSubscriptions([]);
|
||||
});
|
||||
|
||||
// -- incrementServerFact: zero increment --
|
||||
|
||||
Tinytest.add("facts-base - incrementServerFact with zero increment", (test) => {
|
||||
Facts.resetServerFacts();
|
||||
Facts.incrementServerFact("pkg", "counter", 0);
|
||||
|
||||
test.equal(Facts._factsByPackage["pkg"].counter, 0);
|
||||
});
|
||||
|
||||
// -- incrementServerFact: multiple facts same package --
|
||||
|
||||
Tinytest.add("facts-base - incrementServerFact supports many facts per package", (test) => {
|
||||
Facts.resetServerFacts();
|
||||
|
||||
Facts.incrementServerFact("multi", "a", 1);
|
||||
Facts.incrementServerFact("multi", "b", 2);
|
||||
Facts.incrementServerFact("multi", "c", 3);
|
||||
|
||||
test.equal(Facts._factsByPackage["multi"], { a: 1, b: 2, c: 3 });
|
||||
});
|
||||
|
||||
// -- subscription: changed sends only the changed field --
|
||||
|
||||
Tinytest.add("facts-base - changed notification only includes the updated field", (test) => {
|
||||
Facts.resetServerFacts();
|
||||
const sub = mockSub();
|
||||
|
||||
Facts.incrementServerFact("pkg", "a", 1);
|
||||
Facts.incrementServerFact("pkg", "b", 2);
|
||||
Facts._setActiveSubscriptions([sub]);
|
||||
|
||||
Facts.incrementServerFact("pkg", "a", 10);
|
||||
|
||||
test.equal(sub.calls.changed.length, 1);
|
||||
test.equal(sub.calls.changed[0].fields, { a: 11 });
|
||||
|
||||
Facts._setActiveSubscriptions([]);
|
||||
});
|
||||
|
||||
// -- subscription added uses correct collection name --
|
||||
|
||||
Tinytest.add("facts-base - subscription added uses 'meteor_Facts_server' collection", (test) => {
|
||||
Facts.resetServerFacts();
|
||||
const sub = mockSub();
|
||||
Facts._setActiveSubscriptions([sub]);
|
||||
|
||||
Facts.incrementServerFact("test-pkg", "val", 1);
|
||||
|
||||
test.equal(sub.calls.added[0].collection, "meteor_Facts_server");
|
||||
|
||||
Facts._setActiveSubscriptions([]);
|
||||
});
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<template name='serverFacts'>
|
||||
<template name="serverFacts">
|
||||
<ul>
|
||||
{{#each factsByPackage}}
|
||||
<li>{{ _id }}
|
||||
<dl>
|
||||
{{#each facts}}
|
||||
<dt>{{ name }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
{{/each}}
|
||||
</dl>
|
||||
</li>
|
||||
<li>
|
||||
{{ _id }}
|
||||
<dl>
|
||||
{{#each facts}}
|
||||
<dt>{{ name }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
{{/each}}
|
||||
</dl>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Facts, FACTS_COLLECTION, FACTS_PUBLICATION } from 'meteor/facts-base';
|
||||
import { Facts, FACTS_COLLECTION, FACTS_PUBLICATION } from "meteor/facts-base";
|
||||
|
||||
Facts.server = new Mongo.Collection(FACTS_COLLECTION);
|
||||
|
||||
@@ -7,11 +7,10 @@ Template.serverFacts.helpers({
|
||||
facts: function () {
|
||||
const factArray = [];
|
||||
Object.entries(this).forEach(function ([name, value]) {
|
||||
if (name !== '_id')
|
||||
factArray.push({name: name, value: value});
|
||||
if (name !== "_id") factArray.push({ name: name, value: value });
|
||||
});
|
||||
return factArray;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Subscribe when the template is first made, and unsubscribe when it
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
Package.describe({
|
||||
summary: "Display internal app statistics",
|
||||
version: '1.0.2',
|
||||
version: "1.0.2",
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use([
|
||||
'ecmascript',
|
||||
'facts-base',
|
||||
'mongo',
|
||||
'templating@1.4.2'
|
||||
], 'client');
|
||||
api.use(["ecmascript", "facts-base", "mongo", "templating@1.4.2"], "client");
|
||||
|
||||
api.imply('facts-base');
|
||||
api.imply("facts-base");
|
||||
|
||||
api.addFiles('facts_ui.html', 'client');
|
||||
api.mainModule('facts_ui_client.js', 'client');
|
||||
api.addFiles("facts_ui.html", "client");
|
||||
api.mainModule("facts_ui_client.js", "client");
|
||||
|
||||
api.export('Facts', 'client');
|
||||
api.export("Facts", "client");
|
||||
});
|
||||
|
||||
@@ -20,22 +20,15 @@ async function main(builder) {
|
||||
|
||||
// Always match the protocol (http or https) and the domain:port of the
|
||||
// current page.
|
||||
const url = [
|
||||
"//" +
|
||||
location.host +
|
||||
methodNameStats +
|
||||
"?cacheBuster=" +
|
||||
Math.random().toString(36).slice(2)
|
||||
].join();
|
||||
const url = `//${location.host}${methodNameStats}?cacheBuster=${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
try {
|
||||
const data = await fetch(url, { method: "GET" });
|
||||
new builder({ container }).loadJson(await data.json())
|
||||
} catch (err) {
|
||||
console.error([
|
||||
packageName + ": Couldn't load stats for visualization.",
|
||||
"Are you using standard-minifier-js >= 2.1.0 as the minifier?",
|
||||
].join(" "))
|
||||
} catch {
|
||||
console.error(
|
||||
`${packageName}: Couldn't load stats for visualization. Are you using standard-minifier-js >= 2.1.0 as the minifier?`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ function getStatBundles() {
|
||||
function readOrNull(file) {
|
||||
try {
|
||||
return JSON.parse(fsReadFileSync(file, "utf8"));
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ function d3TreeFromStats(stats) {
|
||||
.map(name =>
|
||||
sizeOrDetail(name
|
||||
// Change the "packages/bundle.js" name to "(bundle)"
|
||||
.replace(/^[^\/]+\/(.*)\.js$/, "($1)"),
|
||||
.replace(/^[^/]+\/(.*)\.js$/, "($1)"),
|
||||
stats.minifiedBytesByPackage[name]));
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ function statsMiddleware(request, response) {
|
||||
|
||||
sendJSON({
|
||||
name: "main",
|
||||
children: statBundles.map((statBundle, index, array) => ({
|
||||
children: statBundles.map((statBundle) => ({
|
||||
name: statBundle.arch,
|
||||
type: typeBundle,
|
||||
children: d3TreeFromStats(statBundle.stats),
|
||||
|
||||
@@ -41,8 +41,6 @@ import {
|
||||
prefixedClass,
|
||||
} from "./common.js";
|
||||
|
||||
import * as classes from "./classNames.js";
|
||||
|
||||
// Dimensions of sunburst.
|
||||
const width = 950;
|
||||
const height = 600;
|
||||
@@ -75,32 +73,32 @@ export class Sunburst {
|
||||
this.elements.main =
|
||||
this.elements.container
|
||||
.append("div")
|
||||
.attr("class", prefixedClass("main"));
|
||||
.attr("class", prefixedClass("main"));
|
||||
|
||||
this.elements.pillContainer =
|
||||
this.elements.container
|
||||
.append("div")
|
||||
.attr("class", prefixedClass("pills"));
|
||||
.attr("class", prefixedClass("pills"));
|
||||
|
||||
this.elements.sequence =
|
||||
this.elements.main
|
||||
.append("div")
|
||||
.attr("class", prefixedClass("sequence"));
|
||||
.attr("class", prefixedClass("sequence"));
|
||||
|
||||
this.elements.chart =
|
||||
this.elements.main
|
||||
.append("div")
|
||||
.attr("class", prefixedClass("chart"));
|
||||
.attr("class", prefixedClass("chart"));
|
||||
|
||||
this.elements.explanation =
|
||||
this.elements.chart
|
||||
.append("div")
|
||||
.attr("class", prefixedClass("explanation"));
|
||||
.attr("class", prefixedClass("explanation"));
|
||||
|
||||
this.elements.percentage =
|
||||
this.elements.explanation
|
||||
.append("span")
|
||||
.attr("class", prefixedClass("percentage"));
|
||||
.attr("class", prefixedClass("percentage"));
|
||||
|
||||
// BR between percentage and bytes.
|
||||
this.elements.explanation.append("br");
|
||||
@@ -108,7 +106,7 @@ export class Sunburst {
|
||||
this.elements.bytes =
|
||||
this.elements.explanation
|
||||
.append("span")
|
||||
.attr("class", prefixedClass("bytes"));
|
||||
.attr("class", prefixedClass("bytes"));
|
||||
|
||||
this.partition = d3.partition()
|
||||
.size([2 * Math.PI, radius * radius]);
|
||||
@@ -182,24 +180,24 @@ export class Sunburst {
|
||||
});
|
||||
}
|
||||
|
||||
draw(json, i) {
|
||||
draw(json) {
|
||||
const svg = this.elements.chart
|
||||
.append("svg:svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.style("display", "none");
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.style("display", "none");
|
||||
|
||||
const vis = svg
|
||||
.append("svg:g")
|
||||
.attr("class", prefixedClass("top"))
|
||||
.attr("transform", `translate(${width / 2},${height / 2})`)
|
||||
.attr("class", prefixedClass("top"))
|
||||
.attr("transform", `translate(${width / 2},${height / 2})`)
|
||||
|
||||
// Bounding circle underneath the sunburst, to make it easier to detect
|
||||
// when the mouse leaves the parent g.
|
||||
vis
|
||||
.append("svg:circle")
|
||||
.attr("r", radius)
|
||||
.style("opacity", 0);
|
||||
.attr("r", radius)
|
||||
.style("opacity", 0);
|
||||
|
||||
// Add the mouseleave handler to the bounding circle.
|
||||
vis.on("mouseleave", this.mouseleaveEvent());
|
||||
@@ -219,12 +217,12 @@ export class Sunburst {
|
||||
.data(this.nodes)
|
||||
.enter()
|
||||
.append("svg:path")
|
||||
.attr("display", d => d.depth ? null : "none")
|
||||
.attr("d", this.arc)
|
||||
.attr("fill-rule", "evenodd")
|
||||
.style("fill", d => this.getColor(d.data))
|
||||
.style("opacity", 1)
|
||||
.on("mouseover", this.mouseoverEvent());
|
||||
.attr("display", d => d.depth ? null : "none")
|
||||
.attr("d", this.arc)
|
||||
.attr("fill-rule", "evenodd")
|
||||
.style("fill", d => this.getColor(d.data))
|
||||
.style("opacity", 1)
|
||||
.on("mouseover", this.mouseoverEvent());
|
||||
|
||||
// // Get total size of the tree = value of root node from partition.
|
||||
const totalSize = this.path.datum().value;
|
||||
@@ -268,7 +266,7 @@ export class Sunburst {
|
||||
|
||||
const sequenceArray = d.ancestors().reverse();
|
||||
sequenceArray.shift(); // remove root node from the array
|
||||
this.updateBreadcrumbs(sequenceArray, percentageString);
|
||||
this.updateBreadcrumbs(sequenceArray);
|
||||
|
||||
// Fade all the segments.
|
||||
d3.selectAll("path")
|
||||
@@ -284,7 +282,7 @@ export class Sunburst {
|
||||
// Restore everything to full opacity when moving off the visualization.
|
||||
mouseleaveEvent() {
|
||||
const self = this;
|
||||
return self.mouseleave || (self.mouseleave = function (d) {
|
||||
return self.mouseleave || (self.mouseleave = function (_d) {
|
||||
// Hide the breadcrumb trail
|
||||
self.elements.trail
|
||||
.style("visibility", "hidden");
|
||||
@@ -297,7 +295,7 @@ export class Sunburst {
|
||||
.transition()
|
||||
.duration(1000)
|
||||
.style("opacity", 1)
|
||||
.on("end", function() {
|
||||
.on("end", function () {
|
||||
d3.select(this).on("mouseover", self.mouseoverEvent());
|
||||
});
|
||||
|
||||
@@ -306,8 +304,8 @@ export class Sunburst {
|
||||
});
|
||||
}
|
||||
|
||||
// Update the breadcrumb trail to show the current sequence and percentage.
|
||||
updateBreadcrumbs(nodeArray, percentageString) {
|
||||
// Update the breadcrumb trail to show the current sequence.
|
||||
updateBreadcrumbs(nodeArray) {
|
||||
// Data join; key function combines name and depth (= position in sequence).
|
||||
const trail = this.elements.trail
|
||||
.selectAll("div")
|
||||
@@ -319,9 +317,9 @@ export class Sunburst {
|
||||
// Add breadcrumb and label for entering nodes.
|
||||
const entering = trail.enter()
|
||||
.append("div")
|
||||
.attr("class", prefixedClass("trailSegment"))
|
||||
.style("background-color", d => this.getColor(d.data))
|
||||
.text(d => d.data.name);
|
||||
.attr("class", prefixedClass("trailSegment"))
|
||||
.style("background-color", d => this.getColor(d.data))
|
||||
.text(d => d.data.name);
|
||||
|
||||
// Merge enter and update selections; set position for all nodes.
|
||||
entering
|
||||
|
||||
@@ -1,16 +1,80 @@
|
||||
Tinytest.addAsync("mongo-decimal - insert/find Decimal", async function (test) {
|
||||
// -- EJSON integration --
|
||||
|
||||
Tinytest.add("mongo-decimal - typeName returns 'Decimal'", (test) => {
|
||||
const d = Decimal("1.5");
|
||||
test.equal(d.typeName(), "Decimal");
|
||||
});
|
||||
|
||||
Tinytest.add("mongo-decimal - toJSONValue returns string representation", (test) => {
|
||||
const d = Decimal("3.141592653589793");
|
||||
const json = d.toJSONValue();
|
||||
test.equal(typeof json, "string");
|
||||
test.equal(json, "3.141592653589793");
|
||||
});
|
||||
|
||||
Tinytest.add("mongo-decimal - clone produces equal but independent copy", (test) => {
|
||||
const original = Decimal("99.99");
|
||||
const copy = original.clone();
|
||||
|
||||
test.equal(copy.toString(), original.toString());
|
||||
// They must be different object instances
|
||||
test.isFalse(copy === original);
|
||||
});
|
||||
|
||||
Tinytest.add("mongo-decimal - EJSON.stringify and EJSON.parse round-trip", (test) => {
|
||||
const d = Decimal("2.718281828459045");
|
||||
const str = EJSON.stringify({ value: d });
|
||||
const parsed = EJSON.parse(str);
|
||||
|
||||
test.equal(parsed.value.toString(), d.toString());
|
||||
test.instanceOf(parsed.value, Decimal);
|
||||
});
|
||||
|
||||
Tinytest.add("mongo-decimal - EJSON.clone preserves Decimal", (test) => {
|
||||
const d = Decimal("42");
|
||||
const cloned = EJSON.clone(d);
|
||||
|
||||
test.equal(cloned.toString(), "42");
|
||||
test.isFalse(cloned === d);
|
||||
test.instanceOf(cloned, Decimal);
|
||||
});
|
||||
|
||||
Tinytest.add("mongo-decimal - Decimal handles zero correctly", (test) => {
|
||||
const d = Decimal("0");
|
||||
test.equal(d.toString(), "0");
|
||||
test.equal(d.toJSONValue(), "0");
|
||||
test.equal(d.typeName(), "Decimal");
|
||||
});
|
||||
|
||||
Tinytest.add("mongo-decimal - Decimal handles negative numbers", (test) => {
|
||||
const d = Decimal("-123.456");
|
||||
test.equal(d.toString(), "-123.456");
|
||||
|
||||
const json = EJSON.stringify({ n: d });
|
||||
const parsed = EJSON.parse(json);
|
||||
test.equal(parsed.n.toString(), "-123.456");
|
||||
});
|
||||
|
||||
Tinytest.add("mongo-decimal - Decimal handles very large numbers", (test) => {
|
||||
const big = "9999999999999999999999999999999999.9999";
|
||||
const d = Decimal(big);
|
||||
// Verify round-trip through EJSON preserves value
|
||||
const str = EJSON.stringify({ v: d });
|
||||
const parsed = EJSON.parse(str);
|
||||
test.equal(parsed.v.toString(), d.toString());
|
||||
});
|
||||
|
||||
// -- MongoDB insert/find (server only) --
|
||||
|
||||
Tinytest.addAsync("mongo-decimal - insert/find Decimal", async (test) => {
|
||||
// TODO [fibers]: this should work on the client as well.
|
||||
// it looks like we should insert just in the minimongo and then test,
|
||||
// but right now the coll.insertAsync is finishing when the server side finishes
|
||||
// meaning the data on the client side is no longer there. Maybe the idea of accept callbacks
|
||||
// on the new Async methods could solve these issues.
|
||||
if (Meteor.isClient) return;
|
||||
|
||||
var coll = new Mongo.Collection("mongo-decimal");
|
||||
var pi = Decimal("3.141592653589793");
|
||||
const coll = new Mongo.Collection("mongo-decimal");
|
||||
const pi = Decimal("3.141592653589793");
|
||||
|
||||
await coll.insertAsync({ pi: pi });
|
||||
var found = await coll.findOneAsync({ pi: pi });
|
||||
await coll.insertAsync({ pi });
|
||||
const found = await coll.findOneAsync({ pi });
|
||||
|
||||
test.equal(found.pi, pi);
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ Package.onUse(function (api) {
|
||||
Package.onTest(function (api) {
|
||||
api.use('mongo');
|
||||
api.use('mongo-decimal');
|
||||
api.use('ejson');
|
||||
api.use('insecure');
|
||||
api.use(['tinytest']);
|
||||
api.addFiles('decimal_tests.js', ['client', 'server']);
|
||||
|
||||
@@ -14,3 +14,8 @@ Package.onUse(function (api) {
|
||||
|
||||
api.export('XmlBuilder', 'server');
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use(['tinytest', 'xmlbuilder']);
|
||||
api.addFiles('xmlbuilder_tests.js', 'server');
|
||||
});
|
||||
|
||||
50
packages/non-core/xmlbuilder/xmlbuilder_tests.js
Normal file
50
packages/non-core/xmlbuilder/xmlbuilder_tests.js
Normal file
@@ -0,0 +1,50 @@
|
||||
Tinytest.add("xmlbuilder - XmlBuilder is exported and defined", (test) => {
|
||||
test.isTrue(typeof XmlBuilder === "object" || typeof XmlBuilder === "function");
|
||||
});
|
||||
|
||||
Tinytest.add("xmlbuilder - create returns an object with methods", (test) => {
|
||||
const root = XmlBuilder.create("root");
|
||||
test.isTrue(typeof root === "object");
|
||||
test.isTrue(typeof root.end === "function");
|
||||
});
|
||||
|
||||
Tinytest.add("xmlbuilder - simple element generation", (test) => {
|
||||
const xml = XmlBuilder.create("root")
|
||||
.ele("child", "hello")
|
||||
.end({ pretty: false });
|
||||
|
||||
test.matches(xml, /<root>/);
|
||||
test.matches(xml, /<child>hello<\/child>/);
|
||||
test.matches(xml, /<\/root>/);
|
||||
});
|
||||
|
||||
Tinytest.add("xmlbuilder - element with attributes", (test) => {
|
||||
const xml = XmlBuilder.create("item")
|
||||
.att("id", "42")
|
||||
.att("type", "test")
|
||||
.end({ pretty: false });
|
||||
|
||||
test.matches(xml, /id="42"/);
|
||||
test.matches(xml, /type="test"/);
|
||||
});
|
||||
|
||||
Tinytest.add("xmlbuilder - nested elements", (test) => {
|
||||
const xml = XmlBuilder.create("parent")
|
||||
.ele("child")
|
||||
.ele("grandchild", "value")
|
||||
.end({ pretty: false });
|
||||
|
||||
test.matches(xml, /<parent>/);
|
||||
test.matches(xml, /<child>/);
|
||||
test.matches(xml, /<grandchild>value<\/grandchild>/);
|
||||
});
|
||||
|
||||
Tinytest.add("xmlbuilder - XML declaration is included by default", (test) => {
|
||||
const xml = XmlBuilder.create("root").end();
|
||||
test.matches(xml, /^<\?xml version="1\.0"\?>/);
|
||||
});
|
||||
|
||||
Tinytest.add("xmlbuilder - empty element", (test) => {
|
||||
const xml = XmlBuilder.create("empty").end({ pretty: false });
|
||||
test.matches(xml, /<empty\/>/);
|
||||
});
|
||||
161
scripts/list_package_changes.sh
Normal file
161
scripts/list_package_changes.sh
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# list_package_changes.sh
|
||||
# Lists folders changed inside /package for every open PR
|
||||
#
|
||||
# Usage:
|
||||
# ./list_package_changes.sh [--output file.json] [--exclude-author author1,author2,...]
|
||||
#
|
||||
# Examples:
|
||||
# ./list_package_changes.sh
|
||||
# ./list_package_changes.sh --output packages_by_pr.json
|
||||
# ./list_package_changes.sh --exclude-author dependabot,renovate
|
||||
# ./list_package_changes.sh --output packages_by_pr.json --exclude-author dependabot
|
||||
#
|
||||
# Requirements:
|
||||
# - gh CLI installed and authenticated (gh auth login)
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OUTPUT_FILE=""
|
||||
EXCLUDE_AUTHORS=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--exclude-author)
|
||||
EXCLUDE_AUTHORS="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Check gh CLI is available and authenticated ---
|
||||
if ! command -v gh &>/dev/null; then
|
||||
echo "❌ gh CLI not found. Install it at https://cli.github.com/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! gh auth status &>/dev/null 2>&1; then
|
||||
echo "❌ gh CLI is not authenticated. Run: gh auth login"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Detect owner/repo from git remote ---
|
||||
REPO=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null || echo "")
|
||||
if [[ -z "$REPO" ]]; then
|
||||
echo "❌ Could not detect repository. Run this script inside a git repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Repository: $REPO"
|
||||
echo "⬇️ Fetching open PRs..."
|
||||
|
||||
# --- Fetch all open PRs ---
|
||||
ALL_PRS=$(gh pr list \
|
||||
--repo "$REPO" \
|
||||
--state open \
|
||||
--limit 500 \
|
||||
--json number,title,headRefName,author,url)
|
||||
|
||||
TOTAL=$(echo "$ALL_PRS" | jq 'length')
|
||||
echo "✅ $TOTAL open PRs found"
|
||||
echo ""
|
||||
|
||||
# --- Loop through each PR ---
|
||||
RESULT="{}"
|
||||
FOUND_ANY=false
|
||||
|
||||
while IFS= read -r pr; do
|
||||
PR_NUM=$(echo "$pr" | jq -r '.number')
|
||||
PR_TITLE=$(echo "$pr" | jq -r '.title')
|
||||
PR_BRANCH=$(echo "$pr" | jq -r '.headRefName')
|
||||
PR_AUTHOR=$(echo "$pr" | jq -r '.author.login')
|
||||
PR_URL=$(echo "$pr" | jq -r '.url')
|
||||
|
||||
# Skip excluded authors
|
||||
if [[ -n "$EXCLUDE_AUTHORS" ]]; then
|
||||
IFS=',' read -ra EXCLUDED <<< "$EXCLUDE_AUTHORS"
|
||||
SKIP=false
|
||||
for excluded in "${EXCLUDED[@]}"; do
|
||||
if [[ "$PR_AUTHOR" == "$excluded" ]]; then
|
||||
SKIP=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$SKIP" == true ]]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fetch changed files for this PR
|
||||
PR_FILES=$(gh pr view "$PR_NUM" \
|
||||
--repo "$REPO" \
|
||||
--json files \
|
||||
-q '.files[].path' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$PR_FILES" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Filter files inside /packages and extract immediate subfolder
|
||||
# e.g. packages/my-module/src/foo.ts → packages/my-module
|
||||
PACKAGES=$(echo "$PR_FILES" \
|
||||
| (grep -E '^packages/' || true) \
|
||||
| awk -F'/' '{print $1"/"$2}' \
|
||||
| sort -u)
|
||||
|
||||
if [[ -z "$PACKAGES" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
FOUND_ANY=true
|
||||
|
||||
# Print to terminal
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "PR #$PR_NUM — $PR_TITLE"
|
||||
echo "👤 $PR_AUTHOR | 🌿 $PR_BRANCH"
|
||||
echo "🔗 $PR_URL"
|
||||
echo "📁 Changed folders in /package:"
|
||||
while IFS= read -r pkg; do
|
||||
echo " • $pkg"
|
||||
done <<< "$PACKAGES"
|
||||
echo ""
|
||||
|
||||
# Accumulate JSON result — keyed by package name
|
||||
PR_ENTRY=$(jq -n \
|
||||
--argjson num "$PR_NUM" \
|
||||
--arg title "$PR_TITLE" \
|
||||
--arg branch "$PR_BRANCH" \
|
||||
--arg author "$PR_AUTHOR" \
|
||||
--arg url "$PR_URL" \
|
||||
'{pr: $num, title: $title, branch: $branch, author: $author, url: $url}')
|
||||
while IFS= read -r pkg; do
|
||||
RESULT=$(echo "$RESULT" | jq --arg pkg "$pkg" --argjson entry "$PR_ENTRY" \
|
||||
'if has($pkg) then .[$pkg] += [$entry] else . + {($pkg): [$entry]} end')
|
||||
done <<< "$PACKAGES"
|
||||
|
||||
done < <(echo "$ALL_PRS" | jq -c '.[]')
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if [[ "$FOUND_ANY" == false ]]; then
|
||||
echo "ℹ️ No PRs found that change files inside /packages."
|
||||
else
|
||||
PKG_COUNT=$(echo "$RESULT" | jq 'keys | length')
|
||||
echo "📊 $PKG_COUNT package(s) touched across open PRs."
|
||||
fi
|
||||
|
||||
# --- Save JSON if requested ---
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
echo "$RESULT" | jq '.' > "$OUTPUT_FILE"
|
||||
echo "💾 Results saved to: $OUTPUT_FILE"
|
||||
fi
|
||||
Reference in New Issue
Block a user