Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/action/src/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@

import type { ActionInputs } from "./types.ts";

/**
* Check whether a percent reduction violates configured thresholds.
*
* @param reduction - Percent change in size after minification (positive means size decreased; negative means size increased)
* @param inputs - Configuration with:
* - `failOnIncrease`: if true, treat any increase (negative `reduction`) as a violation
* - `minReduction`: minimum allowed reduction percentage; values below this are violations when > 0
* @returns A human-readable error message describing the threshold violation, or `null` if no violation
*/
export function checkThresholds(
reduction: number,
inputs: Pick<ActionInputs, "failOnIncrease" | "minReduction">
Expand All @@ -19,4 +28,4 @@ export function checkThresholds(
}

return null;
}
}
9 changes: 8 additions & 1 deletion packages/action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import { addAnnotations } from "./reporters/annotations.ts";
import { postPRComment } from "./reporters/comment.ts";
import { generateSummary } from "./reporters/summary.ts";

/**
* Orchestrates the minification workflow for the GitHub Action.
*
* Parses and validates inputs, runs the minification, and persists outputs.
* Optionally generates a summary, posts a pull-request comment when running in a PR, and adds annotations based on inputs.
* If configured thresholds are violated or an error is thrown, signals action failure with an explanatory message.
*/
async function run(): Promise<void> {
try {
const inputs = parseInputs();
Expand Down Expand Up @@ -56,4 +63,4 @@ async function run(): Promise<void> {
}
}

run();
run();
27 changes: 26 additions & 1 deletion packages/action/src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ const DEPRECATED_COMPRESSORS: Record<string, string> = {
sqwish: "sqwish is no longer maintained. Use 'lightningcss' or 'clean-css' instead.",
};

/**
* Parse and validate GitHub Action inputs into an ActionInputs object.
*
* Reads inputs such as `input`, `output`, `compressor`, `type`, `options`,
* reporting flags, benchmarking settings, and other flags, applying defaults
* and validations (including JSON parsing for `options` and required `type`
* for certain compressors).
*
* @returns An object containing the parsed action inputs, including `input`,
* `output`, `compressor`, `type`, `options`, report flags, benchmark settings,
* `minReduction`, `includeGzip`, `workingDirectory`, and `githubToken`.
*
* @throws Error if a compressor that requires a `type` is selected but `type`
* is not provided.
* @throws Error if the `options` input is present but is not valid JSON.
*/
export function parseInputs(): ActionInputs {
const compressor = getInput("compressor") || "terser";
const type = getInput("type") as "js" | "css" | undefined;
Expand Down Expand Up @@ -64,6 +80,15 @@ export function parseInputs(): ActionInputs {
};
}

/**
* Validates a compressor identifier and emits warnings for deprecated or non-built-in compressors.
*
* Emits a warning when the compressor is listed as deprecated and emits a separate warning
* when the compressor is not recognized as a built-in compressor (indicating it will be
* treated as a custom npm package or local file).
*
* @param compressor - The compressor name or identifier to validate (e.g., "terser", "esbuild", or a custom package)
*/
export function validateCompressor(compressor: string): void {
const deprecationMessage = DEPRECATED_COMPRESSORS[compressor];
if (deprecationMessage) {
Expand All @@ -80,4 +105,4 @@ export function validateCompressor(compressor: string): void {

export const validateJavaCompressor = validateCompressor;

export { DEPRECATED_COMPRESSORS, JAVA_COMPRESSORS, TYPE_REQUIRED_COMPRESSORS };
export { DEPRECATED_COMPRESSORS, JAVA_COMPRESSORS, TYPE_REQUIRED_COMPRESSORS };
23 changes: 22 additions & 1 deletion packages/action/src/minify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,32 @@ import { minify } from "@node-minify/core";
import { getFilesizeGzippedRaw, resolveCompressor } from "@node-minify/utils";
import type { ActionInputs, FileResult, MinifyResult } from "./types.ts";

/**
* Get the size of a file in bytes.
*
* @param filePath - Path to the file
* @returns The file size in bytes
*/
async function getFileSize(filePath: string): Promise<number> {
const stats = await stat(filePath);
return stats.size;
}

/**
* Minifies a single input file according to the provided action inputs and returns summary metrics.
*
* Uses `inputs` to resolve input/output paths, run the selected compressor with optional type and options,
* and optionally computes gzipped size. The result includes per-file metrics and aggregated totals.
*
* @param inputs - Configuration for the minification run (input/output paths relative to `workingDirectory`, `compressor` selection, optional `type` and `options`, and `includeGzip` flag)
* @returns A `MinifyResult` containing:
* - `files`: an array with one `FileResult` (`file`, `originalSize`, `minifiedSize`, `reduction`, optional `gzipSize`, `timeMs`)
* - `compressor`: the human-readable compressor label
* - `totalOriginalSize`: original file size in bytes
* - `totalMinifiedSize`: minified file size in bytes
* - `totalReduction`: percentage reduction (0–100)
* - `totalTimeMs`: elapsed minification time in milliseconds
*/
export async function runMinification(
inputs: ActionInputs
): Promise<MinifyResult> {
Expand Down Expand Up @@ -67,4 +88,4 @@ export async function runMinification(
totalReduction: reduction,
totalTimeMs: timeMs,
};
}
}
14 changes: 13 additions & 1 deletion packages/action/src/outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
import { setOutput } from "@actions/core";
import type { BenchmarkResult, MinifyResult } from "./types.ts";

/**
* Publishes minification metrics from a MinifyResult to GitHub Actions outputs.
*
* Sets the following outputs: `original-size`, `minified-size`, `reduction-percent` (formatted to two decimal places), `time-ms`, and `report-json`. If per-file gzip sizes are present, also sets `gzip-size` to the total gzip size across files.
*
* @param result - Minification summary containing total and per-file metrics used to populate action outputs
*/
export function setMinifyOutputs(result: MinifyResult): void {
setOutput("original-size", result.totalOriginalSize);
setOutput("minified-size", result.totalMinifiedSize);
Expand All @@ -23,6 +30,11 @@ export function setMinifyOutputs(result: MinifyResult): void {
}
}

/**
* Exposes benchmark metrics as GitHub Actions outputs.
*
* @param result - The benchmark result object whose properties are published as Action outputs. When present, `recommended` is written to `benchmark-winner`, `bestCompression` to `best-compression`, and `bestSpeed` to `best-speed`. The entire `result` is written as JSON to `benchmark-json`.
*/
export function setBenchmarkOutputs(result: BenchmarkResult): void {
if (result.recommended) {
setOutput("benchmark-winner", result.recommended);
Expand All @@ -34,4 +46,4 @@ export function setBenchmarkOutputs(result: BenchmarkResult): void {
setOutput("best-speed", result.bestSpeed);
}
setOutput("benchmark-json", JSON.stringify(result));
}
}
18 changes: 17 additions & 1 deletion packages/action/src/reporters/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import type { MinifyResult } from "../types.ts";
const LOW_REDUCTION_THRESHOLD = 20;
const VERY_LOW_REDUCTION_THRESHOLD = 5;

/**
* Emit GitHub Actions annotations for each minified file based on its size reduction.
*
* For each file in `result.files` this reports an annotation scoped to that file:
* - An error if the minified file is larger than the original.
* - A warning if the reduction is very small, suggesting review for dead code or prior minification.
* - A notice if the reduction is low, suggesting the file may already be optimized.
*
* @param result - Minification result containing an array of file reports with reduction percentages and file paths
*/
export function addAnnotations(result: MinifyResult): void {
for (const file of result.files) {
if (file.reduction < 0) {
Expand All @@ -34,6 +44,12 @@ export function addAnnotations(result: MinifyResult): void {
}
}

/**
* Record an error annotation for a specific file indicating that minification failed.
*
* @param file - The file path to associate with the annotation
* @param errorMsg - A human-readable message describing the minification error
*/
export function addErrorAnnotation(file: string, errorMsg: string): void {
error(`Minification failed: ${errorMsg}`, { file });
}
}
14 changes: 13 additions & 1 deletion packages/action/src/reporters/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import type { MinifyResult } from "../types.ts";

const COMMENT_TAG = "<!-- node-minify-report -->";

/**
* Posts or updates a pull request comment with a minification report.
*
* @param result - Minification results containing per-file metrics, totals, compressor name, and execution time
* @param githubToken - GitHub API token used to authenticate requests; when `undefined`, the function skips posting
*/
export async function postPRComment(
result: MinifyResult,
githubToken: string | undefined
Expand Down Expand Up @@ -58,6 +64,12 @@ export async function postPRComment(
}
}

/**
* Builds the Markdown body for the node-minify PR comment, including a per-file table, totals, and a configuration section.
*
* @param result - Minification results used to populate the report (expects `files`, `totalOriginalSize`, `totalMinifiedSize`, `totalReduction`, `compressor`, and `totalTimeMs`)
* @returns The Markdown string for the PR comment, beginning with `COMMENT_TAG` and containing the file table, total summary, and configuration details
*/
function generateCommentBody(result: MinifyResult): string {
const filesTable = result.files
.map(
Expand Down Expand Up @@ -86,4 +98,4 @@ ${filesTable}
---
*Generated by [node-minify](https://github.com/srod/node-minify) action*
`;
}
}
17 changes: 16 additions & 1 deletion packages/action/src/reporters/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import { summary } from "@actions/core";
import { prettyBytes } from "@node-minify/utils";
import type { BenchmarkResult, MinifyResult } from "../types.ts";

/**
* Generate a GitHub Actions summary reporting per-file minification metrics and totals.
*
* Builds a Markdown table with columns File, Original, Minified, Reduction, Gzip, and Time,
* includes the compressor name, and appends a total line showing aggregated original/minified sizes and overall reduction.
*
* @param result - Minification results containing per-file metrics and aggregate totals used to populate the summary
*/
export async function generateSummary(result: MinifyResult): Promise<void> {
const rows = result.files.map((f) => [
{ data: `\`${f.file}\`` },
Expand Down Expand Up @@ -40,6 +48,13 @@ export async function generateSummary(result: MinifyResult): Promise<void> {
.write();
}

/**
* Generate a Markdown benchmark summary comparing compressors and write it to the GitHub Actions summary.
*
* Builds a table of compressors showing status, size, reduction, gzip size, and time; marks recommended, best-speed, and best-compression entries with badges; and includes the source file and recommended compressor in the summary.
*
* @param result - BenchmarkResult containing the file path, originalSize, a list of compressors with their metrics or errors, and optional recommended/best markers used to annotate the table
*/
export async function generateBenchmarkSummary(
result: BenchmarkResult
): Promise<void> {
Expand Down Expand Up @@ -90,4 +105,4 @@ export async function generateBenchmarkSummary(
.addBreak()
.addRaw(`**Recommended:** ${result.recommended || "N/A"}`)
.write();
}
}
9 changes: 8 additions & 1 deletion packages/benchmark/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ export async function runBenchmark(
};
}

/**
* Benchmarks a single input file using the configured compressors and returns per-compressor metrics.
*
* @param file - Path to the input file to benchmark
* @param options - Benchmark configuration (compressors to run, iterations, callbacks, and metric options)
* @returns A FileResult containing the file path, original size in bytes and human-readable form, and an array of CompressorMetrics for each attempted compressor
*/
async function benchmarkFile(
file: string,
options: BenchmarkOptions
Expand Down Expand Up @@ -228,4 +235,4 @@ function cleanupTempFiles(files: string[]): void {
unlinkSync(file);
} catch {}
}
}
}
19 changes: 9 additions & 10 deletions packages/utils/src/getFilesizeGzippedInBytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import { isValidFile } from "./isValidFile.ts";
import { prettyBytes } from "./prettyBytes.ts";

/**
* Internal helper to calculate gzipped size of a file using streaming.
* @param file - Path to the file
* @returns Gzipped size in bytes
* @throws {FileOperationError} If file doesn't exist or is not a valid file
* Compute the gzipped size of a file in bytes.
*
* @param file - Path to the file to measure
* @returns The gzipped size in bytes
* @throws FileOperationError if the file does not exist or the path is not a valid file
* @internal
*/
async function getGzipSize(file: string): Promise<number> {
Expand Down Expand Up @@ -45,12 +46,10 @@ async function getGzipSize(file: string): Promise<number> {
}

/**
* Get the gzipped file size as a human-readable string.
* Get the gzipped size of a file as a human-readable string.
*
* @param file - Path to the file
* @returns Formatted gzipped file size string (e.g., "1.5 kB")
* @example
* const size = await getFilesizeGzippedInBytes('bundle.js')
* console.log(size) // '12.3 kB'
* @returns The gzipped size formatted for display (for example, "1.5 kB")
*/
export async function getFilesizeGzippedInBytes(file: string): Promise<string> {
try {
Expand Down Expand Up @@ -83,4 +82,4 @@ export async function getFilesizeGzippedRaw(file: string): Promise<number> {
error as Error
);
}
}
}
23 changes: 22 additions & 1 deletion scripts/check-published.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { join } from "node:path";

const PACKAGES_DIR = "packages";

/**
* Lists package subdirectories under PACKAGES_DIR that contain a package.json file.
*
* @returns An array of directory names for packages that have a package.json in their folder
*/
function getPackageDirs() {
return readdirSync(PACKAGES_DIR, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
Expand All @@ -13,6 +18,13 @@ function getPackageDirs() {
.map((entry) => entry.name);
}

/**
* Checks whether an npm package exists on the registry and whether a specific version is published.
*
* @param packageName - The npm package name to check (e.g., "lodash")
* @param version - The package version to check for publication (e.g., "1.2.3")
* @returns An object with `exists`: `true` if the package is found on the npm registry, `false` otherwise; and `publishedVersion`: `true` if the specified `version` is published, `false` otherwise.
*/
async function checkPublished(packageName: string, version: string) {
try {
const latest = execSync(`npm view ${packageName} version --json`, {
Expand All @@ -38,6 +50,15 @@ async function checkPublished(packageName: string, version: string) {
}
}

/**
* Checks all non-private packages under the packages directory and reports their publication status on npm.
*
* Scans each package's package.json, queries npm to determine whether the package exists and whether the current
* package version is published, and prints categorized results to the console:
* - packages not found on npm (new packages)
* - packages with unpublished versions (updates needed)
* - or a confirmation that all packages are up to date
*/
async function main() {
const dirs = getPackageDirs();
const results = [];
Expand Down Expand Up @@ -81,4 +102,4 @@ async function main() {
main().catch((err) => {
console.error("Error checking packages:", err);
process.exit(1);
});
});