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:
Italo José
2026-04-10 07:35:24 -03:00
committed by GitHub
14 changed files with 425 additions and 79 deletions

View File

@@ -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/

View File

@@ -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/

View File

@@ -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([]);
});

View File

@@ -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>

View File

@@ -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

View File

@@ -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");
});

View File

@@ -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?`
)
}
}

View File

@@ -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),

View File

@@ -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

View File

@@ -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);
});

View File

@@ -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']);

View File

@@ -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');
});

View 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\/>/);
});

View 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