better-asset 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/assets.config.schema.json +85 -0
- package/bin/better-asset.js +85 -0
- package/package.json +38 -0
- package/src/clean.js +18 -0
- package/src/config.js +91 -0
- package/src/fs-utils.js +14 -0
- package/src/generate.js +57 -0
- package/src/image.js +48 -0
- package/src/log.js +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sazzadur
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# better-asset
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/better-asset)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Config-driven image asset generator. Resize, convert, and generate favicons from a single JSON config.
|
|
7
|
+
|
|
8
|
+
> Zero config UI. Just a JSON file and one command.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g better-asset
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or use without installing:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx better-asset
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Create a starter config
|
|
26
|
+
better-asset init
|
|
27
|
+
|
|
28
|
+
# Edit assets.config.json, then generate
|
|
29
|
+
better-asset
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## CLI
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
better-asset Generate assets from config
|
|
36
|
+
better-asset init Create a starter config file
|
|
37
|
+
better-asset --config <path> Use a custom config file
|
|
38
|
+
better-asset --help Show help
|
|
39
|
+
better-asset --version Show version
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Supported Formats
|
|
43
|
+
|
|
44
|
+
**Input:** svg, png, jpg/jpeg, webp, avif
|
|
45
|
+
|
|
46
|
+
**Output:** png (default), webp, jpeg, avif, ico
|
|
47
|
+
|
|
48
|
+
## Config File
|
|
49
|
+
|
|
50
|
+
By default `better-asset` looks for `assets.config.json` in the current directory.
|
|
51
|
+
|
|
52
|
+
Add a `$schema` key for editor autocompletion:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"$schema": "https://unpkg.com/better-asset/assets.config.schema.json"
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Example
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"$schema": "https://unpkg.com/better-asset/assets.config.schema.json",
|
|
65
|
+
"source_dir": "public/assets",
|
|
66
|
+
|
|
67
|
+
"assets": [
|
|
68
|
+
{
|
|
69
|
+
"name": "icon",
|
|
70
|
+
"source": "icon.svg",
|
|
71
|
+
"sizes": [48, 96, 144, 512],
|
|
72
|
+
"destination": "public/icons"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "favicon",
|
|
76
|
+
"source": "favicon.svg",
|
|
77
|
+
"sizes": [16, 32, 48],
|
|
78
|
+
"destination": "public/icons",
|
|
79
|
+
"ico": {
|
|
80
|
+
"required": true,
|
|
81
|
+
"size": 96
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "opengraph-image",
|
|
86
|
+
"source": "icon.svg",
|
|
87
|
+
"sizes": ["1200x630"],
|
|
88
|
+
"destination": "public/icons",
|
|
89
|
+
"format": "jpeg"
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Asset Fields
|
|
96
|
+
|
|
97
|
+
| Field | Type | Required | Default | Description |
|
|
98
|
+
| ------------- | ------- | -------- | ------- | -------------------------------------------- |
|
|
99
|
+
| `name` | string | yes | — | Base name for generated files |
|
|
100
|
+
| `source` | string | yes | — | Source filename (relative to `source_dir`) |
|
|
101
|
+
| `sizes` | array | yes | — | Sizes to generate |
|
|
102
|
+
| `destination` | string | yes | — | Output directory (relative to project root) |
|
|
103
|
+
| `format` | string | no | `"png"` | Output format: `png`, `webp`, `jpeg`, `avif` |
|
|
104
|
+
| `sizeInName` | boolean | no | auto | Force include/exclude size in filename |
|
|
105
|
+
| `ico` | object | no | — | Generate a `favicon.ico` |
|
|
106
|
+
|
|
107
|
+
## Size Rules
|
|
108
|
+
|
|
109
|
+
**Square** — use a number:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
48 → 48×48
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Rectangular** — use a string:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
"1200x630" → 1200×630
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## sizeInName
|
|
122
|
+
|
|
123
|
+
By default:
|
|
124
|
+
|
|
125
|
+
- **Multiple sizes** → size is included (`icon-48.png`, `icon-96.png`)
|
|
126
|
+
- **Single size** → size is omitted (`apple-touch-icon.png`)
|
|
127
|
+
|
|
128
|
+
Override per asset:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{ "sizeInName": true }
|
|
132
|
+
{ "sizeInName": false }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## ICO Generation
|
|
136
|
+
|
|
137
|
+
Add an `ico` object to generate a real `favicon.ico`:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"ico": {
|
|
142
|
+
"required": true,
|
|
143
|
+
"size": 96
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Field | Type | Default | Description |
|
|
149
|
+
| ---------- | ------- | ------- | -------------------------------------- |
|
|
150
|
+
| `required` | boolean | — | Whether to generate the `.ico` file |
|
|
151
|
+
| `size` | number | `64` | Size of the embedded PNG in the `.ico` |
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "better-asset Config",
|
|
4
|
+
"description": "Configuration for better-asset — a config-driven image asset generator.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["source_dir", "assets"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Path to the JSON schema file (for editor support)."
|
|
11
|
+
},
|
|
12
|
+
"source_dir": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Directory containing source images, relative to the project root."
|
|
15
|
+
},
|
|
16
|
+
"assets": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"description": "List of asset definitions to generate.",
|
|
19
|
+
"items": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"required": ["name", "source", "sizes", "destination"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"name": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Base name for generated files."
|
|
26
|
+
},
|
|
27
|
+
"source": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Source image filename (relative to source_dir). Supports: svg, png, jpg, jpeg, webp, avif."
|
|
30
|
+
},
|
|
31
|
+
"sizes": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"description": "Sizes to generate. Use a number for square (e.g. 48 → 48x48) or a string for rectangular (e.g. \"1200x630\").",
|
|
34
|
+
"items": {
|
|
35
|
+
"oneOf": [
|
|
36
|
+
{
|
|
37
|
+
"type": "integer",
|
|
38
|
+
"minimum": 1,
|
|
39
|
+
"description": "Square size in pixels."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "string",
|
|
43
|
+
"pattern": "^\\d+x\\d+$",
|
|
44
|
+
"description": "Rectangular size as \"WIDTHxHEIGHT\"."
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"minItems": 1
|
|
49
|
+
},
|
|
50
|
+
"destination": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Output directory for generated files, relative to the project root."
|
|
53
|
+
},
|
|
54
|
+
"format": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"enum": ["png", "webp", "jpeg", "jpg", "avif"],
|
|
57
|
+
"default": "png",
|
|
58
|
+
"description": "Output image format (default: png)."
|
|
59
|
+
},
|
|
60
|
+
"sizeInName": {
|
|
61
|
+
"type": "boolean",
|
|
62
|
+
"description": "Force include/exclude size in filename. Default: included when multiple sizes, omitted for single size."
|
|
63
|
+
},
|
|
64
|
+
"ico": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"description": "Generate a favicon.ico file from this asset.",
|
|
67
|
+
"required": ["required"],
|
|
68
|
+
"properties": {
|
|
69
|
+
"required": {
|
|
70
|
+
"type": "boolean",
|
|
71
|
+
"description": "Whether to generate the .ico file."
|
|
72
|
+
},
|
|
73
|
+
"size": {
|
|
74
|
+
"type": "integer",
|
|
75
|
+
"minimum": 1,
|
|
76
|
+
"default": 64,
|
|
77
|
+
"description": "Size of the embedded PNG in the .ico file (default: 64)."
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs/promises";
|
|
5
|
+
import { generate } from "../src/generate.js";
|
|
6
|
+
import { defaultConfig } from "../src/config.js";
|
|
7
|
+
import * as log from "../src/log.js";
|
|
8
|
+
|
|
9
|
+
const HELP = `
|
|
10
|
+
\x1b[1m\x1b[35mbetter-asset\x1b[0m — config-driven image asset generator
|
|
11
|
+
|
|
12
|
+
\x1b[1mUsage\x1b[0m
|
|
13
|
+
|
|
14
|
+
$ better-asset Generate assets from config
|
|
15
|
+
$ better-asset init Create a starter config file
|
|
16
|
+
$ better-asset --config <path> Use a custom config file
|
|
17
|
+
$ better-asset --help Show this help
|
|
18
|
+
$ better-asset --version Show version
|
|
19
|
+
|
|
20
|
+
\x1b[1mExamples\x1b[0m
|
|
21
|
+
|
|
22
|
+
$ better-asset
|
|
23
|
+
$ better-asset --config my-config.json
|
|
24
|
+
$ better-asset init
|
|
25
|
+
|
|
26
|
+
\x1b[1mLearn More\x1b[0m
|
|
27
|
+
For configuration and usage details, see the documentation:
|
|
28
|
+
https://npmjs.com/package/better-asset
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
async function getVersion() {
|
|
32
|
+
const pkgPath = new URL("../package.json", import.meta.url);
|
|
33
|
+
const raw = await fs.readFile(pkgPath, "utf-8");
|
|
34
|
+
return JSON.parse(raw).version;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function initConfig(cwd) {
|
|
38
|
+
const configPath = path.join(cwd, "assets.config.json");
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await fs.access(configPath);
|
|
42
|
+
log.warn(`assets.config.json already exists — skipping.`);
|
|
43
|
+
return;
|
|
44
|
+
} catch {
|
|
45
|
+
// doesn't exist, good
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await fs.writeFile(configPath, JSON.stringify(defaultConfig(), null, "\t") + "\n");
|
|
49
|
+
log.banner();
|
|
50
|
+
log.success("assets.config.json", "created");
|
|
51
|
+
console.log();
|
|
52
|
+
log.info("Edit the config, then run \x1b[1mbetter-asset\x1b[0m to generate.");
|
|
53
|
+
console.log();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function main() {
|
|
57
|
+
const args = process.argv.slice(2);
|
|
58
|
+
|
|
59
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
60
|
+
console.log(HELP);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
65
|
+
console.log(await getVersion());
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const cwd = process.cwd();
|
|
70
|
+
|
|
71
|
+
if (args[0] === "init") {
|
|
72
|
+
await initConfig(cwd);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const configFlagIndex = args.indexOf("--config");
|
|
77
|
+
const configFlag = configFlagIndex !== -1 ? args[configFlagIndex + 1] : undefined;
|
|
78
|
+
|
|
79
|
+
await generate(cwd, configFlag);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
main().catch(err => {
|
|
83
|
+
log.error(err.message || err);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "better-asset",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Config-driven image asset generator. Resize, convert, and generate favicons from a single JSON config.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"better-asset": "bin/better-asset.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"assets.config.schema.json",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"image",
|
|
17
|
+
"asset",
|
|
18
|
+
"generator",
|
|
19
|
+
"favicon",
|
|
20
|
+
"ico",
|
|
21
|
+
"icon",
|
|
22
|
+
"sharp",
|
|
23
|
+
"resize",
|
|
24
|
+
"convert",
|
|
25
|
+
"webp",
|
|
26
|
+
"avif",
|
|
27
|
+
"png",
|
|
28
|
+
"cli"
|
|
29
|
+
],
|
|
30
|
+
"author": "Sazzadur Rahman",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"sharp": "^0.34.5"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/clean.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileExists } from "./fs-utils.js";
|
|
4
|
+
|
|
5
|
+
export async function cleanGeneratedFiles(destinationDir, assetName) {
|
|
6
|
+
if (!(await fileExists(destinationDir))) return;
|
|
7
|
+
|
|
8
|
+
const files = await fs.readdir(destinationDir);
|
|
9
|
+
|
|
10
|
+
const patterns = [
|
|
11
|
+
new RegExp(`^${assetName}(-.*)?\\.(png|webp|jpeg|jpg|avif)$`),
|
|
12
|
+
...(assetName === "favicon" ? [/^favicon\.ico$/] : []),
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
await Promise.all(
|
|
16
|
+
files.filter(file => patterns.some(regex => regex.test(file))).map(file => fs.unlink(path.join(destinationDir, file))),
|
|
17
|
+
);
|
|
18
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CONFIG_NAME = "assets.config.json";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the config file path.
|
|
8
|
+
* Priority: explicit path → default file in cwd.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveConfigPath(cwd, configFlag) {
|
|
11
|
+
if (configFlag) {
|
|
12
|
+
return path.isAbsolute(configFlag) ? configFlag : path.join(cwd, configFlag);
|
|
13
|
+
}
|
|
14
|
+
return path.join(cwd, DEFAULT_CONFIG_NAME);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load and validate the external JSON config.
|
|
19
|
+
*/
|
|
20
|
+
export async function loadConfig(configPath) {
|
|
21
|
+
try {
|
|
22
|
+
await fs.access(configPath);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error(`Config file not found: ${configPath}\nRun \`better-asset init\` to create one.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const raw = await fs.readFile(configPath, "utf-8");
|
|
28
|
+
let config;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
config = JSON.parse(raw);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new Error(`Invalid JSON in config file: ${configPath}\n${err.message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!config.source_dir || typeof config.source_dir !== "string") {
|
|
37
|
+
throw new Error(`Config must include a "source_dir" string.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!Array.isArray(config.assets) || config.assets.length === 0) {
|
|
41
|
+
throw new Error(`Config must include a non-empty "assets" array.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const asset of config.assets) {
|
|
45
|
+
for (const field of ["name", "source", "destination"]) {
|
|
46
|
+
if (!asset[field] || typeof asset[field] !== "string") {
|
|
47
|
+
throw new Error(`Each asset must have a "${field}" string. Problem in: ${JSON.stringify(asset)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(asset.sizes) || asset.sizes.length === 0) {
|
|
51
|
+
throw new Error(`Asset "${asset.name}" must have a non-empty "sizes" array.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Default config template for `better-asset init`.
|
|
60
|
+
*/
|
|
61
|
+
export function defaultConfig() {
|
|
62
|
+
return {
|
|
63
|
+
$schema: "https://unpkg.com/better-asset/assets.config.schema.json",
|
|
64
|
+
source_dir: "public/assets",
|
|
65
|
+
assets: [
|
|
66
|
+
{
|
|
67
|
+
name: "icon",
|
|
68
|
+
source: "icon.svg",
|
|
69
|
+
sizes: [48, 96, 144, 512],
|
|
70
|
+
destination: "public/icons",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "favicon",
|
|
74
|
+
source: "favicon.svg",
|
|
75
|
+
sizes: [16, 32, 48],
|
|
76
|
+
destination: "public/icons",
|
|
77
|
+
ico: {
|
|
78
|
+
required: true,
|
|
79
|
+
size: 96,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "opengraph-image",
|
|
84
|
+
source: "icon.svg",
|
|
85
|
+
sizes: ["1200x630"],
|
|
86
|
+
destination: "public/icons",
|
|
87
|
+
format: "jpeg",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
package/src/fs-utils.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
|
|
3
|
+
export async function ensureDir(dir) {
|
|
4
|
+
await fs.mkdir(dir, { recursive: true });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function fileExists(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
await fs.access(filePath);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/generate.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { resolveConfigPath, loadConfig } from "./config.js";
|
|
3
|
+
import { ensureDir, fileExists } from "./fs-utils.js";
|
|
4
|
+
import { parseSize, buildFileName, generateImage, generateIco } from "./image.js";
|
|
5
|
+
import { cleanGeneratedFiles } from "./clean.js";
|
|
6
|
+
import * as log from "./log.js";
|
|
7
|
+
|
|
8
|
+
export async function generate(cwd, configFlag) {
|
|
9
|
+
const configPath = resolveConfigPath(cwd, configFlag);
|
|
10
|
+
|
|
11
|
+
log.banner();
|
|
12
|
+
log.configPath(path.relative(cwd, configPath) || configPath);
|
|
13
|
+
|
|
14
|
+
const config = await loadConfig(configPath);
|
|
15
|
+
const start = performance.now();
|
|
16
|
+
let totalFiles = 0;
|
|
17
|
+
|
|
18
|
+
for (const asset of config.assets) {
|
|
19
|
+
const sourcePath = path.join(cwd, config.source_dir, asset.source);
|
|
20
|
+
const destinationDir = path.join(cwd, asset.destination);
|
|
21
|
+
|
|
22
|
+
if (!(await fileExists(sourcePath))) {
|
|
23
|
+
log.warn(`Skipping ${asset.name} — source not found: ${asset.source}`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
log.groupStart(asset.name, asset.source);
|
|
28
|
+
|
|
29
|
+
await ensureDir(destinationDir);
|
|
30
|
+
await cleanGeneratedFiles(destinationDir, asset.name);
|
|
31
|
+
|
|
32
|
+
const totalSizes = asset.sizes.length;
|
|
33
|
+
const format = asset.format || "png";
|
|
34
|
+
|
|
35
|
+
for (const rawSize of asset.sizes) {
|
|
36
|
+
const { width, height, label } = parseSize(rawSize);
|
|
37
|
+
const fileName = buildFileName(asset.name, label, asset.sizeInName, totalSizes, format);
|
|
38
|
+
const outputPath = path.join(destinationDir, fileName);
|
|
39
|
+
|
|
40
|
+
await generateImage(sourcePath, outputPath, width, height, format);
|
|
41
|
+
|
|
42
|
+
log.success(fileName, `${width}×${height}`);
|
|
43
|
+
totalFiles++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (asset.ico?.required) {
|
|
47
|
+
const icoPath = path.join(destinationDir, "favicon.ico");
|
|
48
|
+
await generateIco(sourcePath, icoPath, asset.ico.size || 64);
|
|
49
|
+
|
|
50
|
+
log.success("favicon.ico", `${asset.ico.size || 64}×${asset.ico.size || 64}`);
|
|
51
|
+
totalFiles++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const elapsed = Math.round(performance.now() - start);
|
|
56
|
+
log.done(totalFiles, elapsed);
|
|
57
|
+
}
|
package/src/image.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export function parseSize(size) {
|
|
5
|
+
if (typeof size === "number") {
|
|
6
|
+
return { width: size, height: size, label: `${size}` };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (typeof size === "string") {
|
|
10
|
+
const [w, h] = size.split("x").map(Number);
|
|
11
|
+
if (!w || !h) throw new Error(`Invalid size format: ${size}`);
|
|
12
|
+
return { width: w, height: h, label: `${w}x${h}` };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
throw new Error(`Unsupported size type: ${size}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildFileName(name, label, sizeInName, totalSizes, format) {
|
|
19
|
+
const includeSize = sizeInName === true || (sizeInName === undefined && totalSizes > 1);
|
|
20
|
+
const ext = format || "png";
|
|
21
|
+
return includeSize ? `${name}-${label}.${ext}` : `${name}.${ext}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function generateImage(inputPath, outputPath, width, height, format) {
|
|
25
|
+
let pipeline = sharp(inputPath).resize(width, height);
|
|
26
|
+
|
|
27
|
+
switch (format) {
|
|
28
|
+
case "webp":
|
|
29
|
+
pipeline = pipeline.webp();
|
|
30
|
+
break;
|
|
31
|
+
case "jpeg":
|
|
32
|
+
case "jpg":
|
|
33
|
+
pipeline = pipeline.jpeg();
|
|
34
|
+
break;
|
|
35
|
+
case "avif":
|
|
36
|
+
pipeline = pipeline.avif();
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
pipeline = pipeline.png();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await pipeline.toFile(outputPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function generateIco(inputPath, outputPath, size) {
|
|
46
|
+
const buffer = await sharp(inputPath).resize(size, size).png().toBuffer();
|
|
47
|
+
await sharp(buffer).toFormat("png").toFile(outputPath);
|
|
48
|
+
}
|
package/src/log.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const RESET = "\x1b[0m";
|
|
2
|
+
const BOLD = "\x1b[1m";
|
|
3
|
+
const DIM = "\x1b[2m";
|
|
4
|
+
const RED = "\x1b[31m";
|
|
5
|
+
const GREEN = "\x1b[32m";
|
|
6
|
+
const YELLOW = "\x1b[33m";
|
|
7
|
+
const BLUE = "\x1b[34m";
|
|
8
|
+
const MAGENTA = "\x1b[35m";
|
|
9
|
+
const CYAN = "\x1b[36m";
|
|
10
|
+
|
|
11
|
+
const CHECKMARK = `${GREEN}✔${RESET}`;
|
|
12
|
+
const CROSS = `${RED}✖${RESET}`;
|
|
13
|
+
const WARN = `${YELLOW}⚠${RESET}`;
|
|
14
|
+
const ARROW = `${DIM}→${RESET}`;
|
|
15
|
+
const DOT = `${DIM}·${RESET}`;
|
|
16
|
+
|
|
17
|
+
export function banner() {
|
|
18
|
+
console.log();
|
|
19
|
+
console.log(` ${BOLD}${MAGENTA}better-asset${RESET} ${DIM}— config-driven image asset generator${RESET}`);
|
|
20
|
+
console.log();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function info(msg) {
|
|
24
|
+
console.log(` ${BLUE}info${RESET} ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function success(file, size) {
|
|
28
|
+
console.log(` ${CHECKMARK} ${file} ${DIM}${size}${RESET}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function warn(msg) {
|
|
32
|
+
console.log(` ${WARN} ${YELLOW}${msg}${RESET}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function error(msg) {
|
|
36
|
+
console.error(` ${CROSS} ${RED}${msg}${RESET}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function groupStart(name, source) {
|
|
40
|
+
console.log();
|
|
41
|
+
console.log(` ${BOLD}${CYAN}${name}${RESET} ${DIM}← ${source}${RESET}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function done(count, elapsed) {
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(` ${CHECKMARK} ${BOLD}${count} file${count === 1 ? "" : "s"}${RESET} generated ${DIM}in ${elapsed}ms${RESET}`);
|
|
47
|
+
console.log();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function configPath(p) {
|
|
51
|
+
console.log(` ${DOT} config ${DIM}${p}${RESET}`);
|
|
52
|
+
}
|