minify-html-css
Advanced tools
+251
| #!/usr/bin/env node | ||
| // src/cli.ts | ||
| import { readFileSync, writeFileSync } from "fs"; | ||
| import { resolve } from "path"; | ||
| // src/minify-css.ts | ||
| import { | ||
| transform | ||
| } from "lightningcss"; | ||
| // src/utils/merge-options.ts | ||
| function mergeOptions(targetOptions, sourceOptions, disabledOptions3, callback) { | ||
| const mergedOptions = structuredClone(targetOptions); | ||
| for (const key in sourceOptions) { | ||
| if (disabledOptions3?.includes(key)) { | ||
| continue; | ||
| } | ||
| const targetValue = targetOptions[key]; | ||
| const sourceValue = sourceOptions[key]; | ||
| let mergedValue = sourceValue; | ||
| if (callback) { | ||
| const callbackResult = callback( | ||
| key, | ||
| targetValue, | ||
| sourceValue, | ||
| targetOptions, | ||
| sourceOptions | ||
| ); | ||
| if (callbackResult !== void 0) { | ||
| mergedValue = callbackResult; | ||
| mergedOptions[key] = mergedValue; | ||
| continue; | ||
| } | ||
| } | ||
| if (isPlainObject(sourceValue) && Object.hasOwn(targetOptions, key) && isPlainObject(targetValue)) { | ||
| mergedValue = mergeOptions(targetValue, sourceValue); | ||
| } | ||
| mergedOptions[key] = mergedValue; | ||
| } | ||
| return mergedOptions; | ||
| } | ||
| function isPlainObject(value) { | ||
| return typeof value === "object" && !Array.isArray(value) && value !== null; | ||
| } | ||
| // src/minify-css.ts | ||
| var disabledOptions = [ | ||
| "filename", | ||
| "code", | ||
| "sourceMap", | ||
| "inputSourceMap", | ||
| "projectRoot", | ||
| "include", | ||
| "exclude", | ||
| "visitor", | ||
| "customAtRules" | ||
| ]; | ||
| function minifyCSS(input, options) { | ||
| const baseOptions = { | ||
| filename: "", | ||
| code: Buffer.from(input), | ||
| minify: true, | ||
| sourceMap: false | ||
| }; | ||
| let mergedOptions = baseOptions; | ||
| if (options) { | ||
| mergedOptions = mergeOptions(baseOptions, options, disabledOptions); | ||
| } | ||
| const transformResult = transform(mergedOptions); | ||
| return { ...transformResult, code: transformResult.code.toString() }; | ||
| } | ||
| // src/minify-html.ts | ||
| import { minifyFragmentSync } from "@swc/html"; | ||
| var disabledOptions2 = [ | ||
| "context_element", | ||
| "filename", | ||
| "forceSetHtml5Doctype", | ||
| "form_element", | ||
| "iframeSrcdoc", | ||
| "mode", | ||
| "scriptingEnabled", | ||
| "selfClosingVoidElements" | ||
| ]; | ||
| function minifyHTML(input, options) { | ||
| const baseOptions = { | ||
| collapseWhitespaces: "all", | ||
| sortSpaceSeparatedAttributeValues: false, | ||
| minifyCss: { lib: "lightningcss" } | ||
| }; | ||
| let mergedOptions = baseOptions; | ||
| if (options) { | ||
| mergedOptions = mergeOptions( | ||
| baseOptions, | ||
| options, | ||
| disabledOptions2, | ||
| (key, targetValue, sourceValue) => { | ||
| if (key === "minifyCss") { | ||
| if (sourceValue === true) { | ||
| return targetValue; | ||
| } else { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| const transformOutput = minifyFragmentSync(input, mergedOptions); | ||
| return transformOutput; | ||
| } | ||
| // src/utils/cli-utils.ts | ||
| function parseOptions(args) { | ||
| const options = {}; | ||
| for (let i = 0; i < args.length; i++) { | ||
| const arg = args[i]; | ||
| switch (arg) { | ||
| case "-i": | ||
| case "--input": | ||
| options.input = args[++i]; | ||
| break; | ||
| case "-o": | ||
| case "--output": | ||
| options.output = args[++i]; | ||
| break; | ||
| case "-t": | ||
| case "--type": | ||
| options.type = args[++i]; | ||
| break; | ||
| case "-h": | ||
| case "--help": | ||
| options.help = true; | ||
| break; | ||
| case "-v": | ||
| case "--version": | ||
| options.version = true; | ||
| break; | ||
| default: | ||
| if (arg.startsWith("-")) { | ||
| console.error(`Unknown option: ${arg}.`); | ||
| process.exit(1); | ||
| } | ||
| if (options.input) { | ||
| console.error(`Unknown positional argument: ${arg}.`); | ||
| process.exit(1); | ||
| } | ||
| options.input = arg; | ||
| } | ||
| } | ||
| return options; | ||
| } | ||
| function detectFileType(filePath) { | ||
| const ext = filePath.split(".").pop()?.toLowerCase(); | ||
| if (ext === "html" || ext === "htm") return "html"; | ||
| if (ext === "css") return "css"; | ||
| return null; | ||
| } | ||
| // src/cli.ts | ||
| var helpText = ` | ||
| Usage: minify-html-css <input> [options] | ||
| Positional arguments: | ||
| <input> Input file path (can be provided as a positional argument or with -i/--input) | ||
| Options: | ||
| -i, --input <file> Input file path (required unless provided as a positional argument) | ||
| -o, --output <file> Output file path (optional, defaults to stdout) | ||
| -t, --type <type> File type: html or css (auto-detected if not specified) | ||
| -h, --help Display this help message | ||
| -v, --version Display version number | ||
| Examples: | ||
| minify-html-css input.html -o output.html | ||
| minify-html-css -i input.html -o output.html | ||
| minify-html-css -i styles.css -o styles.min.css | ||
| minify-html-css -i index.html -t html | ||
| minify-html-css input.html > output.html | ||
| minify-html-css -i input.html > output.html | ||
| minify-html-css ./path/to/file.css -o ./path/to/file.min.css | ||
| `; | ||
| function main() { | ||
| const args = process.argv.slice(2); | ||
| const options = parseOptions(args); | ||
| if (options.help) { | ||
| console.log(helpText); | ||
| process.exit(0); | ||
| } | ||
| if (options.version) { | ||
| console.log("1.1.0"); | ||
| process.exit(0); | ||
| } | ||
| if (!options.input) { | ||
| console.error("Error: Input file is required."); | ||
| console.log(helpText); | ||
| process.exit(1); | ||
| } | ||
| try { | ||
| const inputPath = resolve(process.cwd(), options.input); | ||
| const content = readFileSync(inputPath, "utf-8"); | ||
| let fileType; | ||
| if (options.type) { | ||
| const normalizedType = options.type.toLowerCase(); | ||
| if (normalizedType !== "html" && normalizedType !== "css") { | ||
| console.error( | ||
| 'Error: Invalid file type. The file type must be "html" or "css".' | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| fileType = normalizedType; | ||
| } else { | ||
| const detectedFileType = detectFileType(options.input); | ||
| if (!detectedFileType) { | ||
| console.error( | ||
| "Error: Could not detect file type. Please specify using -t option." | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| fileType = detectedFileType; | ||
| } | ||
| let result; | ||
| switch (fileType) { | ||
| case "html": | ||
| result = minifyHTML(content); | ||
| break; | ||
| case "css": | ||
| result = minifyCSS(content); | ||
| break; | ||
| default: | ||
| throw new Error("Unexpected file type"); | ||
| } | ||
| if (options.output) { | ||
| const outputPath = resolve(process.cwd(), options.output); | ||
| writeFileSync(outputPath, result.code, "utf-8"); | ||
| console.log(`Minified ${options.input} written to: ${outputPath}.`); | ||
| process.exit(0); | ||
| } else { | ||
| console.log(result.code); | ||
| process.exit(0); | ||
| } | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| console.error(`Error: ${error.message}.`); | ||
| } else { | ||
| console.error("An unknown error occurred."); | ||
| } | ||
| process.exit(1); | ||
| } | ||
| } | ||
| main(); |
+4
-4
@@ -24,3 +24,3 @@ import * as lightningcss from 'lightningcss'; | ||
| drafts?: { | ||
| /** Whether to enable @custom-media rules. */ | ||
| /** Whether to enable `@custom-media` rules. */ | ||
| customMedia?: boolean; | ||
@@ -30,3 +30,3 @@ }; | ||
| nonStandard?: { | ||
| /** Whether to enable the non-standard >>> and /deep/ selector combinators used by Angular and Vue. */ | ||
| /** Whether to enable the non-standard `>>>` and `/deep/` selector combinators used by Angular and Vue. */ | ||
| deepSelectorCombinator?: boolean; | ||
@@ -73,5 +73,5 @@ }; | ||
| /** | ||
| * A list of class names, ids, and custom identifiers (e.g. @keyframes) that are known | ||
| * A list of class names, ids, and custom identifiers (e.g. `@keyframes`) that are known | ||
| * to be unused. These will be removed during minification. Note that these are not | ||
| * selectors but individual names (without any . or # prefixes). | ||
| * selectors but individual names (without any `.` or `#` prefixes). | ||
| */ | ||
@@ -78,0 +78,0 @@ unusedSymbols?: string[]; |
+4
-4
@@ -24,3 +24,3 @@ import * as lightningcss from 'lightningcss'; | ||
| drafts?: { | ||
| /** Whether to enable @custom-media rules. */ | ||
| /** Whether to enable `@custom-media` rules. */ | ||
| customMedia?: boolean; | ||
@@ -30,3 +30,3 @@ }; | ||
| nonStandard?: { | ||
| /** Whether to enable the non-standard >>> and /deep/ selector combinators used by Angular and Vue. */ | ||
| /** Whether to enable the non-standard `>>>` and `/deep/` selector combinators used by Angular and Vue. */ | ||
| deepSelectorCombinator?: boolean; | ||
@@ -73,5 +73,5 @@ }; | ||
| /** | ||
| * A list of class names, ids, and custom identifiers (e.g. @keyframes) that are known | ||
| * A list of class names, ids, and custom identifiers (e.g. `@keyframes`) that are known | ||
| * to be unused. These will be removed during minification. Note that these are not | ||
| * selectors but individual names (without any . or # prefixes). | ||
| * selectors but individual names (without any `.` or `#` prefixes). | ||
| */ | ||
@@ -78,0 +78,0 @@ unusedSymbols?: string[]; |
+22
-16
| { | ||
| "name": "minify-html-css", | ||
| "version": "1.1.0", | ||
| "version": "1.2.0", | ||
| "description": "🔽 A library to minify HTML and CSS.", | ||
@@ -38,2 +38,5 @@ "keywords": [ | ||
| "types": "dist/index.d.ts", | ||
| "bin": { | ||
| "minify-html-css": "./dist/cli.js" | ||
| }, | ||
| "files": [ | ||
@@ -45,24 +48,27 @@ "dist/", | ||
| "scripts": { | ||
| "dev": "tsup --watch", | ||
| "build": "tsup", | ||
| "test": "bun test", | ||
| "test:watch": "bun test --watch", | ||
| "dev": "bun run --bun tsup --watch", | ||
| "build": "bun run --bun tsup", | ||
| "test": "bun test --concurrent", | ||
| "test:watch": "bun run test --watch", | ||
| "pretest:all": "bun run build", | ||
| "test:all": "CLI_TESTS=1 bun run test", | ||
| "format": "biome format --write", | ||
| "lint": "biome lint . --write" | ||
| "lint": "biome lint . --write", | ||
| "release": "semantic-release" | ||
| }, | ||
| "dependencies": { | ||
| "@swc/html": "^1.13.5", | ||
| "lightningcss": "^1.30.1" | ||
| "@swc/html": "^1.15.7", | ||
| "lightningcss": "^1.30.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@biomejs/biome": "2.2.2", | ||
| "@commitlint/cli": "^19.8.1", | ||
| "@commitlint/config-conventional": "^19.8.1", | ||
| "@biomejs/biome": "^2.3.10", | ||
| "@commitlint/cli": "^20.2.0", | ||
| "@commitlint/config-conventional": "^20.2.0", | ||
| "@semantic-release/changelog": "^6.0.3", | ||
| "@semantic-release/git": "^10.0.1", | ||
| "@types/bun": "^1.2.21", | ||
| "lefthook": "^1.12.3", | ||
| "semantic-release": "^24.2.7", | ||
| "tsup": "^8.5.0", | ||
| "typescript": "^5.9.2" | ||
| "@types/bun": "^1.3.5", | ||
| "lefthook": "^2.0.13", | ||
| "semantic-release": "^25.0.2", | ||
| "tsup": "^8.5.1", | ||
| "typescript": "^5.9.3" | ||
| }, | ||
@@ -69,0 +75,0 @@ "engines": { |
+137
-39
@@ -18,2 +18,3 @@ # minify-html-css | ||
| - [API Documentation](#api-documentation) | ||
| - [CLI](#cli) | ||
| - [TODO](#todo) | ||
@@ -66,6 +67,24 @@ - [Contributing](#contributing) | ||
| const minifiedHtml = minifyHTML(html).code; | ||
| console.log(minifiedHtml); // "<div><h1>Hello World!</h1><style>body{color:red}</style></div>" | ||
| const result = minifyHTML(html).code; | ||
| console.log(result); // "<div><h1>Hello World!</h1><style>body{color:red}</style></div>" | ||
| ``` | ||
| ```typescript | ||
| import { minifyCSS } from 'minify-html-css'; | ||
| const css = ` | ||
| /* page styles */ | ||
| :root { --main-color: red; } | ||
| body { | ||
| color: var(--main-color); | ||
| margin: 0; | ||
| line-height: 1.4; | ||
| } | ||
| `; | ||
| const result = minifyCSS(css).code; | ||
| console.log(result); // ":root{--main-color:red}body{color:var(--main-color);margin:0;line-height:1.4}" | ||
| ``` | ||
| --- | ||
@@ -77,59 +96,138 @@ | ||
| **Description:** | ||
| Minifies an HTML string by removing unnecessary whitespace, comments, and compressing inline JS and CSS (where supported). Returns an object containing `code` property for transformed code and `errors` array for possible errors. | ||
| #### minifyHTML: What it does | ||
| Minifies an HTML string (whitespace, comments, attributes, inline assets depending on options). | ||
| #### minifyHTML: Return value | ||
| Returns the underlying `@swc/html` minifier result. In practice you’ll primarily use: | ||
| - `code`: the minified HTML | ||
| - `errors`: an array of parse/minification errors (if any) | ||
| > **Implementation note:** | ||
| > This function is a wrapper around the [`@swc/html`](https://github.com/swc-project/swc/tree/main/packages/html) package and uses its minification logic under the hood. | ||
| > This is a small wrapper around [`@swc/html`](https://github.com/swc-project/swc/tree/main/packages/html) (SWC’s HTML minifier). | ||
| **Parameters:** | ||
| #### minifyHTML: Parameters | ||
| - `input` (`string`): The HTML code to minify. | ||
| - `options?` (`MinifyHTMLOptions`): Configuration object for fine-grained control. | ||
| - `input` (`string`): HTML to minify. | ||
| - `options?` (`MinifyHTMLOptions`): Optional configuration. | ||
| The available options are: | ||
| #### minifyHTML: Options | ||
| | Option | Type | Default | Description | | ||
| | ----------------------------------- | -------------------------------------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------- | | ||
| | `collapseWhitespaces` | `'none' \| 'all' \| 'smart' \| 'conservative' \| 'advanced-conservative' \| 'only-metadata'` | `'all'` | Controls how whitespace is collapsed and removed throughout the document. | | ||
| | `removeEmptyMetadataElements` | `boolean` | `true` | Removes empty metadata elements such as `<script>`, `<style>`, `<meta>`, and `<link>`. | | ||
| | `removeComments` | `boolean` | `true` | Removes all HTML comments unless matched by `preserveComments`. | | ||
| | `preserveComments` | `string[]` | | Array of regex strings; comments matching any are preserved. You can override the default patterns. | | ||
| | `minifyConditionalComments` | `boolean` | `true` | Minifies IE conditional comments. | | ||
| | `removeEmptyAttributes` | `boolean` | `true` | Removes empty attributes from HTML tags (when safe). | | ||
| | `removeRedundantAttributes` | `'none' \| 'all' \| 'smart'` | `'smart'` | Controls removal of redundant or default attributes. | | ||
| | `collapseBooleanAttributes` | `boolean` | `true` | Collapses boolean attributes to their short form (e.g. `checked`). | | ||
| | `normalizeAttributes` | `boolean` | `true` | Cleans up attribute values by removing unnecessary spaces, and strips `javascript:` from event handlers. | | ||
| | `minifyJson` | `boolean \| { pretty?: boolean }` | `true` | Minifies embedded JSON within `<script type="application/json">`. | | ||
| | `minifyJs` | `boolean` | `true` | Minifies inline JavaScript. | | ||
| | `minifyCss` | `boolean` | `true` | Minifies inline CSS. | | ||
| | `minifyAdditionalScriptsContent` | `[string, MinifierType][]` | | Minifies additional `<script>` types, specifying type pattern and minifier. | | ||
| | `minifyAdditionalAttributes` | `[string, MinifierType][]` | | Minifies additional attribute values, specifying attribute name pattern and minifier. | | ||
| | `sortSpaceSeparatedAttributeValues` | `boolean` | `false` | Sorts space-separated attribute values like `class` or `rel`. | | ||
| | `sortAttributes` | `boolean` | `false` | Sorts all attributes of each element in reverse alphabetical order. | | ||
| | `tagOmission` | `boolean` | `true` | Omits optional end tags when valid per HTML spec. | | ||
| | `quotes` | `boolean` | `false` | Always wrap attribute values in quotes. | | ||
| This package keeps the inline docs for options in the type files (with full JSDoc and defaults). Start here: | ||
| For detailed type definitions and documentation, see [`src/minify-html-types.ts`](https://github.com/femincan/minify-html-css/blob/main/src/minify-html-types.ts). | ||
| - [`src/minify-html-types.ts`](src/minify-html-types.ts) (HTML options + `MinifierType`) | ||
| #### minifyHTML: Examples | ||
| ```ts | ||
| import { minifyHTML } from 'minify-html-css'; | ||
| const input = ` | ||
| <div> | ||
| <!-- hi --> | ||
| Again | ||
| </div> | ||
| `; | ||
| // Keep HTML comments | ||
| const result = minifyHTML(input, { removeComments: false }).code; | ||
| console.log(result); // <div><!-- hi -->Again</div> | ||
| ``` | ||
| ### `minifyCSS(input: string, options?: MinifyCSSOptions): TransformResult` | ||
| **Description:** | ||
| Minifies CSS code by removing unnecessary whitespace, optimizing values, and applying various transformations. | ||
| #### minifyCSS: What it does | ||
| Minifies CSS using Lightning CSS. | ||
| #### minifyCSS: Return value | ||
| Returns the underlying `lightningcss` transform result, with one convenience tweak: `code` is returned as a `string` (not a `Buffer`). | ||
| > **Implementation note:** | ||
| > This function is a wrapper around the [`lightningcss`](https://github.com/parcel-bundler/lightningcss) package and uses its minification logic under the hood. | ||
| > This is a wrapper around [`lightningcss`](https://github.com/parcel-bundler/lightningcss). | ||
| **Parameters:** | ||
| #### minifyCSS: Parameters | ||
| - `input` (`string`): The CSS code to minify. | ||
| - `options?` (`MinifyCSSOptions`): Configuration object for fine-grained control. | ||
| - `input` (`string`): CSS to minify. | ||
| - `options?` (`MinifyCSSOptions`): Optional configuration. | ||
| For detailed type definitions and documentation for options, see [`src/minify-css-types.ts`](https://github.com/femincan/minify-html-css/blob/main/src/minify-css-types.ts). | ||
| #### minifyCSS: Options | ||
| See the full option surface (typed + documented) here: | ||
| - [`src/minify-css-types.ts`](src/minify-css-types.ts) | ||
| #### minifyCSS: Example | ||
| ```ts | ||
| import { minifyCSS, minifyHTML } from 'minify-html-css'; | ||
| const input = ` | ||
| body { | ||
| color: red; | ||
| } | ||
| @keyframes slidein { | ||
| from { | ||
| transform: translateX(0%); | ||
| } | ||
| to { | ||
| transform: translateX(100%); | ||
| } | ||
| } | ||
| `; | ||
| // Remove unused @keyframes 'slidein' | ||
| const result = minifyCSS(input, { unusedSymbols: ['slidein'] }).code; | ||
| console.log(result); // body{color:red} | ||
| ``` | ||
| --- | ||
| ### CLI | ||
| This package also ships a CLI binary named `minify-html-css`. | ||
| **Run via `npx` (no global install):** | ||
| ```bash | ||
| npx minify-html-css -i input.html -o output.html | ||
| npx minify-html-css -i styles.css -o styles.min.css | ||
| ``` | ||
| **Or install globally:** | ||
| ```bash | ||
| npm install -g minify-html-css | ||
| minify-html-css -i input.html -o output.html | ||
| ``` | ||
| **Options:** | ||
| - `-i, --input <file>`: input file path (alternatively you can pass the input file as the first positional argument) | ||
| - `-o, --output <file>`: output file path (if omitted, prints to stdout) | ||
| - `-t, --type <html|css>`: file type (auto-detected from extension if omitted) | ||
| - `-h, --help`: show help | ||
| - `-v, --version`: show version | ||
| **Examples:** | ||
| ```bash | ||
| # Auto-detect file type from extension | ||
| minify-html-css index.html > index.min.html | ||
| # Force type when extension can't be detected | ||
| minify-html-css -i input -t html > output.html | ||
| ``` | ||
| --- | ||
| ## TODO | ||
| - [x] Implement CSS minification function (`minifyCSS`) built on top of Lightning CSS | ||
| - [ ] Implement CLI usage (command-line tool) | ||
| - [x] Implement HTML minification function (`minifyHTML`) built on top of `@swc/html` | ||
| - [x] Implement CSS minification function (`minifyCSS`) built on top of `lightningcss` | ||
| - [x] Implement CLI usage (command-line tool) | ||
@@ -136,0 +234,0 @@ --- |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
62035
6.73%10
11.11%788
44.06%269
57.31%1
Infinity%Updated
Updated