@massimo-cassandro/minimo 1.39.2 → 1.40.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/dev-tools/find-orphan-css-classes/find-orphan-css-classes.mjs +451 -0
- package/dev-tools/find-orphan-css-classes/orphan-css-classes.config.mjs +44 -0
- package/dev-tools/starter-kit/source_files/root/__project__.code-workspace +4 -0
- package/dev-tools/starter-kit/source_files/root/_editorconfig +10 -1
- package/dev-tools/starter-kit/source_files/root/_sf/symfony-webpack-setup.md +1 -1
- package/dev-tools/starter-kit/source_files/root/package-tpl.json +4 -3
- package/dev-tools/starter-kit/starter-install.sh +10 -9
- package/package.json +2 -1
- package/src/minimo.css +1 -4
- package/types/classes.d.ts +0 -525
- package/dev-tools/starter-kit/source_files/app/api/.gitkeep +0 -0
- package/dev-tools/starter-kit/source_files/app/src/components/.gitkeep +0 -0
- package/dev-tools/starter-kit/source_files/app/src/js/.gitkeep +0 -0
- package/dev-tools/starter-kit/source_files/app/src/pagine/.gitkeep +0 -0
- package/dev-tools/starter-kit/source_files/app/src/web-components/.gitkeep +0 -0
- package/src/css/margin-padding-bootstrap-media.css +0 -1845
- package/src/css/margin-padding-bootstrap.css +0 -465
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* find-orphan-css-classes.mjs
|
|
6
|
+
*
|
|
7
|
+
* Analizza i file sorgente (html, js, php, ecc.) ed elenca le classi CSS
|
|
8
|
+
* utilizzate che non sono definite nei file CSS indicati.
|
|
9
|
+
* Per i file js/jsx controlla anche le classi dei CSS modules
|
|
10
|
+
* (styles.xxx, ${styles.xxx}, styles['xxx']), verificandole rispetto al
|
|
11
|
+
* file .module.css importato.
|
|
12
|
+
*
|
|
13
|
+
* Nessuna dipendenza esterna, richiede Node >= 18.
|
|
14
|
+
*
|
|
15
|
+
* Il report viene scritto nel file `orphan-classes.md`, nella stessa directory
|
|
16
|
+
* del file di configurazione, con link cliccabili alle occorrenze. Se non ci
|
|
17
|
+
* sono classi mancanti il file viene eliminato.
|
|
18
|
+
*
|
|
19
|
+
* Uso:
|
|
20
|
+
* node find-orphan-css-classes.mjs --config orphan-css-classes.config.mjs
|
|
21
|
+
* node find-orphan-css-classes.mjs -c orphan-css-classes.config.mjs --json
|
|
22
|
+
*
|
|
23
|
+
* Opzioni:
|
|
24
|
+
* -c, --config file di configurazione JavaScript (.mjs consigliato, con export default)
|
|
25
|
+
* --json output JSON su stdout (nessun file scritto)
|
|
26
|
+
* -h, --help mostra questo messaggio
|
|
27
|
+
*
|
|
28
|
+
* Configurazione (i percorsi relativi sono risolti rispetto alla directory
|
|
29
|
+
* del file di configurazione):
|
|
30
|
+
* src (obbligatorio) file o directory da analizzare
|
|
31
|
+
* css (obbligatorio) file o directory contenenti i CSS
|
|
32
|
+
* ext estensioni dei sorgenti
|
|
33
|
+
* cssExt estensioni dei file CSS
|
|
34
|
+
* excludeDir nomi di directory da saltare (default: node_modules, .git, dist, vendor)
|
|
35
|
+
* excludeCssFiles percorsi di file CSS (o CSS modules) da non considerare
|
|
36
|
+
* nel confronto (default: [])
|
|
37
|
+
* ignore espressioni regolari (RegExp o stringhe) di classi da ignorare
|
|
38
|
+
* cssModules {
|
|
39
|
+
* enabled attiva il controllo dei CSS modules (default: true)
|
|
40
|
+
* ext estensioni dei sorgenti in cui cercare i CSS modules (default: js, jsx)
|
|
41
|
+
* pattern espressione regolare (RegExp o stringa) che identifica i file module
|
|
42
|
+
* camelCase se true, styles.myClass corrisponde anche a .my-class
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* Codice di uscita: 0 = nessuna classe mancante, 1 = trovate classi mancanti,
|
|
46
|
+
* 2 = errore di configurazione.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import fs from 'node:fs';
|
|
50
|
+
import path from 'node:path';
|
|
51
|
+
import { pathToFileURL } from 'node:url';
|
|
52
|
+
import { parseArgs } from 'node:util';
|
|
53
|
+
|
|
54
|
+
const DEFAULTS = {
|
|
55
|
+
ext: ['html', 'htm', 'js', 'mjs', 'jsx', 'ts', 'tsx', 'php', 'vue', 'twig'],
|
|
56
|
+
cssExt: ['css'],
|
|
57
|
+
excludeDir: ['node_modules', '.git', 'dist', 'vendor'],
|
|
58
|
+
excludeCssFiles: [],
|
|
59
|
+
ignore: [],
|
|
60
|
+
cssModules: {
|
|
61
|
+
enabled: true,
|
|
62
|
+
ext: ['js', 'jsx'],
|
|
63
|
+
pattern: '\\.module\\.(css|scss|sass|less)$',
|
|
64
|
+
camelCase: false
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const REPORT_FILE = 'orphan-classes.md';
|
|
69
|
+
|
|
70
|
+
const DYN = '\u0000'; /* segnaposto per le parti dinamiche (PHP, template literal, ecc.) */
|
|
71
|
+
|
|
72
|
+
/* classe CSS in un selettore: .nome, con eventuali caratteri di escape */
|
|
73
|
+
const CSS_CLASS_RE = /\.(-?(?:[_a-zA-Z]|\\.)(?:[\w-]|\\.)*)/g;
|
|
74
|
+
|
|
75
|
+
/* class="..." / className='...' / class: `...` */
|
|
76
|
+
const CLASS_ATTR_RE = /\b(?:class|className)\s*[=:]\s*(["'`])((?:(?!\1)[\s\S])*?)\1/g;
|
|
77
|
+
|
|
78
|
+
/* el.classList.add('a', 'b') ecc. */
|
|
79
|
+
const CLASSLIST_RE = /\.classList\.(?:add|remove|toggle|contains|replace)\(([^)]*)\)/g;
|
|
80
|
+
const STRING_RE = /(["'`])((?:(?!\1).)*?)\1/g;
|
|
81
|
+
|
|
82
|
+
/* querySelector('.a .b'), closest('.a'), matches('.a') */
|
|
83
|
+
const SELECTOR_RE = /\b(querySelectorAll|querySelector|closest|matches)\(\s*(["'`])((?:(?!\2).)*?)\2/g;
|
|
84
|
+
|
|
85
|
+
/* getElementsByClassName('a b') */
|
|
86
|
+
const BY_CLASS_RE = /\bgetElementsByClassName\(\s*(["'`])((?:(?!\1).)*?)\1/g;
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* Import di un CSS module:
|
|
90
|
+
* import styles from './x.module.css'
|
|
91
|
+
* import * as styles from './x.module.css'
|
|
92
|
+
* import styles, { foo } from './x.module.css'
|
|
93
|
+
* const styles = require('./x.module.css')
|
|
94
|
+
*/
|
|
95
|
+
const IMPORT_RE = /\bimport\s+(?:\*\s+as\s+)?([A-Za-z_$][\w$]*)\s*(?:,\s*\{[^}]*\})?\s*from\s*(["'])([^"']+)\2/g;
|
|
96
|
+
const REQUIRE_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*(["'])([^"']+)\2\s*\)/g;
|
|
97
|
+
|
|
98
|
+
/* ---------- utilità ---------- */
|
|
99
|
+
|
|
100
|
+
function unescapeCss(name) {
|
|
101
|
+
return name.replace(/\\(.)/g, '$1');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function escapeRegExp(s) {
|
|
105
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function camelize(s) {
|
|
109
|
+
return s.replace(/[-_]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function maskDynamic(value) {
|
|
113
|
+
return value
|
|
114
|
+
.replace(/<\?[\s\S]*?\?>/g, DYN)
|
|
115
|
+
.replace(/\{\{[\s\S]*?\}\}/g, DYN)
|
|
116
|
+
.replace(/\{%[\s\S]*?%\}/g, DYN)
|
|
117
|
+
.replace(/\$\{[^}]*\}/g, DYN);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
* Divide un valore di attributo in singole classi.
|
|
122
|
+
* I token che contengono parti dinamiche (es. "btn-<?= $x ?>") vengono
|
|
123
|
+
* scartati, perché il nome reale della classe non è noto.
|
|
124
|
+
*/
|
|
125
|
+
function splitTokens(value) {
|
|
126
|
+
return maskDynamic(value)
|
|
127
|
+
.split(/\s+/)
|
|
128
|
+
.filter(t => t && !t.includes(DYN) && !/[<>"'`]/.test(t));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeExt(list) {
|
|
132
|
+
return new Set(list.map(e => e.replace(/^\./, '').toLowerCase()));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function collectFiles(entries, extensions, exclude) {
|
|
136
|
+
const out = new Set();
|
|
137
|
+
|
|
138
|
+
const visit = (p, explicit) => {
|
|
139
|
+
let stat;
|
|
140
|
+
try {
|
|
141
|
+
stat = fs.statSync(p);
|
|
142
|
+
} catch {
|
|
143
|
+
console.error(`Percorso non trovato: ${p}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (stat.isDirectory()) {
|
|
148
|
+
for (const entry of fs.readdirSync(p, { withFileTypes: true })) {
|
|
149
|
+
if (exclude.has(entry.name)) continue;
|
|
150
|
+
visit(path.join(p, entry.name), false);
|
|
151
|
+
}
|
|
152
|
+
} else if (explicit || extensions.has(path.extname(p).slice(1).toLowerCase())) {
|
|
153
|
+
out.add(path.resolve(p));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
entries.forEach(p => visit(p, true));
|
|
158
|
+
return [...out];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildLineIndex(text) {
|
|
162
|
+
const idx = [0];
|
|
163
|
+
for (let i = 0; i < text.length; i++) {
|
|
164
|
+
if (text.charCodeAt(i) === 10) idx.push(i + 1);
|
|
165
|
+
}
|
|
166
|
+
return idx;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function lineAt(idx, pos) {
|
|
170
|
+
let lo = 0;
|
|
171
|
+
let hi = idx.length - 1;
|
|
172
|
+
while (lo < hi) {
|
|
173
|
+
const mid = (lo + hi + 1) >> 1;
|
|
174
|
+
if (idx[mid] <= pos) lo = mid;
|
|
175
|
+
else hi = mid - 1;
|
|
176
|
+
}
|
|
177
|
+
return lo + 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ---------- configurazione ---------- */
|
|
181
|
+
|
|
182
|
+
async function loadConfig(configPath) {
|
|
183
|
+
const abs = path.resolve(configPath);
|
|
184
|
+
const mod = await import(pathToFileURL(abs).href);
|
|
185
|
+
const raw = mod.default ?? mod;
|
|
186
|
+
|
|
187
|
+
for (const key of ['src', 'css']) {
|
|
188
|
+
if (!Array.isArray(raw[key]) || !raw[key].length) {
|
|
189
|
+
throw new Error(`Nel file di configurazione manca la lista "${key}"`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const baseDir = path.dirname(abs);
|
|
194
|
+
const toAbs = list => list.map(p => path.resolve(baseDir, p));
|
|
195
|
+
const modules = { ...DEFAULTS.cssModules, ...(raw.cssModules ?? {}) };
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
src: toAbs(raw.src),
|
|
199
|
+
css: toAbs(raw.css),
|
|
200
|
+
ext: normalizeExt(raw.ext ?? DEFAULTS.ext),
|
|
201
|
+
cssExt: normalizeExt(raw.cssExt ?? DEFAULTS.cssExt),
|
|
202
|
+
excludeDir: new Set(raw.excludeDir ?? DEFAULTS.excludeDir),
|
|
203
|
+
excludeCssFiles: new Set(toAbs(raw.excludeCssFiles ?? DEFAULTS.excludeCssFiles)),
|
|
204
|
+
baseDir,
|
|
205
|
+
ignore: (raw.ignore ?? DEFAULTS.ignore).map(s => new RegExp(s)),
|
|
206
|
+
cssModules: {
|
|
207
|
+
enabled: Boolean(modules.enabled),
|
|
208
|
+
ext: normalizeExt(modules.ext),
|
|
209
|
+
pattern: new RegExp(modules.pattern),
|
|
210
|
+
camelCase: Boolean(modules.camelCase)
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* ---------- estrazione classi dai CSS ---------- */
|
|
216
|
+
|
|
217
|
+
function extractCssClasses(css) {
|
|
218
|
+
const clean = css
|
|
219
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') /* commenti */
|
|
220
|
+
.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""') /* stringhe */
|
|
221
|
+
.replace(/url\([^)]*\)/gi, 'url()');
|
|
222
|
+
|
|
223
|
+
const classes = new Set();
|
|
224
|
+
const preludeRe = /([^{}]+)\{/g; /* testo che precede ogni "{" */
|
|
225
|
+
let m;
|
|
226
|
+
|
|
227
|
+
while ((m = preludeRe.exec(clean))) {
|
|
228
|
+
/* con il CSS nesting il testo può includere dichiarazioni precedenti */
|
|
229
|
+
const prelude = m[1].slice(m[1].lastIndexOf(';') + 1).trim();
|
|
230
|
+
if (!prelude || prelude.startsWith('@')) continue; /* at-rule: @media, @font-face, ecc. */
|
|
231
|
+
|
|
232
|
+
for (const c of prelude.matchAll(CSS_CLASS_RE)) {
|
|
233
|
+
classes.add(unescapeCss(c[1]));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return classes;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ---------- estrazione classi dai sorgenti ---------- */
|
|
241
|
+
|
|
242
|
+
function* extractUsages(text) {
|
|
243
|
+
for (const m of text.matchAll(CLASS_ATTR_RE)) {
|
|
244
|
+
for (const cls of splitTokens(m[2])) yield { cls, index: m.index };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const m of text.matchAll(CLASSLIST_RE)) {
|
|
248
|
+
for (const lit of m[1].matchAll(STRING_RE)) {
|
|
249
|
+
for (const cls of splitTokens(lit[2])) yield { cls, index: m.index };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const m of text.matchAll(BY_CLASS_RE)) {
|
|
254
|
+
for (const cls of splitTokens(m[2])) yield { cls, index: m.index };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const m of text.matchAll(SELECTOR_RE)) {
|
|
258
|
+
const selector = maskDynamic(m[3]);
|
|
259
|
+
for (const c of selector.matchAll(CSS_CLASS_RE)) {
|
|
260
|
+
/* se la classe è seguita da una parte dinamica il nome è incompleto */
|
|
261
|
+
if (selector[c.index + c[0].length] === DYN) continue;
|
|
262
|
+
yield { cls: unescapeCss(c[1]), index: m.index };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/*
|
|
268
|
+
* Classi dei CSS modules: individua le variabili a cui è assegnato un file
|
|
269
|
+
* module (qualunque nome, non solo "styles") e cerca nome.xxx / nome['xxx'].
|
|
270
|
+
* Il caso ${styles.xxx} è coperto da nome.xxx.
|
|
271
|
+
* "module" è il percorso assoluto del file importato, o null se non risolvibile
|
|
272
|
+
* (es. alias di webpack).
|
|
273
|
+
*/
|
|
274
|
+
function* extractModuleUsages(text, file, pattern) {
|
|
275
|
+
const bindings = new Map();
|
|
276
|
+
|
|
277
|
+
for (const re of [IMPORT_RE, REQUIRE_RE]) {
|
|
278
|
+
for (const m of text.matchAll(re)) {
|
|
279
|
+
const name = m[1];
|
|
280
|
+
const spec = m[3];
|
|
281
|
+
if (!pattern.test(spec)) continue;
|
|
282
|
+
|
|
283
|
+
let resolved = null;
|
|
284
|
+
if (spec.startsWith('.')) {
|
|
285
|
+
const candidate = path.resolve(path.dirname(file), spec);
|
|
286
|
+
if (fs.existsSync(candidate)) resolved = candidate;
|
|
287
|
+
}
|
|
288
|
+
bindings.set(name, resolved);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (const [name, resolved] of bindings) {
|
|
293
|
+
const id = escapeRegExp(name);
|
|
294
|
+
const dotRe = new RegExp(`(?<![\\w$.])${id}\\.([A-Za-z_$][\\w$]*)`, 'g');
|
|
295
|
+
const bracketRe = new RegExp(`(?<![\\w$.])${id}\\[\\s*(["'])([^"']+)\\1\\s*\\]`, 'g');
|
|
296
|
+
|
|
297
|
+
for (const m of text.matchAll(dotRe)) {
|
|
298
|
+
yield { cls: m[1], index: m.index, isModule: true, module: resolved };
|
|
299
|
+
}
|
|
300
|
+
for (const m of text.matchAll(bracketRe)) {
|
|
301
|
+
yield { cls: m[2], index: m.index, isModule: true, module: resolved };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* ---------- main ---------- */
|
|
307
|
+
|
|
308
|
+
const { values } = parseArgs({
|
|
309
|
+
options: {
|
|
310
|
+
config: { type: 'string', short: 'c' },
|
|
311
|
+
json: { type: 'boolean', default: false },
|
|
312
|
+
help: { type: 'boolean', short: 'h', default: false }
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (values.help) {
|
|
317
|
+
const header = fs.readFileSync(new URL(import.meta.url), 'utf8').match(/\/\*[\s\S]*?\*\//)[0];
|
|
318
|
+
console.log(header);
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!values.config) {
|
|
323
|
+
console.error('Specificare il file di configurazione con --config <file> (usare --help per i dettagli).');
|
|
324
|
+
process.exit(2);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let cfg;
|
|
328
|
+
try {
|
|
329
|
+
cfg = await loadConfig(values.config);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.error(`Errore nel file di configurazione: ${err.message}`);
|
|
332
|
+
process.exit(2);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const camel = cfg.cssModules.camelCase;
|
|
336
|
+
|
|
337
|
+
/* con camelCase attivo ogni classe è disponibile anche in forma camelCase */
|
|
338
|
+
function withCamel(set) {
|
|
339
|
+
if (camel) for (const c of [...set]) set.add(camelize(c));
|
|
340
|
+
return set;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const allCssFiles = collectFiles(cfg.css, cfg.cssExt, cfg.excludeDir);
|
|
344
|
+
const cssFiles = allCssFiles.filter(f => !cfg.excludeCssFiles.has(f));
|
|
345
|
+
const srcFiles = collectFiles(cfg.src, cfg.ext, cfg.excludeDir).filter(f => !allCssFiles.includes(f));
|
|
346
|
+
|
|
347
|
+
if (!cssFiles.length) {
|
|
348
|
+
console.error('Nessun file CSS trovato.');
|
|
349
|
+
process.exit(2);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/*
|
|
353
|
+
* I file module presenti nella lista CSS non contribuiscono alle classi
|
|
354
|
+
* "globali" (sono raggiungibili solo tramite import), ma servono come
|
|
355
|
+
* riferimento di riserva quando l'import non è risolvibile.
|
|
356
|
+
*/
|
|
357
|
+
const globalClasses = new Set();
|
|
358
|
+
const allModuleClasses = new Set();
|
|
359
|
+
const moduleCache = new Map();
|
|
360
|
+
|
|
361
|
+
function getModuleClasses(file) {
|
|
362
|
+
if (!moduleCache.has(file)) {
|
|
363
|
+
moduleCache.set(file, withCamel(extractCssClasses(fs.readFileSync(file, 'utf8'))));
|
|
364
|
+
}
|
|
365
|
+
return moduleCache.get(file);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const file of cssFiles) {
|
|
369
|
+
if (cfg.cssModules.enabled && cfg.cssModules.pattern.test(file)) {
|
|
370
|
+
for (const cls of getModuleClasses(file)) allModuleClasses.add(cls);
|
|
371
|
+
} else {
|
|
372
|
+
for (const cls of extractCssClasses(fs.readFileSync(file, 'utf8'))) globalClasses.add(cls);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const fallbackClasses = new Set([...globalClasses, ...allModuleClasses]);
|
|
377
|
+
|
|
378
|
+
function isDefined(usage) {
|
|
379
|
+
/* usages of an excluded CSS module are not checked */
|
|
380
|
+
if (usage.module && cfg.excludeCssFiles.has(usage.module)) return true;
|
|
381
|
+
if (!usage.isModule) return globalClasses.has(usage.cls);
|
|
382
|
+
const set = usage.module ? getModuleClasses(usage.module) : fallbackClasses;
|
|
383
|
+
return set.has(usage.cls);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const missing = new Map(); /* classe -> [{ file, line, module? }] */
|
|
387
|
+
|
|
388
|
+
for (const file of srcFiles) {
|
|
389
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
390
|
+
const lineIndex = buildLineIndex(text);
|
|
391
|
+
const rel = path.relative(cfg.baseDir, file);
|
|
392
|
+
const ext = path.extname(file).slice(1).toLowerCase();
|
|
393
|
+
|
|
394
|
+
const usages = [...extractUsages(text)];
|
|
395
|
+
if (cfg.cssModules.enabled && cfg.cssModules.ext.has(ext)) {
|
|
396
|
+
usages.push(...extractModuleUsages(text, file, cfg.cssModules.pattern));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const usage of usages) {
|
|
400
|
+
if (isDefined(usage) || cfg.ignore.some(re => re.test(usage.cls))) continue;
|
|
401
|
+
|
|
402
|
+
const entry = { file: rel, line: lineAt(lineIndex, usage.index) };
|
|
403
|
+
if (usage.isModule) {
|
|
404
|
+
entry.module = usage.module ? path.relative(cfg.baseDir, usage.module) : 'non risolto';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const list = missing.get(usage.cls) ?? [];
|
|
408
|
+
if (!list.some(e => e.file === entry.file && e.line === entry.line && e.module === entry.module)) {
|
|
409
|
+
list.push(entry);
|
|
410
|
+
}
|
|
411
|
+
missing.set(usage.cls, list);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const sorted = [...missing.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
416
|
+
|
|
417
|
+
if (values.json) {
|
|
418
|
+
console.log(JSON.stringify({
|
|
419
|
+
cssFiles: cssFiles.length,
|
|
420
|
+
sourceFiles: srcFiles.length,
|
|
421
|
+
definedClasses: globalClasses.size,
|
|
422
|
+
missing: Object.fromEntries(sorted)
|
|
423
|
+
}, null, 2));
|
|
424
|
+
} else {
|
|
425
|
+
const reportPath = path.resolve(cfg.baseDir, REPORT_FILE);
|
|
426
|
+
|
|
427
|
+
if (sorted.length) {
|
|
428
|
+
/* flat list sorted by file and line, with a clickable link to each occurrence */
|
|
429
|
+
const items = sorted
|
|
430
|
+
.flatMap(([cls, locations]) => locations.map(loc => ({ cls, ...loc })))
|
|
431
|
+
.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
432
|
+
|
|
433
|
+
const lines = items.map(({ cls, file, line, module }) => {
|
|
434
|
+
const href = `${file.split(path.sep).map(encodeURIComponent).join('/')}#L${line}`;
|
|
435
|
+
return `* [${path.basename(file)}:${line}](${href}) -> \`.${cls}\``
|
|
436
|
+
+ (module ? ` (CSS module: ${module})` : '');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
fs.writeFileSync(reportPath, `## Missing CSS classes\n\n${lines.join('\n')}\n`, 'utf-8');
|
|
440
|
+
} else if (fs.existsSync(reportPath)) {
|
|
441
|
+
fs.unlinkSync(reportPath);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
console.log(
|
|
445
|
+
`File CSS: ${cssFiles.length} (${globalClasses.size} classi globali) | File analizzati: ${srcFiles.length}\n`
|
|
446
|
+
+ `Classi mancanti: ${sorted.length}`
|
|
447
|
+
+ (sorted.length ? ` -> ${path.relative(process.cwd(), reportPath)}` : '')
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
process.exit(sorted.length ? 1 : 0);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Configurazione per find-prphan-css-classes.mjs
|
|
3
|
+
* I percorsi relativi sono risolti rispetto alla directory di questo file.
|
|
4
|
+
*
|
|
5
|
+
* Config Da Copiare Nella Root Del Progetto
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const minimo_path = '../../node_modules/@massimo-cassandro/minimo';
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
/* file o directory da analizzare */
|
|
12
|
+
src: ['../src', '../../templates'],
|
|
13
|
+
|
|
14
|
+
/* file o directory contenenti i CSS */
|
|
15
|
+
css: ['./', '../src', `${minimo_path}/src`],
|
|
16
|
+
|
|
17
|
+
/* estensioni dei sorgenti */
|
|
18
|
+
ext: ['html', 'js', 'jsx', 'php'],
|
|
19
|
+
|
|
20
|
+
/* estensioni dei file CSS */
|
|
21
|
+
cssExt: ['css'],
|
|
22
|
+
|
|
23
|
+
/* directory da saltare */
|
|
24
|
+
excludeDir: [/* 'node_modules', */'.git', 'dist', 'vendor'],
|
|
25
|
+
|
|
26
|
+
/* file CSS (o CSS modules) da non considerare nel confronto (default: []) */
|
|
27
|
+
excludeCssFiles: [],
|
|
28
|
+
|
|
29
|
+
/* classi da ignorare (RegExp o stringhe) */
|
|
30
|
+
ignore: [/^js-/, /^is-/],
|
|
31
|
+
|
|
32
|
+
cssModules: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
|
|
35
|
+
/* sorgenti in cui cercare l'uso dei CSS modules */
|
|
36
|
+
ext: ['js', 'jsx'],
|
|
37
|
+
|
|
38
|
+
/* file considerati CSS modules */
|
|
39
|
+
pattern: /\.module\.css$/,
|
|
40
|
+
|
|
41
|
+
/* se true, styles.myClass corrisponde anche a .my-class */
|
|
42
|
+
camelCase: false
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -11,7 +11,16 @@ indent_style = space
|
|
|
11
11
|
insert_final_newline = true
|
|
12
12
|
trim_trailing_whitespace = true
|
|
13
13
|
|
|
14
|
+
# [*.yaml]
|
|
15
|
+
# indent_size = 4
|
|
16
|
+
|
|
17
|
+
# [*.php]
|
|
18
|
+
# indent_size = 4
|
|
19
|
+
|
|
20
|
+
# [{compose.yaml,compose.*.yaml}]
|
|
21
|
+
# indent_size = 2
|
|
22
|
+
|
|
14
23
|
[*.md]
|
|
15
|
-
max_line_length =
|
|
24
|
+
max_line_length = 80
|
|
16
25
|
trim_trailing_whitespace = false
|
|
17
26
|
|
|
@@ -34,7 +34,7 @@ twig:
|
|
|
34
34
|
file_name_pattern: '*.twig'
|
|
35
35
|
|
|
36
36
|
form_themes:
|
|
37
|
-
- '
|
|
37
|
+
- '_incl/minimo-form-layout.html.twig'
|
|
38
38
|
paths:
|
|
39
39
|
'%kernel.project_dir%/public/%env(ASSETS_DIR)%': assets_path
|
|
40
40
|
'%kernel.project_dir%/public': public_path
|
|
@@ -21,21 +21,22 @@
|
|
|
21
21
|
"php server": "php -S localhost:[[port8000]] # -t root_dir/",
|
|
22
22
|
"webpack DEV (sf)": "rm -rf ./public/_dev && NODE_ENV=development webpack serve --config ./webpack.config.mjs # http://192.168.1.135:[[port5700]",
|
|
23
23
|
"symfony server": "symfony serve --port=[[port8000]] --listen-ip=0.0.0.0 # http://192.168.1.135:[[port8000]] # -d (deamon): rimane aperto anche dopo la chiusura del terminale, terminare con `symfony server:stop`",
|
|
24
|
+
"launchChrome": "open -n -a 'Google Chrome' --args --profile-directory='Profile 3' # mbn: 1 / mg2: 3",
|
|
24
25
|
"webpack PROD": "npm run 'UPD-version' && NODE_ENV=production webpack --config ./webpack.config.mjs --stats minimal",
|
|
26
|
+
"UPD-version": "npx update-version --no-log-v0 --log-file=./changelog.md # --log-patch --pkg=alt/path/to/package.json",
|
|
25
27
|
"email sass": "npx sass ./app/email:./templates/email/css --no-source-map --style=compressed --watch",
|
|
26
28
|
"postcss email": "postcss ./app/email/[!_]*.css --dir ./templates/email/css --no-map --verbose --config ./app/email --watch",
|
|
27
29
|
"eslint-check": "npx eslint app/",
|
|
28
30
|
"stylelint-check": "npx stylelint 'app/**/*.css'",
|
|
29
31
|
"create-favicons INIT": "npx create-favicons init",
|
|
30
32
|
"create-favicons": "npx create-favicons --dir=./app/favicons",
|
|
31
|
-
"UPD-version": "npx update-version --no-log-v0 --log-file=./changelog.md # --log-patch --pkg=alt/path/to/package.json",
|
|
32
|
-
"upd@m": "npx upd@m",
|
|
33
|
-
"launchChrome": "open -n -a 'Google Chrome' --args --profile-directory='Profile 3' # mbn: 1 / mg2: 3",
|
|
34
33
|
"python server": "python3 -m http.server [[port8000]] # --directory __dirname__ # 8000 = default port",
|
|
35
34
|
"node server": "node serve # richiede ‘npm i serve’",
|
|
36
35
|
"build tokens": "npx buildTokens --config ./app/css/tokens-config.mjs",
|
|
37
36
|
"check unresolved props": "npx checkUnresolvedProps --config ./app/css/tokens-config.mjs",
|
|
37
|
+
"find orphan css classes": "npx findOrphanCssClasses --config ./app/css/orphan-css-classes.config.mjs",
|
|
38
38
|
"npm-outdated": "npm outdated",
|
|
39
|
+
"upd@m": "npx upd@m",
|
|
39
40
|
"npm-upd": "npm update --save"
|
|
40
41
|
},
|
|
41
42
|
"allowScripts": {
|
|
@@ -357,14 +357,20 @@ echo -e "\n${GREEN}...creating default folders${NC}"
|
|
|
357
357
|
mkdir -p \
|
|
358
358
|
"_private" \
|
|
359
359
|
"${FRONTEND_INSTALL_PATH}" \
|
|
360
|
+
"${FRONTEND_INSTALL_PATH}/api" \
|
|
360
361
|
"${FRONTEND_INSTALL_PATH}/assets" \
|
|
361
|
-
"${FRONTEND_INSTALL_PATH}/src" \
|
|
362
362
|
"${FRONTEND_INSTALL_PATH}/css" \
|
|
363
|
-
"${FRONTEND_INSTALL_PATH}/
|
|
363
|
+
"${FRONTEND_INSTALL_PATH}/email" \
|
|
364
|
+
"${FRONTEND_INSTALL_PATH}/error-pages" \
|
|
365
|
+
"${FRONTEND_INSTALL_PATH}/favicons" \
|
|
364
366
|
"${FRONTEND_INSTALL_PATH}/icons" \
|
|
365
367
|
"${FRONTEND_INSTALL_PATH}/imgs" \
|
|
366
|
-
"${FRONTEND_INSTALL_PATH}/
|
|
367
|
-
"${FRONTEND_INSTALL_PATH}/
|
|
368
|
+
"${FRONTEND_INSTALL_PATH}/src" \
|
|
369
|
+
"${FRONTEND_INSTALL_PATH}/src/components" \
|
|
370
|
+
"${FRONTEND_INSTALL_PATH}/src/js" \
|
|
371
|
+
"${FRONTEND_INSTALL_PATH}/src/pages" \
|
|
372
|
+
"${FRONTEND_INSTALL_PATH}/src/web-components" \
|
|
373
|
+
"${FRONTEND_INSTALL_PATH}/tpl"
|
|
368
374
|
|
|
369
375
|
|
|
370
376
|
|
|
@@ -425,11 +431,6 @@ force_cat "${MINIMO_PKG_DIR}/src/minimo.css" "${FRONTEND_INSTALL_PATH}/css/index
|
|
|
425
431
|
force_cat "${MINIMO_PKG_DIR}/dev-tools/design-tokens-builder/tokens-config-sample.mjs" "${FRONTEND_INSTALL_PATH}/css/tokens-config.mjs"
|
|
426
432
|
|
|
427
433
|
|
|
428
|
-
|
|
429
|
-
# favicons: config di default per `npx create-favicons`
|
|
430
|
-
force_cat "${MINIMO_PKG_DIR}/dev-tools/create-favicons/src/default-params.mjs" "${FRONTEND_INSTALL_PATH}/favicons/create-favicons-cfg.mjs"
|
|
431
|
-
|
|
432
|
-
|
|
433
434
|
if [ ${#selectedOptionalDevPkgs[@]} -gt 0 ] || [ ${#selectedOptionalDepPkgs[@]} -gt 0 ]; then
|
|
434
435
|
echo -e "\n${YELLOW}Pacchetti opzionali installati:${NC}"
|
|
435
436
|
for pkg in "${selectedOptionalDevPkgs[@]}" "${selectedOptionalDepPkgs[@]}"; do
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massimo-cassandro/minimo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"description": "Light css framework & utilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"bin": {
|
|
23
23
|
"buildTokens": "dev-tools/design-tokens-builder/build-tokens.mjs",
|
|
24
24
|
"checkUnresolvedProps": "dev-tools/design-tokens-builder/check-unresolved-custom-props.mjs",
|
|
25
|
+
"findOrphanCssClasses": "dev-tools/find-orphan-css-classes/find-orphan-css-classes.mjs",
|
|
25
26
|
"update-version": "dev-tools/update-version/index.mjs",
|
|
26
27
|
"upd@m": "dev-tools/upd@m/index.mjs",
|
|
27
28
|
"svgIconsTools": "dev-tools/svg-icons-tools/index.mjs",
|
package/src/minimo.css
CHANGED
|
@@ -41,10 +41,7 @@
|
|
|
41
41
|
@import '@minimoSrc/css/display.css';
|
|
42
42
|
@import '@minimoSrc/css/margin-padding.css';
|
|
43
43
|
/* @import '@minimoSrc/css/margin-padding-media.css'; responsive version */
|
|
44
|
-
|
|
45
|
-
@import '@minimoSrc/css/margin-padding-bootstrap.css';
|
|
46
|
-
@import '@minimoSrc/css/margin-padding-bootsrap-media.css';
|
|
47
|
-
*/
|
|
44
|
+
|
|
48
45
|
@import '@minimoSrc/css/overflow.css';
|
|
49
46
|
@import '@minimoSrc/css/position.css';
|
|
50
47
|
@import '@minimoSrc/css/visibility.css';
|