diff --git a/.plans/github_action_implementation_plan.md b/.plans/github_action_implementation_plan.md index c7892ff97..8af8f9948 100644 --- a/.plans/github_action_implementation_plan.md +++ b/.plans/github_action_implementation_plan.md @@ -1,5 +1,9 @@ # Implementation Plan - GitHub Action for node-minify +> **Status:** ✅ Complete +> **Updated:** 2026-01-27 +> **Published as:** `srod/node-minify@v1` + ## Context Create a GitHub Action that runs node-minify in CI/CD pipelines, providing: @@ -878,32 +882,30 @@ jobs: ### Phase 1 (Composite Action) -- [ ] `.github/actions/node-minify/action.yml` -- [ ] `.github/actions/node-minify/scripts/run.sh` -- [ ] Documentation in existing docs +Skipped — went directly to JavaScript Action (Phase 2). ### Phase 2 (JavaScript Action) -- [ ] `packages/action/package.json` -- [ ] `packages/action/tsconfig.json` -- [ ] `packages/action/action.yml` -- [ ] `packages/action/src/index.ts` -- [ ] `packages/action/src/inputs.ts` -- [ ] `packages/action/src/minify.ts` -- [ ] `packages/action/src/compare.ts` -- [ ] `packages/action/src/outputs.ts` -- [ ] `packages/action/src/types.ts` -- [ ] `packages/action/src/reporters/summary.ts` -- [ ] `packages/action/src/reporters/comment.ts` -- [ ] `packages/action/src/reporters/annotations.ts` -- [ ] `packages/action/README.md` -- [ ] `packages/action/__tests__/action.test.ts` - -### Files to Modify - -- [ ] Root `action.yml` (symlink or copy for marketplace) -- [ ] `.github/workflows/release-action.yml` -- [ ] Docs site: new page for GitHub Action +- [x] `packages/action/package.json` +- [x] `packages/action/tsconfig.json` +- [x] `packages/action/action.yml` +- [x] `packages/action/src/index.ts` +- [x] `packages/action/src/inputs.ts` +- [x] `packages/action/src/minify.ts` +- [x] `packages/action/src/compare.ts` +- [x] `packages/action/src/outputs.ts` +- [x] `packages/action/src/types.ts` +- [x] `packages/action/src/reporters/summary.ts` +- [x] `packages/action/src/reporters/comment.ts` → `packages/action/src/comment.ts` +- [x] `packages/action/src/reporters/annotations.ts` → `packages/action/src/annotations.ts` +- [x] `packages/action/README.md` +- [x] `packages/action/__tests__/action.test.ts` + +### Files Modified + +- [x] Root `action.yml` (in `packages/action/action.yml`, referenced by marketplace) +- [x] `.github/workflows/test-action.yml` +- [x] Docs site: `docs/src/content/docs/github-action.md` --- diff --git a/packages/esbuild/src/index.ts b/packages/esbuild/src/index.ts index f71cf40af..dacc42fe5 100644 --- a/packages/esbuild/src/index.ts +++ b/packages/esbuild/src/index.ts @@ -5,7 +5,10 @@ */ import type { CompressorResult, MinifierOptions } from "@node-minify/types"; -import { ensureStringContent } from "@node-minify/utils"; +import { + ensureStringContent, + extractSourceMapOption, +} from "@node-minify/utils"; import { transform } from "esbuild"; /** @@ -30,12 +33,14 @@ export async function esbuild({ } const loader = settings.type === "css" ? "css" : "js"; - const { sourceMap, ...restOptions } = settings?.options ?? {}; + const { sourceMap, restOptions } = extractSourceMapOption( + settings?.options + ); const result = await transform(contentStr, { loader, minify: true, - sourcemap: !!sourceMap, + sourcemap: sourceMap, ...restOptions, }); diff --git a/packages/lightningcss/src/index.ts b/packages/lightningcss/src/index.ts index fd2e78e60..54aa36735 100644 --- a/packages/lightningcss/src/index.ts +++ b/packages/lightningcss/src/index.ts @@ -5,7 +5,10 @@ */ import type { CompressorResult, MinifierOptions } from "@node-minify/types"; -import { ensureStringContent } from "@node-minify/utils"; +import { + ensureStringContent, + extractSourceMapOption, +} from "@node-minify/utils"; import { transform } from "lightningcss"; /** @@ -21,14 +24,16 @@ export async function lightningCss({ }: MinifierOptions): Promise { const contentStr = ensureStringContent(content, "lightningcss"); - const options = settings?.options ?? {}; + const { sourceMap, restOptions } = extractSourceMapOption( + settings?.options + ); const result = transform({ filename: "input.css", code: Buffer.from(contentStr), minify: true, - sourceMap: !!options.sourceMap, - ...options, + sourceMap, + ...restOptions, }); return { diff --git a/packages/oxc/src/index.ts b/packages/oxc/src/index.ts index 43cb42ebc..dda7be28d 100644 --- a/packages/oxc/src/index.ts +++ b/packages/oxc/src/index.ts @@ -7,6 +7,7 @@ import type { CompressorResult, MinifierOptions } from "@node-minify/types"; import { ensureStringContent, + extractSourceMapOption, validateMinifyResult, wrapMinificationError, } from "@node-minify/utils"; @@ -24,12 +25,14 @@ export async function oxc({ content, }: MinifierOptions): Promise { const contentStr = ensureStringContent(content, "oxc"); - const options = settings?.options ?? {}; + const { sourceMap, restOptions } = extractSourceMapOption( + settings?.options + ); try { const result = await oxcMinify("input.js", contentStr, { - sourcemap: !!options.sourceMap, - ...options, + sourcemap: sourceMap, + ...restOptions, }); validateMinifyResult(result, "oxc"); diff --git a/packages/swc/src/index.ts b/packages/swc/src/index.ts index 2352d19ee..7744a5e58 100644 --- a/packages/swc/src/index.ts +++ b/packages/swc/src/index.ts @@ -5,7 +5,10 @@ */ import type { CompressorResult, MinifierOptions } from "@node-minify/types"; -import { ensureStringContent } from "@node-minify/utils"; +import { + ensureStringContent, + extractSourceMapOption, +} from "@node-minify/utils"; import { minify as swcMinify } from "@swc/core"; /** @@ -21,13 +24,15 @@ export async function swc({ }: MinifierOptions): Promise { const contentStr = ensureStringContent(content, "swc"); - const options = settings?.options ?? {}; + const { sourceMap, restOptions } = extractSourceMapOption( + settings?.options + ); const result = await swcMinify(contentStr, { compress: true, mangle: true, - sourceMap: !!options.sourceMap, - ...options, + sourceMap, + ...restOptions, }); return { diff --git a/packages/utils/__tests__/sourceMap.test.ts b/packages/utils/__tests__/sourceMap.test.ts new file mode 100644 index 000000000..fa4fe883b --- /dev/null +++ b/packages/utils/__tests__/sourceMap.test.ts @@ -0,0 +1,124 @@ +/*! + * node-minify + * Copyright (c) 2011-2026 Rodolphe Stoclin + * MIT Licensed + */ + +import { describe, expect, test } from "vitest"; +import { + extractSourceMapOption, + getSourceMapBoolean, +} from "../src/sourceMap.ts"; + +describe("getSourceMapBoolean", () => { + test("returns false for undefined options", () => { + expect(getSourceMapBoolean(undefined)).toBe(false); + }); + + test("returns false for empty options object", () => { + expect(getSourceMapBoolean({})).toBe(false); + }); + + test("returns false when sourceMap is false", () => { + expect(getSourceMapBoolean({ sourceMap: false })).toBe(false); + }); + + test("returns false when sourceMap is null", () => { + expect(getSourceMapBoolean({ sourceMap: null })).toBe(false); + }); + + test("returns false when sourceMap is undefined", () => { + expect(getSourceMapBoolean({ sourceMap: undefined })).toBe(false); + }); + + test("returns true when sourceMap is true", () => { + expect(getSourceMapBoolean({ sourceMap: true })).toBe(true); + }); + + test("returns true when sourceMap is 1", () => { + expect(getSourceMapBoolean({ sourceMap: 1 })).toBe(true); + }); + + test("returns true when sourceMap is a non-empty string", () => { + expect(getSourceMapBoolean({ sourceMap: "true" })).toBe(true); + }); + + test("returns true when sourceMap is an object", () => { + expect(getSourceMapBoolean({ sourceMap: {} })).toBe(true); + }); + + test("ignores other options", () => { + expect( + getSourceMapBoolean({ + sourceMap: true, + compress: true, + mangle: false, + }) + ).toBe(true); + }); +}); + +describe("extractSourceMapOption", () => { + test("returns false and empty object for undefined options", () => { + const result = extractSourceMapOption(undefined); + expect(result.sourceMap).toBe(false); + expect(result.restOptions).toEqual({}); + }); + + test("returns false and empty object for empty options", () => { + const result = extractSourceMapOption({}); + expect(result.sourceMap).toBe(false); + expect(result.restOptions).toEqual({}); + }); + + test("returns false and empty object when sourceMap is false", () => { + const result = extractSourceMapOption({ sourceMap: false }); + expect(result.sourceMap).toBe(false); + expect(result.restOptions).toEqual({}); + }); + + test("returns true and empty object when sourceMap is true", () => { + const result = extractSourceMapOption({ sourceMap: true }); + expect(result.sourceMap).toBe(true); + expect(result.restOptions).toEqual({}); + }); + + test("returns true and preserves other options", () => { + const result = extractSourceMapOption({ + sourceMap: true, + compress: true, + mangle: false, + }); + expect(result.sourceMap).toBe(true); + expect(result.restOptions).toEqual({ + compress: true, + mangle: false, + }); + }); + + test("returns false and preserves other options when sourceMap is false", () => { + const result = extractSourceMapOption({ + sourceMap: false, + compress: true, + mangle: false, + }); + expect(result.sourceMap).toBe(false); + expect(result.restOptions).toEqual({ + compress: true, + mangle: false, + }); + }); + + test("returns true when sourceMap is truthy value", () => { + const result = extractSourceMapOption({ sourceMap: 1 }); + expect(result.sourceMap).toBe(true); + expect(result.restOptions).toEqual({}); + }); + + test("does not mutate original options object", () => { + const original = { sourceMap: true, compress: true }; + const result = extractSourceMapOption(original); + expect(original).toEqual({ sourceMap: true, compress: true }); + expect(result.restOptions).toEqual({ compress: true }); + }); +}); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 09b03861b..ad4184c23 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -33,6 +33,7 @@ import { readFile, readFileAsync } from "./readFile.ts"; import { run } from "./run.ts"; import { setFileNameMin } from "./setFileNameMin.ts"; import { setPublicFolder } from "./setPublicFolder.ts"; +import { extractSourceMapOption, getSourceMapBoolean } from "./sourceMap.ts"; import type { BuildArgsOptions } from "./types.ts"; import type { WildcardOptions } from "./wildcards.ts"; import { DEFAULT_IGNORES, wildcards } from "./wildcards.ts"; @@ -44,6 +45,7 @@ export { DEFAULT_IGNORES, deleteFile, ensureStringContent, + extractSourceMapOption, getContentFromFiles, getContentFromFilesAsync, getFilesizeBrotliInBytes, @@ -52,6 +54,7 @@ export { getFilesizeGzippedRaw, getFilesizeInBytes, getKnownExportName, + getSourceMapBoolean, isBuiltInCompressor, isImageFile, isLocalPath, diff --git a/packages/utils/src/sourceMap.ts b/packages/utils/src/sourceMap.ts new file mode 100644 index 000000000..92ae9b298 --- /dev/null +++ b/packages/utils/src/sourceMap.ts @@ -0,0 +1,38 @@ +/*! + * node-minify + * Copyright (c) 2011-2026 Rodolphe Stoclin + * MIT Licensed + */ + +/** + * Normalize sourceMap option to boolean for compressors that only accept boolean. + * + * @param options - Compressor options object that may contain a `sourceMap` property + * @returns `true` if `sourceMap` is truthy, `false` otherwise + */ +export function getSourceMapBoolean( + options?: Record +): boolean { + return !!options?.sourceMap; +} + +/** + * Extract sourceMap from options, returning the boolean flag and remaining options separately. + * Useful for compressors where sourceMap must be handled as a distinct parameter. + * + * @param options - Compressor options object that may contain a `sourceMap` property + * @returns Object with `sourceMap` boolean and `restOptions` without the sourceMap key + */ +export function extractSourceMapOption(options?: Record): { + sourceMap: boolean; + restOptions: Record; +} { + if (!options) { + return { sourceMap: false, restOptions: {} }; + } + const { sourceMap, ...restOptions } = options; + return { + sourceMap: !!sourceMap, + restOptions, + }; +}