Skip to content

Commit cc5c841

Browse files
authored
feat(cli): add command line tool (#2)
1 parent ba42ef6 commit cc5c841

15 files changed

Lines changed: 786 additions & 0 deletions

packages/cli/README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# @srcset/cli
2+
3+
[![ESM-only package][package]][package-url]
4+
[![NPM version][npm]][npm-url]
5+
[![Node version][node]][node-url]
6+
[![Dependencies status][deps]][deps-url]
7+
[![Install size][size]][size-url]
8+
[![Build status][build]][build-url]
9+
[![Coverage status][coverage]][coverage-url]
10+
11+
[package]: https://img.shields.io/badge/package-ESM--only-ffe536.svg
12+
[package-url]: https://nodejs.org/api/esm.html
13+
14+
[npm]: https://img.shields.io/npm/v/%40srcset%2Fcli.svg
15+
[npm-url]: https://npmjs.com/package/@srcset/cli
16+
17+
[node]: https://img.shields.io/node/v/%40srcset%2Fcli.svg
18+
[node-url]: https://nodejs.org
19+
20+
[deps]: https://img.shields.io/librariesio/release/npm/%40srcset%2Fcli
21+
[deps-url]: https://libraries.io/npm/%40srcset%2Fcli
22+
23+
[size]: https://packagephobia.com/badge?p=%40srcset%2Fcli
24+
[size-url]: https://packagephobia.com/result?p=%40srcset%2Fcli
25+
26+
[build]: https://img.shields.io/github/actions/workflow/status/TrigenSoftware/srcset/tests.yml?branch=main
27+
[build-url]: https://github.com/TrigenSoftware/srcset/actions
28+
29+
[coverage]: https://img.shields.io/codecov/c/github/TrigenSoftware/srcset.svg
30+
[coverage-url]: https://app.codecov.io/gh/TrigenSoftware/srcset
31+
32+
Command line tool to generate responsive images.
33+
34+
- 🌠 Resize images for different screen sizes and densities
35+
- 📸 Convert images to [modern formats](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images#use_modern_image_formats_boldly) such as [WebP](https://developers.google.com/speed/webp) and [Avif](https://jakearchibald.com/2020/avif-has-landed/)
36+
- ⚡️ Powered by [sharp](https://sharp.pixelplumbing.com/)
37+
- 🎯 Glob sources, match rules and `srcset.config.js` configuration
38+
39+
## Install
40+
41+
```bash
42+
# pnpm
43+
pnpm add -D @srcset/cli
44+
# yarn
45+
yarn add -D @srcset/cli
46+
# npm
47+
npm i -D @srcset/cli
48+
```
49+
50+
## Usage
51+
52+
```bash
53+
npx srcset "src/images/*.jpg" --width 1920,1280,860,320 --format avif,webp,jpg -d static/images
54+
```
55+
56+
Or with the `srcset.config.js` configuration file - an ES module, so set `"type": "module"` in your `package.json` or pass an `.mjs` file via `--config`:
57+
58+
```js
59+
export default {
60+
src: 'src/images/**/*.jpg',
61+
dest: 'static/images',
62+
rules: [
63+
{
64+
match: '(min-width: 1920px)',
65+
width: [1920, 1280, 860, 320],
66+
format: ['avif', 'webp', 'jpg']
67+
}
68+
]
69+
}
70+
```
71+
72+
## Documentation
73+
74+
For more details, guides and API references, check out the [documentation website](https://srcset.js.org/integrations/cli/).

packages/cli/oxlint.config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineConfig } from '@trigen/oxlint'
2+
import testConfig from '@trigen/oxlint-config/test'
3+
import tsTypeCheckedConfig from '@trigen/oxlint-config/typescript-type-checked'
4+
import rootConfig from '../../oxlint.config.ts'
5+
6+
export default defineConfig({
7+
extends: [
8+
rootConfig,
9+
tsTypeCheckedConfig,
10+
testConfig
11+
],
12+
env: {
13+
node: true,
14+
browser: false
15+
}
16+
})

packages/cli/package.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"name": "@srcset/cli",
3+
"type": "module",
4+
"version": "1.0.0",
5+
"description": "Command line tool to generate responsive images.",
6+
"author": {
7+
"name": "Dan Onoshko",
8+
"email": "danon0404@gmail.com",
9+
"url": "https://github.com/dangreen"
10+
},
11+
"license": "MIT",
12+
"homepage": "https://srcset.js.org/integrations/cli/",
13+
"funding": "https://ko-fi.com/dangreen",
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/TrigenSoftware/srcset.git",
17+
"directory": "packages/cli"
18+
},
19+
"bugs": {
20+
"url": "https://github.com/TrigenSoftware/srcset/issues"
21+
},
22+
"keywords": [
23+
"srcset",
24+
"image",
25+
"responsive",
26+
"cli"
27+
],
28+
"engines": {
29+
"node": ">=22"
30+
},
31+
"bin": {
32+
"srcset": "./dist/cli.js"
33+
},
34+
"publishConfig": {
35+
"directory": "package",
36+
"linkDirectory": false
37+
},
38+
"files": [
39+
"dist"
40+
],
41+
"scripts": {
42+
"clear:package": "del ./package",
43+
"clear:dist": "del ./dist",
44+
"clear": "del ./package ./dist ./coverage",
45+
"prepublishOnly": "run build clear:package clean-publish",
46+
"postpublish": "pnpm clear:package",
47+
"emitDeclarations": "tsc -p ./tsconfig.build.json --emitDeclarationOnly",
48+
"build:dist": "run -p emitDeclarations [ vite build ]",
49+
"build": "run clear:dist build:dist",
50+
"lint": "oxlint",
51+
"format": "oxlint --fix",
52+
"test:unit": "vitest run --coverage",
53+
"test:unit:watch": "vitest watch",
54+
"test:types": "tsc --noEmit",
55+
"test": "run -p lint test:unit test:types"
56+
},
57+
"dependencies": {
58+
"@srcset/core": "workspace:^",
59+
"argue-cli": "^3.1.0",
60+
"tinyglobby": "^0.2.10"
61+
},
62+
"devDependencies": {
63+
"@types/node": "^22.0.0",
64+
"sharp": "^0.35.3"
65+
}
66+
}

packages/cli/src/args.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
describe,
3+
it,
4+
expect
5+
} from 'vitest'
6+
import { setArgs } from 'argue-cli'
7+
import { parseCliArgs } from './args.ts'
8+
9+
describe('cli', () => {
10+
describe('args', () => {
11+
describe('parseCliArgs', () => {
12+
it('should parse sources and options', () => {
13+
setArgs(
14+
'images/**/*.jpg',
15+
'--dest',
16+
'dist',
17+
'--verbose',
18+
'--skip-optimization',
19+
'--concurrency',
20+
'2'
21+
)
22+
23+
const args = parseCliArgs()
24+
25+
expect(args.sources).toEqual(['images/**/*.jpg'])
26+
expect(args.dest).toBe('dist')
27+
expect(args.verbose).toBe(true)
28+
expect(args.skipOptimization).toBe(true)
29+
expect(args.concurrency).toBe(2)
30+
expect(args.help).toBe(false)
31+
expect(args.rule).toBeNull()
32+
})
33+
34+
it('should build rule from match, width and format', () => {
35+
setArgs(
36+
'-m',
37+
'**/*.jpg',
38+
'-w',
39+
'1,0.5',
40+
'-f',
41+
'webp',
42+
'-f',
43+
'jpg'
44+
)
45+
46+
expect(parseCliArgs().rule).toEqual({
47+
match: ['**/*.jpg'],
48+
width: [1, 0.5],
49+
format: ['webp', 'jpg']
50+
})
51+
})
52+
53+
it('should support camelCase twins and negation', () => {
54+
setArgs('--skipOptimization', '--no-scalingUp')
55+
56+
const args = parseCliArgs()
57+
58+
expect(args.skipOptimization).toBe(true)
59+
expect(args.scalingUp).toBe(false)
60+
})
61+
62+
it('should parse help flag', () => {
63+
setArgs('-h')
64+
65+
expect(parseCliArgs().help).toBe(true)
66+
})
67+
})
68+
})
69+
})

packages/cli/src/args.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { ImageFormat } from '@srcset/core'
2+
import {
3+
rest,
4+
alias,
5+
autocase,
6+
flag,
7+
option,
8+
readOptions
9+
} from 'argue-cli'
10+
import type { SrcSetCliRule } from './types.ts'
11+
12+
export const usage = `srcset [...sources] [...options]
13+
14+
sources Source image(s) glob patterns.
15+
--help, -h Print this message.
16+
--verbose, -v Print processed images.
17+
--match, -m Glob pattern(s) or media query(ies) to match images by name or size. Multiple values must all match.
18+
--width, -w Output image(s) widths to resize, value less than or equal to 1 is treated as a multiplier.
19+
--format, -f Output image(s) formats to convert.
20+
--skip-optimization Do not optimize output images.
21+
--no-scaling-up Do not generate images larger than the source.
22+
--dest, -d Destination directory.
23+
--config, -c Config file path. Defaults to the \`srcset.config.js\` lookup.
24+
--concurrency Concurrency limit.
25+
`
26+
27+
export interface CliArgs {
28+
help: boolean
29+
verbose: boolean | undefined
30+
sources: string[]
31+
rule: SrcSetCliRule | null
32+
skipOptimization: boolean | undefined
33+
scalingUp: boolean | undefined
34+
dest: string | undefined
35+
config: string | undefined
36+
concurrency: number | undefined
37+
}
38+
39+
/**
40+
* Parse command line arguments.
41+
* @returns Parsed options.
42+
*/
43+
export function parseCliArgs(): CliArgs {
44+
const {
45+
help,
46+
verbose,
47+
match,
48+
width,
49+
format,
50+
skipOptimization,
51+
scalingUp,
52+
dest,
53+
config,
54+
concurrency
55+
} = readOptions(
56+
flag(alias('help', 'h')),
57+
flag(alias('verbose', 'v')),
58+
option(alias('match', 'm'), Array),
59+
option(alias('width', 'w'), Array),
60+
option(alias('format', 'f'), Array),
61+
flag(autocase('skipOptimization')),
62+
flag(autocase('scalingUp')),
63+
option(alias('dest', 'd'), String),
64+
option(alias('config', 'c'), String),
65+
option('concurrency', Number)
66+
)
67+
const rule: SrcSetCliRule = {
68+
...match && {
69+
match
70+
},
71+
...width && {
72+
width: width.map(Number)
73+
},
74+
...format && {
75+
format: format as ImageFormat[]
76+
}
77+
}
78+
79+
return {
80+
help: Boolean(help),
81+
verbose,
82+
sources: rest(),
83+
rule: Object.keys(rule).length ? rule : null,
84+
skipOptimization,
85+
scalingUp,
86+
dest,
87+
config,
88+
concurrency
89+
}
90+
}

packages/cli/src/cli.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import process from 'node:process'
2+
import type { SrcSetCliOptions } from './types.ts'
3+
import {
4+
usage,
5+
parseCliArgs
6+
} from './args.ts'
7+
import { loadConfig } from './config.ts'
8+
import { run } from './run.ts'
9+
10+
const args = parseCliArgs()
11+
12+
if (args.help) {
13+
console.info(usage)
14+
process.exit(0)
15+
}
16+
17+
try {
18+
const config = await loadConfig(args.config)
19+
const options: SrcSetCliOptions = {
20+
...config,
21+
src: args.sources.length ? args.sources : config.src ?? [],
22+
dest: args.dest ?? config.dest ?? '',
23+
rules: args.rule ? [args.rule] : config.rules,
24+
...args.verbose !== undefined && {
25+
verbose: args.verbose
26+
},
27+
...args.skipOptimization !== undefined && {
28+
skipOptimization: args.skipOptimization
29+
},
30+
...args.scalingUp !== undefined && {
31+
scalingUp: args.scalingUp
32+
},
33+
...args.concurrency !== undefined && {
34+
concurrency: args.concurrency
35+
}
36+
}
37+
38+
if (!options.src.length) {
39+
throw new Error('No source images: pass glob patterns or set `src` in the config.')
40+
}
41+
42+
if (!options.dest) {
43+
throw new Error('No destination directory: pass `--dest` or set `dest` in the config.')
44+
}
45+
46+
await run(options)
47+
} catch (error) {
48+
console.error(error instanceof Error ? error.message : error)
49+
process.exit(1)
50+
}

0 commit comments

Comments
 (0)