@massimo-cassandro/minimo 1.39.2 → 1.40.1
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/create-favicons/src/default-params.mjs +5 -5
- package/dev-tools/design-tokens-builder/tokens-config-sample.mjs +1 -1
- 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/app/css/fonts.css +2 -2
- 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/_gitignore +1 -0
- package/dev-tools/starter-kit/source_files/root/_sf/controller/UnsplashController.php +2 -1
- package/dev-tools/starter-kit/source_files/root/_sf/symfony-webpack-setup.md +1 -1
- package/dev-tools/starter-kit/source_files/root/_sf/templates/_main-tpl.html.twig +9 -2
- package/dev-tools/starter-kit/source_files/root/_sf/templates/bundles/TwigBundle/Exception/error.html.twig +5 -1
- package/dev-tools/starter-kit/source_files/root/_sf/templates/home.html.twig +17 -0
- package/dev-tools/starter-kit/source_files/root/package-tpl.json +5 -4
- package/dev-tools/starter-kit/source_files/root/webpack-config-modules/custom-props-purgecss-plugin.mjs +30 -6
- package/dev-tools/starter-kit/source_files/root/webpack.config.mjs +140 -58
- package/dev-tools/starter-kit/starter-install.sh +24 -13
- package/dev-tools/starter-kit/webpack-config-upg.md +99 -5
- package/package.json +2 -1
- package/src/components/unsplash-page/unsplash-page.module.css +6 -5
- package/src/minimo.css +3 -6
- 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
|
@@ -15,7 +15,7 @@ export const default_params = {
|
|
|
15
15
|
|
|
16
16
|
// directory output (percorso relativo alla dir di lavoro)
|
|
17
17
|
// se la dir non esiste, viene creata
|
|
18
|
-
output_dir: './output',
|
|
18
|
+
output_dir: './output', // symfony -> '../../public',
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
// WEBMANIFEST
|
|
@@ -43,12 +43,12 @@ export const default_params = {
|
|
|
43
43
|
// nome del file snippet, compresa l'estensione
|
|
44
44
|
// se questo parametro è null e anche `snippet_target_file` lo è, lo snippet non viene generato
|
|
45
45
|
// se il linguaggio è ejs il nome è sempre 'favicons.incl.ejs' e snippet_name viene ignorato
|
|
46
|
-
snippet_name: 'favicons.html',
|
|
46
|
+
snippet_name: 'favicons.html', // symfony -> 'favicons.incl.html.twig',
|
|
47
47
|
|
|
48
48
|
// path (relativo a questo file) in cui salvare lo snippet
|
|
49
49
|
// se null, viene utilizzato lo stesso percorso indicato in `output_dir`
|
|
50
50
|
// se la dir non esiste, viene creata
|
|
51
|
-
snippet_path: null,
|
|
51
|
+
snippet_path: null, // symfony -> '../../templates/_incl',
|
|
52
52
|
|
|
53
53
|
// path del file in cui scrivere le righe dello snippet
|
|
54
54
|
// questa impostazione è alternativa alle due precedenti e, se presente, queste
|
|
@@ -62,7 +62,7 @@ export const default_params = {
|
|
|
62
62
|
|
|
63
63
|
// linguaggio da utilizzare per la sintassi dello snippet html
|
|
64
64
|
// html, pug, ejs
|
|
65
|
-
snippet_language: 'ejs',
|
|
65
|
+
snippet_language: 'ejs', // symfony: 'html',
|
|
66
66
|
|
|
67
67
|
// template per la costruzione dello snippet.
|
|
68
68
|
// si tratta di una stringa (anche su più righe) in cui deve essere presente
|
|
@@ -78,7 +78,7 @@ export const default_params = {
|
|
|
78
78
|
|
|
79
79
|
// se true aggiunge allo snippet una query string per ovviare ad eventuali
|
|
80
80
|
// problemi di caching del browser
|
|
81
|
-
add_cache_buster: false,
|
|
81
|
+
add_cache_buster: false, // symfony: true,
|
|
82
82
|
|
|
83
83
|
//****************************************************************************
|
|
84
84
|
|
|
@@ -114,7 +114,7 @@ const config = {
|
|
|
114
114
|
// your project tokens — e.g. remaps a Figma/Penpot-exported `primary`
|
|
115
115
|
// color to minimo's own `primary.100` naming
|
|
116
116
|
{
|
|
117
|
-
src: './
|
|
117
|
+
src: './design-tokens/*.{js,mjs,jsonc,json}',
|
|
118
118
|
prefix: 'my-project',
|
|
119
119
|
transform: { primary: 'primary.100' },
|
|
120
120
|
}
|
|
@@ -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
|
|
|
@@ -22,7 +22,8 @@ class UnsplashController extends AbstractController
|
|
|
22
22
|
$orientation = 'landscape';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
$unsplash_access_key = 'xxxxxxx';
|
|
25
|
+
$unsplash_access_key = 'xxxxxxx'; // vedi minimo/_private/unsplash-access-keys.md
|
|
26
|
+
|
|
26
27
|
$collections_ids = '3660951'; // comma separated list
|
|
27
28
|
$count = null;
|
|
28
29
|
|
|
@@ -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
|
|
@@ -4,17 +4,22 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
6
6
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
|
7
|
-
<title>{%- block title -%}{%- endblock %} |
|
|
7
|
+
<title>{%- block title -%}{%- endblock %} | xxxxx</title>
|
|
8
8
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
9
9
|
{%- include '_incl/favicons.incl.html.twig'-%}
|
|
10
|
+
|
|
11
|
+
<link rel="preload" href="{{ asset('fonts/xxxxx.woff2') }}" as="font" type="font/woff2" crossorigin>
|
|
12
|
+
|
|
10
13
|
{{- '' -}}
|
|
14
|
+
{#- shared.js, runtime.js e shared.css servono solo con `useSharedChunk: true` in webpack.config.mjs:
|
|
15
|
+
con `false` rimuovere tutte le righe che li referenziano (qui sotto e a fine body) -#}
|
|
11
16
|
<link rel="preload" href="{{ asset('index.js') }}" as="script">
|
|
12
17
|
<link rel="preload" href="{{ asset('shared.js') }}" as="script">
|
|
13
18
|
<link rel="preload" href="{{ asset('runtime.js') }}" as="script">
|
|
14
19
|
{{- '' -}}
|
|
15
20
|
<link rel="preload" href="{{ asset('shared.css') }}" as="style">
|
|
16
21
|
<link rel="preload" href="{{ asset('index.css') }}" as="style">
|
|
17
|
-
{# shared.css va
|
|
22
|
+
{# shared.css va inserito PRIMA del css della entry (ordine della cascata) #}
|
|
18
23
|
<link rel="stylesheet" href="{{ asset('shared.css') }}" type="text/css" media="all">
|
|
19
24
|
<link rel="stylesheet" href="{{ asset('index.css') }}" type="text/css" media="all">
|
|
20
25
|
<style>
|
|
@@ -24,6 +29,8 @@
|
|
|
24
29
|
</head>
|
|
25
30
|
<body>
|
|
26
31
|
|
|
32
|
+
{%- block body -%}{%- endblock -%}
|
|
33
|
+
|
|
27
34
|
<script src="{{ asset('runtime.js') }}"></script>
|
|
28
35
|
<script src="{{ asset('shared.js') }}"></script>
|
|
29
36
|
<script src="{{ asset('index.js') }}"></script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{#- https://symfony.com/doc/current/controller/error_pages.html#testing-error-pages-during-development -#}
|
|
2
2
|
<!DOCTYPE html>
|
|
3
|
-
<html lang="it" data-err-code="{{ status_code }}" data-err-text="{{ status_text}}">
|
|
3
|
+
<html lang="it" class="uspg" data-err-code="{{ status_code }}" data-err-text="{{ status_text}}">
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="utf-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
<title>Errore {{ status_code }}</title>
|
|
9
9
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
10
10
|
{%- include '_incl/favicons.incl.html.twig'-%}
|
|
11
|
+
{#- shared.css, shared.js e runtime.js servono solo con `useSharedChunk: true` in webpack.config.mjs:
|
|
12
|
+
con `false` rimuovere tutte le righe che li referenziano -#}
|
|
13
|
+
<link rel="preload" href="{{ asset('shared.css') }}" as="style">
|
|
14
|
+
<link rel="stylesheet" href="{{ asset('shared.css') }}" media="all">
|
|
11
15
|
<link rel="preload" href="{{ asset('index.css') }}" as="style">
|
|
12
16
|
<link rel="stylesheet" href="{{ asset('index.css') }}" media="all">
|
|
13
17
|
<link rel="preload" href="{{ asset('error-pages.css') }}" as="style">
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{% extends '_main-tpl.html.twig' %}
|
|
2
|
+
|
|
3
|
+
{% block title %}xxxx{% endblock %}
|
|
4
|
+
{% block head %}
|
|
5
|
+
|
|
6
|
+
<link rel="preload" href="{{ asset('__page__.css') }}" as="style">
|
|
7
|
+
<link rel="stylesheet" href="{{ asset('__page__.css') }}" media="all">
|
|
8
|
+
<link rel="preload" href="{{ asset('__page__.js') }}" as="script">
|
|
9
|
+
{% endblock %}
|
|
10
|
+
{% block js %}
|
|
11
|
+
<script src="{{ asset('__page__.js') }}"></script>
|
|
12
|
+
{% endblock %}
|
|
13
|
+
|
|
14
|
+
{% block body %}
|
|
15
|
+
home
|
|
16
|
+
<div id="root"><div class="spinner"></div></div>
|
|
17
|
+
{% endblock %}
|