html-minifier-setup 0.0.0-stage → 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.txt +21 -0
- package/README.md +111 -2
- package/cli.js +199 -0
- package/package.json +57 -3
- package/src/catalog.js +158 -0
- package/src/color.js +17 -0
- package/src/equivalence.js +68 -0
- package/src/hazards.js +329 -0
- package/src/index.js +8 -0
- package/src/measure.js +60 -0
- package/src/pages.js +230 -0
- package/src/progress.js +165 -0
- package/src/render.js +396 -0
- package/src/report.js +251 -0
- package/src/select.js +508 -0
- package/src/setup.js +227 -0
package/src/setup.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Offers to save the configuration and to install or update HTML Minifier, each declined by
|
|
2
|
+
// default
|
|
3
|
+
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
export const CONFIG_FILE = 'html-minifier.config.json';
|
|
9
|
+
|
|
10
|
+
/** @typedef {'npm' | 'pnpm' | 'yarn' | 'bun'} PackageManager */
|
|
11
|
+
|
|
12
|
+
/** @type {[string, PackageManager][]} */
|
|
13
|
+
const LOCKFILES = [['pnpm-lock.yaml', 'pnpm'], ['yarn.lock', 'yarn'], ['bun.lock', 'bun'], ['bun.lockb', 'bun'], ['package-lock.json', 'npm']];
|
|
14
|
+
|
|
15
|
+
/** @type {Record<PackageManager, string[]>} */
|
|
16
|
+
export const INSTALL_ARGS = {
|
|
17
|
+
npm: ['install', '--save-dev'],
|
|
18
|
+
pnpm: ['add', '--save-dev'],
|
|
19
|
+
yarn: ['add', '--dev'],
|
|
20
|
+
bun: ['add', '--dev']
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The directory and its ancestors, nearest first
|
|
25
|
+
* @param {string} dir
|
|
26
|
+
*/
|
|
27
|
+
function* upwards(dir) {
|
|
28
|
+
for (let current = path.resolve(dir); ; current = path.dirname(current)) {
|
|
29
|
+
yield current;
|
|
30
|
+
if (path.dirname(current) === current) return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Finds a package where Node would resolve it from the directory
|
|
36
|
+
* @param {string} name
|
|
37
|
+
* @param {string} dir
|
|
38
|
+
* @returns {{dir: string, version: string} | null}
|
|
39
|
+
*/
|
|
40
|
+
export function findPackage(name, dir) {
|
|
41
|
+
for (const current of upwards(dir)) {
|
|
42
|
+
const filePackage = path.join(current, 'node_modules', name, 'package.json');
|
|
43
|
+
if (fs.existsSync(filePackage)) {
|
|
44
|
+
return { dir: path.dirname(filePackage), version: JSON.parse(fs.readFileSync(filePackage, 'utf8')).version };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The nearest directory with a package.json within the repository, the repository, or else the
|
|
52
|
+
* directory itself
|
|
53
|
+
* @param {string} dir
|
|
54
|
+
*/
|
|
55
|
+
function findRoot(dir) {
|
|
56
|
+
for (const current of upwards(dir)) {
|
|
57
|
+
if (fs.existsSync(path.join(current, 'package.json')) || fs.existsSync(path.join(current, '.git'))) return current;
|
|
58
|
+
}
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The project the inputs belong to: that of the first local input, or else the working directory
|
|
64
|
+
* @param {string[]} inputs
|
|
65
|
+
* @param {string} cwd
|
|
66
|
+
*/
|
|
67
|
+
export function projectOf(inputs, cwd) {
|
|
68
|
+
const input = inputs.find((item) => !/^https?:\/\//i.test(item));
|
|
69
|
+
if (input === undefined) return cwd;
|
|
70
|
+
const resolved = path.resolve(cwd, input);
|
|
71
|
+
const dir = fs.existsSync(resolved) && !fs.statSync(resolved).isDirectory() ? path.dirname(resolved) : resolved;
|
|
72
|
+
return findRoot(dir);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The package manager a package.json declares, if known
|
|
77
|
+
* @param {string} dir
|
|
78
|
+
* @returns {PackageManager | null}
|
|
79
|
+
*/
|
|
80
|
+
function declaredPackageManager(dir) {
|
|
81
|
+
try {
|
|
82
|
+
const name = String(JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).packageManager ?? '').split('@')[0];
|
|
83
|
+
return name && name in INSTALL_ARGS ? /** @type {PackageManager} */ (name) : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The package manager of the nearest project, by lockfile, then by declaration
|
|
91
|
+
* @param {string} dir
|
|
92
|
+
* @returns {PackageManager}
|
|
93
|
+
*/
|
|
94
|
+
export function detectPackageManager(dir) {
|
|
95
|
+
for (const current of upwards(dir)) {
|
|
96
|
+
const found = LOCKFILES.find(([file]) => fs.existsSync(path.join(current, file)));
|
|
97
|
+
if (found) return found[1];
|
|
98
|
+
const declared = declaredPackageManager(current);
|
|
99
|
+
if (declared) return declared;
|
|
100
|
+
}
|
|
101
|
+
return 'npm';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Installs HTML Minifier as development dependency, showing the package manager’s output
|
|
106
|
+
* @param {string} dir
|
|
107
|
+
* @param {PackageManager} manager
|
|
108
|
+
* @returns {Promise<boolean>}
|
|
109
|
+
*/
|
|
110
|
+
export const installPackage = (dir, manager) => new Promise((resolve) => {
|
|
111
|
+
const child = spawn(manager, [...INSTALL_ARGS[manager], 'html-minifier@latest'], { cwd: dir, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
112
|
+
child.on('error', () => resolve(false));
|
|
113
|
+
child.on('close', (code) => resolve(code === 0));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The latest version of a package, or null where the registry can’t tell in time
|
|
118
|
+
* @param {string} name
|
|
119
|
+
* @param {{fetch?: (url: string, init?: RequestInit) => Promise<Response>, registry?: string, timeout?: number}} [settings]
|
|
120
|
+
* @returns {Promise<string | null>}
|
|
121
|
+
*/
|
|
122
|
+
export async function fetchLatestVersion(name, { fetch = globalThis.fetch, registry = process.env.npm_config_registry || 'https://registry.npmjs.org/', timeout = 3000 } = {}) {
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch(`${registry.replace(/\/?$/, '/')}${name}/latest`, { signal: AbortSignal.timeout(timeout) });
|
|
125
|
+
if (!response.ok) return null;
|
|
126
|
+
const { version } = await response.json();
|
|
127
|
+
return typeof version === 'string' ? version : null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @param {string} version */
|
|
134
|
+
const parseVersion = (version) => {
|
|
135
|
+
const [, core = '', prerelease = ''] = /^v?([^+-]*)(?:-([^+]*))?/.exec(version.trim()) ?? [];
|
|
136
|
+
return { core: core.split('.').slice(0, 3).map(Number), prerelease: prerelease ? prerelease.split('.') : [] };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Whether version `a` is newer than `b`, by SemVer precedence
|
|
141
|
+
* @param {string} a
|
|
142
|
+
* @param {string} b
|
|
143
|
+
*/
|
|
144
|
+
export function isNewer(a, b) {
|
|
145
|
+
const [versionA, versionB] = [parseVersion(a), parseVersion(b)];
|
|
146
|
+
for (let index = 0; index < 3; index++) {
|
|
147
|
+
const difference = (versionA.core[index] ?? 0) - (versionB.core[index] ?? 0);
|
|
148
|
+
if (difference) return difference > 0;
|
|
149
|
+
}
|
|
150
|
+
// A release is newer than its prereleases
|
|
151
|
+
if (!versionA.prerelease.length || !versionB.prerelease.length) return !versionA.prerelease.length && versionB.prerelease.length > 0;
|
|
152
|
+
for (let index = 0; ; index++) {
|
|
153
|
+
const [idA, idB] = [versionA.prerelease[index], versionB.prerelease[index]];
|
|
154
|
+
if (idA === undefined || idB === undefined) return idB === undefined && idA !== undefined;
|
|
155
|
+
if (idA === idB) continue;
|
|
156
|
+
const [numericA, numericB] = [/^\d+$/.test(idA), /^\d+$/.test(idB)];
|
|
157
|
+
// Numeric identifiers rank below alphanumeric ones
|
|
158
|
+
if (numericA && numericB) return Number(idA) > Number(idB);
|
|
159
|
+
return numericA === numericB ? idA > idB : numericB;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @param {string} answer */
|
|
164
|
+
export const isYes = (answer) => /^\s*y(?:es)?\s*$/i.test(answer);
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {string} dir
|
|
168
|
+
* @param {string} cwd
|
|
169
|
+
*/
|
|
170
|
+
const describeDir = (dir, cwd) => dir === cwd ? 'current folder' : path.relative(cwd, dir) || '.';
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {{config: Record<string, unknown>, dirProject?: string, configPreset?: {name: string, changes: string[], config: Record<string, unknown>} | null, cwd: string, ask: (question: string) => Promise<string>, install?: (dir: string, manager: PackageManager) => Promise<boolean>, latest?: Promise<string | null> | null, log: (message: string) => void}} settings
|
|
174
|
+
* @returns {Promise<{fileConfig: string | null, installed: boolean}>}
|
|
175
|
+
*/
|
|
176
|
+
export async function offerSetup({ config, dirProject, configPreset = null, cwd, ask, install = installPackage, latest = null, log }) {
|
|
177
|
+
/** @type {string | null} */
|
|
178
|
+
let fileConfig = null;
|
|
179
|
+
let dirTarget = dirProject ?? cwd;
|
|
180
|
+
|
|
181
|
+
if (isYes(await ask('Save configuration file? (y/N) '))) {
|
|
182
|
+
dirTarget = path.resolve(cwd, (await ask(`Folder (${describeDir(dirTarget, cwd)}): `)).trim() || dirTarget);
|
|
183
|
+
const file = path.join(dirTarget, CONFIG_FILE);
|
|
184
|
+
if (!fs.existsSync(file) || isYes(await ask(`Overwrite ${path.relative(cwd, file)}? (y/N) `))) {
|
|
185
|
+
const asPreset = configPreset && isYes(await ask(`Save it as preset \`${configPreset.name}\` with ${configPreset.changes.length} change${configPreset.changes.length === 1 ? '' : 's'} (${configPreset.changes.map((name) => `\`${name}\``).join(', ')})? Same effect, shorter, and follows changes to the preset (y/N) `));
|
|
186
|
+
await fs.promises.mkdir(dirTarget, { recursive: true });
|
|
187
|
+
await fs.promises.writeFile(file, `${JSON.stringify(asPreset ? configPreset.config : config, null, 2)}\n`);
|
|
188
|
+
fileConfig = file;
|
|
189
|
+
log(`Saved ${path.relative(cwd, file)}${dirTarget === cwd ? '' : '—HTML Minifier reads it when run from that folder'}`);
|
|
190
|
+
} else {
|
|
191
|
+
log('Not saved');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
log('');
|
|
196
|
+
const found = findPackage('html-minifier', dirTarget);
|
|
197
|
+
const versionLatest = await latest;
|
|
198
|
+
if (found) {
|
|
199
|
+
// As the project folder above `node_modules`
|
|
200
|
+
const dirInstalled = path.dirname(path.dirname(found.dir));
|
|
201
|
+
const where = dirInstalled === cwd ? '' : ` in ${describeDir(dirInstalled, cwd)}`;
|
|
202
|
+
if (!versionLatest || !isNewer(versionLatest, found.version)) {
|
|
203
|
+
log(`HTML Minifier ${found.version} is installed${where}${versionLatest ? ' (latest)' : ''}`);
|
|
204
|
+
} else if (isYes(await ask(`Update HTML Minifier ${found.version}${where} to ${versionLatest} (latest)? (y/N) `))) {
|
|
205
|
+
if (!await install(dirInstalled, detectPackageManager(dirInstalled))) log('Update failed');
|
|
206
|
+
}
|
|
207
|
+
return { fileConfig, installed: true };
|
|
208
|
+
}
|
|
209
|
+
let installed = false;
|
|
210
|
+
if (isYes(await ask(`Install HTML Minifier${versionLatest ? ` ${versionLatest} (latest)` : ''}? (y/N) `))) {
|
|
211
|
+
const dirDefault = findRoot(dirTarget);
|
|
212
|
+
const dirInstall = path.resolve(cwd, (await ask(`Folder (${describeDir(dirDefault, cwd)}): `)).trim() || dirDefault);
|
|
213
|
+
const filePackage = path.join(dirInstall, 'package.json');
|
|
214
|
+
// Package managers would otherwise install into the nearest project above
|
|
215
|
+
if (!fs.existsSync(filePackage)) {
|
|
216
|
+
if (!isYes(await ask(`Create package.json in ${describeDir(dirInstall, cwd)}? Needed to install there (y/N) `))) {
|
|
217
|
+
log('Not installed—`npx html-minifier` runs HTML Minifier without installing it');
|
|
218
|
+
return { fileConfig, installed };
|
|
219
|
+
}
|
|
220
|
+
await fs.promises.mkdir(dirInstall, { recursive: true });
|
|
221
|
+
await fs.promises.writeFile(filePackage, `${JSON.stringify({ private: true }, null, 2)}\n`);
|
|
222
|
+
}
|
|
223
|
+
installed = await install(dirInstall, detectPackageManager(dirInstall));
|
|
224
|
+
if (!installed) log('Installation failed');
|
|
225
|
+
}
|
|
226
|
+
return { fileConfig, installed };
|
|
227
|
+
}
|