@massimo-cassandro/minimo 1.15.1 → 1.16.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.
Files changed (30) hide show
  1. package/README.md +3 -3
  2. package/design-tokens/_src/typography.minimo.tokens.mjs +1 -1
  3. package/dev-tools/README.md +28 -2
  4. package/dev-tools/starter-kit/TODO.md +135 -0
  5. package/dev-tools/starter-kit/src/merge-jsonc.mjs +144 -0
  6. package/dev-tools/{webpack-setup → starter-kit}/starter-install.sh +66 -10
  7. package/dev-tools/starter-kit/templates/_root_htaccess +68 -0
  8. package/dev-tools/starter-kit/templates/_root_robots.txt +5 -0
  9. package/dev-tools/starter-kit/templates/entry-tpl.critical.css +18 -0
  10. package/dev-tools/starter-kit/templates/entry-tpl.css +24 -0
  11. package/dev-tools/{webpack-setup → starter-kit}/templates/package-tpl.json +2 -2
  12. package/dev-tools/starter-kit/templates/tokens-config.mjs +33 -0
  13. package/dev-tools/{webpack-setup → starter-kit}/webpack-modules/css-rules.mjs +6 -1
  14. package/dev-tools/{webpack-setup → starter-kit}/webpack-modules/postcss.config.mjs +14 -34
  15. package/dev-tools/starter-kit/webpack-modules/purgecss-variables-safelist.mjs +92 -0
  16. package/dev-tools/{webpack-setup → starter-kit}/webpack.config.mjs +169 -37
  17. package/package.json +3 -2
  18. /package/dev-tools/{webpack-setup → starter-kit}/templates/__project__.code-workspace +0 -0
  19. /package/dev-tools/{webpack-setup → starter-kit}/templates/_browserslistrc +0 -0
  20. /package/dev-tools/{webpack-setup → starter-kit}/templates/_editorconfig +0 -0
  21. /package/dev-tools/{webpack-setup → starter-kit}/templates/_gitignore +0 -0
  22. /package/dev-tools/{webpack-setup → starter-kit}/templates/_prettierrc +0 -0
  23. /package/dev-tools/{webpack-setup → starter-kit}/templates/eslint.config.mjs +0 -0
  24. /package/dev-tools/{webpack-setup → starter-kit}/templates/jsconfig.json +0 -0
  25. /package/dev-tools/{webpack-setup → starter-kit}/templates/stylelint.config.mjs +0 -0
  26. /package/dev-tools/{webpack-setup → starter-kit}/webpack-modules/get-jsConfig-aliases.mjs +0 -0
  27. /package/dev-tools/{webpack-setup → starter-kit}/webpack-modules/mini-svg-data-uri-loader.cjs +0 -0
  28. /package/dev-tools/{webpack-setup → starter-kit}/webpack-modules/svg-rules.mjs +0 -0
  29. /package/dev-tools/{webpack-setup → starter-kit}/webpack-modules/svgo.config.mjs +0 -0
  30. /package/dev-tools/{webpack-setup → starter-kit}/webpack-template.ejs +0 -0
package/README.md CHANGED
@@ -12,13 +12,13 @@ npm i -D @massimo-cassandro/eslint-config
12
12
  npm i -D @massimo-cassandro/stylelint-config
13
13
  ```
14
14
 
15
- > Note: **minimo** is designed to be used with WebPack using the configuration you can find in the [webpack-setup folder](./webpack-setup/) of this repository. If changes are made (for example, to how SVG files are imported), things may not work as expected.
15
+ > Note: **minimo** is designed to be used with WebPack using the configuration you can find in the [starter-kit folder](./dev-tools/starter-kit/) of this repository. If changes are made (for example, to how SVG files are imported), things may not work as expected.
16
16
  >
17
17
 
18
- For a complete install of all required webpack modules, use the install script at `./webpack-setup/starter-install.sh` launching:
18
+ For a complete install of all required webpack modules, use the install script at `./dev-tools/starter-kit/starter-install.sh` launching:
19
19
 
20
20
  ```bash
21
- zsh ./node_modules/@massimo-cassandro/minimo/webpack-setup/starter-install.sh
21
+ zsh ./node_modules/@massimo-cassandro/minimo/dev-tools/starter-kit/starter-install.sh
22
22
  ```
23
23
  ---
24
24
 
@@ -5,7 +5,7 @@ export default {
5
5
  $type: 'fontFamily'
6
6
  },
7
7
  'family-mono': {
8
- $value: "'Monaspace Neon Var', 'Monaspace Neon', 'Aptos Mono', 'Roboto Mono', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Andale Mono', 'Courier New', monospace",
8
+ $value: '\'Monaspace Neon Var\', \'Monaspace Neon\', \'Aptos Mono\', \'Roboto Mono\', \'SFMono-Regular\', \'Menlo\', \'Monaco\', \'Consolas\', \'Liberation Mono\', \'Andale Mono\', \'Courier New\', monospace',
9
9
  $type: 'fontFamily'
10
10
  },
11
11
 
@@ -6,9 +6,35 @@ A collection of standalone Node.js CLI utilities to help manage a web project's
6
6
  * [Svg Icons Tools](./svg-icons-tools/README.md) — optimizes SVG icons and combines/converts them into symbols, JSX, or SCSS variables
7
7
  * [Create favicons](./create-favicons/README.md) — generates the full favicon set (SVG, PNG, ICO, webmanifest, HTML snippet) from a source image
8
8
  * [upd@m](./upd@m/README.md) — updates all `@massimo-cassandro/*` packages installed in `node_modules`
9
- * **webpack-setup** — script for installing minimo environment
9
+ * [starter-kit](#starter-kit) — script for installing minimo environment
10
10
 
11
- ## Usage of SVG adn favicons tools
11
+ ## starter-kit
12
+
13
+ `starter-kit/starter-install.sh` bootstraps a new frontend project: it copies the starter config files (`package.json`, `.gitignore`, `jsconfig.json`, eslint/stylelint configs, webpack config and modules, ...) and installs all the required dev dependencies.
14
+
15
+ The script **requires zsh** (it exits with an error if launched with `sh` or `bash`). Existing files are never overwritten: root config files are copied with a `NEW-` prefix, while `package.json` and the `*.code-workspace` file are merged (template keys are appended at the end, prefixed with `_`).
16
+
17
+ ### If minimo is already installed
18
+
19
+ The script is exposed as a `bin` of the package:
20
+
21
+ ```bash
22
+ npx starter-kit
23
+ ```
24
+
25
+ Since the installation is fully overwrite-safe, the script can also be **re-run at any time to update** an existing setup (e.g. to refresh the webpack modules or reinstall the dev dependencies): existing files are skipped or copied with the `NEW-` prefix, and the `package.json` / `*.code-workspace` merges are idempotent (running the script again neither duplicates nor overwrites anything).
26
+
27
+ ### Without installing minimo
28
+
29
+ To bootstrap a project **before** (or without) installing the whole package, download only the `starter-kit` folder from the npm tarball and run the script from there:
30
+
31
+ ```bash
32
+ curl -sL "$(npm view @massimo-cassandro/minimo dist.tarball)" | tar xz package/dev-tools/starter-kit
33
+ zsh package/dev-tools/starter-kit/starter-install.sh
34
+ rm -rf package
35
+ ```
36
+
37
+ ## SVG adn favicons tools
12
38
 
13
39
  ```bash
14
40
  ## SVG Icons Tools
@@ -0,0 +1,135 @@
1
+ # TODO — Starter kit: config come punto di regia, moduli autonomi
2
+
3
+ ## Obiettivo
4
+
5
+ `webpack.config.mjs` deve diventare **solo** un punto di impostazione di scelte e
6
+ parametri (in piccolo già lo è: `useSass`, `useSvgo`, `purgeCSSDebug`, ecc.).
7
+ Tutta la logica deve migrare nei moduli di `webpack-modules/`, che devono diventare
8
+ **completamente autonomi e indipendenti dalle impostazioni e dal layout del progetto**:
9
+ in questo modo potranno essere aggiornati/sovrascritti in qualsiasi progetto senza
10
+ danni, perché il timone resta sempre nel config del progetto.
11
+
12
+ ## Contratto dei moduli (da rispettare in ogni intervento)
13
+
14
+ 1. **Nessun percorso di progetto hardcodato** nei moduli: tutti i path (sorgenti,
15
+ node_modules, template, ecc.) arrivano come parametri dal config.
16
+ 2. Ogni modulo esporta **factory pure** (funzioni che ricevono opzioni e
17
+ restituiscono rules/plugin/config): nessun side effect a import time.
18
+ 3. Ogni parametro ha un **default sensato** e JSDoc: il modulo deve funzionare
19
+ anche senza opzioni, e un'opzione nuova non deve mai rompere un config vecchio.
20
+ 4. **Niente dipendenze implicite tra moduli**: se un modulo ha bisogno di un altro
21
+ (es. css-rules → postcss config), il collegamento avviene via parametro passato
22
+ dal config, non con import/path interni.
23
+ 5. Aggiornare un modulo non deve mai richiedere modifiche al config esistente,
24
+ se non per adottare volontariamente opzioni nuove.
25
+ 6. **Import opzionali = import dinamici**: i pacchetti usati solo quando una
26
+ scelta li attiva (es. sass-loader con `useSass`, responsive-loader, svgo)
27
+ non vanno importati staticamente: con la nuova struttura ogni import
28
+ condizionato da un flag va fatto con `await import(...)` dentro la factory
29
+ (come già avviene per `svgo.config.mjs` nel config), così un pacchetto non
30
+ installato non rompe il caricamento del config/modulo se la relativa
31
+ funzionalità è disattivata.
32
+
33
+ ## Stato attuale (inventario del coupling)
34
+
35
+ | Modulo | Stato | Problemi |
36
+ |---|---|---|
37
+ | `get-jsConfig-aliases.mjs` | ✅ autonomo | riceve il path del jsconfig dal config |
38
+ | `purgecss-variables-safelist.mjs` | ✅ autonomo | tutti i glob/seeds arrivano dal config |
39
+ | `svgo.config.mjs` | ✅ autonomo | nessun riferimento al progetto |
40
+ | `mini-svg-data-uri-loader.cjs` | ✅ autonomo | loader puro |
41
+ | `svg-rules.mjs` | ⚠️ quasi | parametrico, ma da riverificare rispetto al contratto (default, JSDoc) |
42
+ | `css-rules.mjs` | ⚠️ quasi | parametrico, ma riceve `postcssConfig_path` (file di config esterno invece di opzioni); scelte come `localIdentName` non sovrascrivibili dal config |
43
+ | `postcss.config.mjs` | ❌ accoppiato | **hardcoda** `../src/css/custom-media.css` (layout del progetto): non aggiornabile senza danni |
44
+ | `webpack.config.mjs` | ❌ contiene logica | blocchi che sono logica, non scelte: sezione PurgeCSS (opzioni comuni + 2 istanze + log debug), test del cacheGroup `shared`, rules asset (js/ejs/html/img/font/video/favicon), BannerPlugin |
45
+
46
+ ## Piano d'azione
47
+
48
+ ### 1. Oggetto `settings` unico nel config
49
+ Raggruppare in testa al config un unico oggetto di scelte e percorsi
50
+ (flag attuali + tutti i path oggi sparsi: sorgenti js/css, template twig/php,
51
+ node_modules di minimo, glob shadow-DOM, seeds, favicons, output...).
52
+ Le factory dei moduli ricevono ciò che serve da qui. Il config diventa:
53
+ `settings` → chiamate alle factory → oggetto `config`.
54
+
55
+ ### 2. `postcss.config.mjs` → factory senza path hardcodati
56
+ - Trasformarlo in una factory (es. `postcssPlugins({ globalDataFiles, preserveCustomMedia })`)
57
+ che riceve i file per `postcss-global-data` dal config.
58
+ - `css-rules.mjs` non riceve più `postcssConfig_path` (file caricato da
59
+ postcss-loader) ma direttamente l'oggetto `postcssOptions` (o la factory + le sue
60
+ opzioni) costruito nel config: sparisce l'ultimo path relativo al layout progetto.
61
+ - Attenzione a preservare il comportamento attuale di `loaderContext.mode`.
62
+ - **DA CONTROLLARE E VALUTARE — eliminazione del file postcss config**: verificare
63
+ la possibilità di eliminare del tutto il file di config postcss e sostituirlo
64
+ con regole definite direttamente nel webpack config (plugin postcss dichiarati
65
+ inline in `postcssOptions`). Considerare però che in certi casi postcss
66
+ necessita di impostazioni ad hoc per progetto (è il motivo per cui oggi esiste
67
+ il parametro `postcssConfig_path`): in quel caso la strada è mantenere la
68
+ possibilità di override — es. default inline nel config + parametro opzionale
69
+ per un file/oggetto di impostazioni custom. Tenere conto anche degli altri
70
+ consumatori del file: un postcss config a livello di progetto può servire ad
71
+ altri tool (stylelint, editor/IDE, CLI postcss) oltre che a postcss-loader.
72
+
73
+ ### 3. Sezione PurgeCSS → `webpack-modules/purgecss-plugins.mjs`
74
+ Factory (es. `purgecssPlugins({ contentGlobs, safelist, variablesSafelistOptions, debug })`)
75
+ che incapsula: opzioni comuni, safelist base (deep `m_`, greedy `&`), le due istanze
76
+ (critical + generale) e il mini-plugin di log `purgedStats`.
77
+ Restituisce l'array di plugin pronto per lo spread nel config.
78
+ Nel config restano SOLO: i glob dei contenuti, le safelist specifiche del progetto
79
+ (standard/seeds), il flag `purgeCSSDebug`. I commenti "strutturali" (problemi a/b/c)
80
+ migrano nel modulo; nel config restano i commenti sulle scelte.
81
+
82
+ ### 4. Rules asset generiche → `webpack-modules/asset-rules.mjs`
83
+ Estrarre js/babel, ejs, html, immagini/pdf, font, video, favicons in una factory
84
+ parametrica con on/off per gruppo (es. `assetRules({ favicons_path_regexp, useVideo, ... })`).
85
+ Le parti oggi commentate nel config (responsive-loader, video) diventano opzioni
86
+ documentate del modulo.
87
+
88
+ ### 5. splitChunks
89
+ Valutare una piccola factory (es. `sharedCacheGroup({ pathsRegexp })`) che incapsuli
90
+ l'esclusione dei moduli css (`module.type !== 'css/mini-extract'`, requisito PurgeCSS)
91
+ lasciando nel config solo l'elenco dei path. In alternativa lasciarla nel config ma
92
+ con l'esclusione css delegata a una helper del modulo purgecss (è un suo requisito).
93
+
94
+ ### 6. `starter-install.sh`
95
+ - Aggiornare l'array `FILES` con i nuovi moduli.
96
+ - Distinguere due categorie nel flusso di copia:
97
+ - `webpack-modules/` = aggiornabili sempre (una volta autonomi, la copia
98
+ NEW- può diventare sovrascrittura diretta o diff);
99
+ - `webpack.config.mjs` e template = mai sovrascritti (proprietà del progetto).
100
+ - **Audit delle dipendenze installate**: verificare la corrispondenza tra i
101
+ pacchetti degli `npm i -D` dello script e quelli effettivamente usati da
102
+ config, moduli e template, per evitare installazioni inutili. Da controllare
103
+ in entrambe le direzioni:
104
+ - installati ma (forse) mai usati: es. `postcss-preset-env` (assente dal
105
+ postcss config), `responsive-loader` (solo in blocchi commentati),
106
+ `style-dictionary` (riguarda i design token, non la build webpack);
107
+ - usati ma non installati: es. `file-loader` (referenziato dalla rule video
108
+ del config) e `sass-loader`/`sass` (necessari con `useSass: true`).
109
+ - i pacchetti legati a funzionalità opzionali (sass, responsive-loader,
110
+ video, svgr...) escono dall'installazione di default: o vengono installati
111
+ su richiesta dallo script (prompt/flag) o documentati come "da installare
112
+ quando si attiva l'opzione X" — coerentemente con la regola degli import
113
+ dinamici del contratto (punto 6).
114
+
115
+ ### 7. Versionamento dei moduli
116
+ Aggiungere in testa a ogni modulo un commento con versione (o data) per capire a
117
+ colpo d'occhio, nei progetti, quale revisione è installata.
118
+
119
+ ### 8. Documentazione
120
+ README dello starter kit con la mappa: cosa si imposta nel config (e solo lì),
121
+ cosa fa ogni modulo, come aggiornare i moduli in un progetto esistente.
122
+
123
+ ## Ordine consigliato
124
+
125
+ 2 → 3 → 4 → 5 (refactor moduli, uno per volta, build demo di verifica dopo ognuno)
126
+ → 1 (consolidamento `settings`) → 6 → 7 → 8.
127
+
128
+ ## Note
129
+
130
+ - `merge-jsonc` è già stato spostato in `src/merge-jsonc.mjs` (convertito in ESM):
131
+ la dir `src/` ospita le utility interne dell'installer, NON copiate nei progetti;
132
+ `webpack-modules/` resta la dir dei moduli copiati nei progetti.
133
+ - Criterio di "fatto": si deve poter copiare l'intera `webpack-modules/` aggiornata
134
+ in un progetto esistente senza toccare il suo `webpack.config.mjs` e ottenere
135
+ una build identica (o migliorata) senza errori.
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ // Merge JSONC: aggiunge in fondo al file di destinazione le chiavi di primo
3
+ // livello del template, prefissate con '_', preservando commenti e
4
+ // formattazione di entrambi i file.
5
+ // Uso: node merge-jsonc.mjs <template> <destinazione>
6
+
7
+ import fs from 'fs';
8
+
9
+ // Analizza un testo JSONC ignorando stringhe e commenti: individua
10
+ // l'oggetto principale, le virgole a profondità 1 e la posizione
11
+ // dell'ultimo carattere significativo (non spazio, non commento)
12
+ function scan(text) {
13
+ let depth = 0;
14
+ let open = -1;
15
+ let close = -1;
16
+ let last = -1;
17
+ const commas = [];
18
+ let i = 0;
19
+ while (i < text.length) {
20
+ const c = text[i];
21
+ const next = text[i + 1];
22
+ if (c === '"') {
23
+ i++;
24
+ while (i < text.length && text[i] !== '"') {
25
+ if (text[i] === '\\') i++;
26
+ i++;
27
+ }
28
+ last = i;
29
+ } else if (c === '/' && next === '/') {
30
+ while (i < text.length && text[i] !== '\n') i++;
31
+ continue;
32
+ } else if (c === '/' && next === '*') {
33
+ i += 2;
34
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++;
35
+ i++;
36
+ } else {
37
+ if (c === '{' || c === '[') {
38
+ if (depth === 0 && open === -1 && c === '{') open = i;
39
+ depth++;
40
+ } else if (c === '}' || c === ']') {
41
+ depth--;
42
+ if (depth === 0 && close === -1) close = i;
43
+ } else if (c === ',' && depth === 1) {
44
+ commas.push(i);
45
+ }
46
+ if (!/\s/.test(c)) last = i;
47
+ }
48
+ i++;
49
+ }
50
+ return { open, close, commas, last };
51
+ }
52
+
53
+ // Posizione della prima doppia virgoletta fuori dai commenti
54
+ function firstQuotePos(text) {
55
+ let i = 0;
56
+ while (i < text.length) {
57
+ const c = text[i];
58
+ const next = text[i + 1];
59
+ if (c === '/' && next === '/') {
60
+ while (i < text.length && text[i] !== '\n') i++;
61
+ } else if (c === '/' && next === '*') {
62
+ i += 2;
63
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++;
64
+ i += 2;
65
+ } else if (c === '"') {
66
+ return i;
67
+ } else {
68
+ i++;
69
+ }
70
+ }
71
+ return -1;
72
+ }
73
+
74
+ // Voci di primo livello di un oggetto JSONC (spezzate sulle virgole a profondità 1)
75
+ function topLevelEntries(text, s) {
76
+ const body = text.slice(s.open + 1, s.close);
77
+ const offset = s.open + 1;
78
+ const bounds = s.commas.map((p) => p - offset).concat(body.length);
79
+ const entries = [];
80
+ let prev = 0;
81
+ for (const b of bounds) {
82
+ entries.push(body.slice(prev, b));
83
+ prev = b + 1;
84
+ }
85
+ return entries;
86
+ }
87
+
88
+ // Nome della chiave di una voce (contenuto della prima stringa fuori dai commenti)
89
+ function keyName(entry) {
90
+ const q = firstQuotePos(entry);
91
+ if (q < 0) return null;
92
+ let key = '';
93
+ let i = q + 1;
94
+ while (i < entry.length && entry[i] !== '"') {
95
+ if (entry[i] === '\\') i++;
96
+ key += entry[i];
97
+ i++;
98
+ }
99
+ return key;
100
+ }
101
+
102
+ const [tplPath, destPath] = process.argv.slice(2);
103
+
104
+ try {
105
+ const tpl = fs.readFileSync(tplPath, 'utf8');
106
+ const dest = fs.readFileSync(destPath, 'utf8');
107
+ const t = scan(tpl);
108
+ const d = scan(dest);
109
+ if (t.open < 0 || t.close < 0) throw new Error(tplPath + ': oggetto JSON non trovato');
110
+ if (d.open < 0 || d.close < 0) throw new Error(destPath + ': oggetto JSON non trovato');
111
+
112
+ // chiavi di primo livello già presenti nella destinazione
113
+ const destKeys = new Set(
114
+ topLevelEntries(dest, d).map((entry) => keyName(entry)).filter((k) => k !== null)
115
+ );
116
+
117
+ // prefissa con '_' la chiave di ogni voce del template, saltando le voci di
118
+ // soli commenti/spazi e le chiavi già presenti nella destinazione
119
+ // (così rilanciare lo script non produce duplicati)
120
+ const renamed = [];
121
+ for (const entry of topLevelEntries(tpl, t)) {
122
+ const cleaned = entry.replace(/^\s+/, '').replace(/\s+$/, '');
123
+ const key = keyName(cleaned);
124
+ if (key === null || destKeys.has('_' + key)) continue;
125
+ const q = firstQuotePos(cleaned);
126
+ renamed.push('\n ' + cleaned.slice(0, q + 1) + '_' + cleaned.slice(q + 1));
127
+ }
128
+ if (renamed.length === 0) process.exit(0);
129
+
130
+ // aggiunge le voci prima della chiusura dell'oggetto principale della
131
+ // destinazione, inserendo una virgola dopo l'ultima voce esistente se serve
132
+ const dBody = dest.slice(d.open + 1, d.close);
133
+ const b = scan(dBody);
134
+ const needsComma = b.last >= 0 && dBody[b.last] !== ',';
135
+ const insertAt = d.open + 1 + b.last + 1;
136
+ const out = needsComma
137
+ ? dest.slice(0, insertAt) + ',' + dest.slice(insertAt, d.close) + renamed.join(',') + '\n' + dest.slice(d.close)
138
+ : dest.slice(0, d.close) + renamed.join(',') + '\n' + dest.slice(d.close);
139
+
140
+ fs.writeFileSync(destPath, out);
141
+ } catch (err) {
142
+ console.error('merge-jsonc: ' + err.message);
143
+ process.exit(1);
144
+ }
@@ -1,8 +1,20 @@
1
1
  #!/bin/zsh
2
2
 
3
+ # IMPORTANTE: questo script è idempotente e deve restare tale.
4
+ # Può essere rilanciato più volte sullo stesso progetto (anche via `npx starter-kit`
5
+ # per aggiornare un setup esistente) senza sovrascrivere nulla né creare duplicati:
6
+ # i file esistenti vengono saltati o copiati con prefisso NEW-, i merge di
7
+ # package.json e *.code-workspace non duplicano le chiavi già presenti.
8
+ # Ogni modifica futura deve conservare questa proprietà.
9
+
10
+ RED='\033[0;31m'
11
+ YELLOW='\033[0;33m'
12
+ GREEN='\033[0;32m'
13
+ NC='\033[0m' # No Color
14
+
3
15
  # Questo script richiede zsh: se lanciato con sh/bash blocca l'esecuzione
4
16
  if [ -z "$ZSH_VERSION" ]; then
5
- echo "Errore: lanciare con 'zsh starter-install.sh', non con sh/bash." >&2
17
+ printf "${RED}Errore: lanciare con 'zsh starter-install.sh', non con sh/bash.${NC}\n" >&2
6
18
  exit 1
7
19
  fi
8
20
 
@@ -19,14 +31,9 @@ BASE_URL=${0:A:h}
19
31
  echo "BASE_URL: ${BASE_URL}"
20
32
 
21
33
  TEMPLATES_DIR="${BASE_URL}/templates"
22
- # WEBPACK_SOURCE_DIR="$BASE_URL/webpack-setup"
34
+ # WEBPACK_SOURCE_DIR="$BASE_URL/starter-kit"
23
35
  WEBPACK_SOURCE_DIR="$BASE_URL"
24
36
 
25
- RED='\033[0;31m'
26
- YELLOW='\033[0;33m'
27
- GREEN='\033[0;32m'
28
- NC='\033[0m' # No Color
29
-
30
37
  # protezione contro la sovrascrittura
31
38
  set -C
32
39
 
@@ -58,7 +65,25 @@ safe_cat() {
58
65
  }
59
66
 
60
67
  echo -e "${GREEN}...package.json${NC}"
61
- safe_cat "${TEMPLATES_DIR}/package-tpl.json" package.json new
68
+ if [ ! -f package.json ]; then
69
+ safe_cat "${TEMPLATES_DIR}/package-tpl.json" package.json
70
+ elif [ ! -f "${TEMPLATES_DIR}/package-tpl.json" ]; then
71
+ echo -e "${RED}Sorgente mancante: ${TEMPLATES_DIR}/package-tpl.json${NC}"
72
+ else
73
+ # package.json esistente: merge con il template, aggiungendo in fondo
74
+ # le chiavi del template prefissate con '_' (ordine originale preservato)
75
+ node -e '
76
+ const fs = require("fs");
77
+ const existing = JSON.parse(fs.readFileSync("package.json", "utf8"));
78
+ const tpl = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
79
+ for (const [key, value] of Object.entries(tpl)) {
80
+ existing["_" + key] = value;
81
+ }
82
+ fs.writeFileSync("package.json", JSON.stringify(existing, null, 2) + "\n");
83
+ ' "${TEMPLATES_DIR}/package-tpl.json"
84
+ echo -e "${YELLOW}package.json esistente: chiavi del template aggiunte in fondo con prefisso '_' (da integrare o rimuovere)${NC}"
85
+ BLOCKED_FILES+=("package.json → merge: chiavi template aggiunte con prefisso \`_\`")
86
+ fi
62
87
 
63
88
  echo -e "${GREEN}...gitignore${NC}"
64
89
  safe_cat "${TEMPLATES_DIR}/_gitignore" .gitignore new
@@ -88,7 +113,29 @@ safe_cat "${TEMPLATES_DIR}/_browserslistrc" .browserslistrc new
88
113
  safe_cat "${TEMPLATES_DIR}/_editorconfig" .editorconfig new
89
114
  safe_cat "${TEMPLATES_DIR}/_prettierrc" .prettierrc new
90
115
  safe_cat "${TEMPLATES_DIR}/jsconfig.json" jsconfig.json new
91
- safe_cat "${TEMPLATES_DIR}/__project__.code-workspace" __project__.code-workspace new
116
+ safe_cat "${TEMPLATES_DIR}/tokens-config.mjs" tokens-config.mjs new
117
+ safe_cat "${TEMPLATES_DIR}/_root_htaccess" _root_htaccess
118
+ safe_cat "${TEMPLATES_DIR}/_root_robots.txt" _root_robots.txt
119
+
120
+ # code-workspace: se un file .code-workspace esiste già (qualunque sia il nome),
121
+ # le chiavi del template vengono aggiunte in fondo con prefisso '_'
122
+ WORKSPACE_TPL="${TEMPLATES_DIR}/__project__.code-workspace"
123
+ WORKSPACE_FILES=( *.code-workspace(N) )
124
+ if [ ${#WORKSPACE_FILES[@]} -gt 1 ]; then
125
+ echo -e "${RED}Trovati più file .code-workspace (${WORKSPACE_FILES[*]}): merge non eseguito${NC}"
126
+ elif [ ${#WORKSPACE_FILES[@]} -eq 0 ]; then
127
+ safe_cat "$WORKSPACE_TPL" __project__.code-workspace
128
+ elif [ ! -f "$WORKSPACE_TPL" ]; then
129
+ echo -e "${RED}Sorgente mancante: ${WORKSPACE_TPL}${NC}"
130
+ else
131
+ WORKSPACE_FILE="${WORKSPACE_FILES[1]}"
132
+ if node "${BASE_URL}/src/merge-jsonc.mjs" "$WORKSPACE_TPL" "$WORKSPACE_FILE"; then
133
+ echo -e "${YELLOW}${WORKSPACE_FILE} esistente: chiavi del template aggiunte in fondo con prefisso '_' (da integrare o rimuovere)${NC}"
134
+ BLOCKED_FILES+=("${WORKSPACE_FILE} → merge: chiavi template aggiunte con prefisso \`_\`")
135
+ else
136
+ echo -e "${RED}Merge di ${WORKSPACE_FILE} non riuscito${NC}"
137
+ fi
138
+ fi
92
139
 
93
140
 
94
141
  echo -e "\n${GREEN}...eslint${NC}"
@@ -104,7 +151,9 @@ npm i -D webpack-cli webpack-dev-server webpack-manifest-plugin webpack
104
151
  npm i -D @babel/core @babel/preset-env babel-loader terser-webpack-plugin
105
152
  npm i -D webpack-remove-empty-scripts copy-webpack-plugin html-loader html-webpack-plugin
106
153
  npm i -D postcss autoprefixer postcss-custom-media @csstools/postcss-global-data postcss-loader postcss-preset-env
107
- npm i -D cssnano mini-css-extract-plugin style-loader css-loader css-minimizer-webpack-plugin
154
+ # NB: niente cssnano: la minificazione è di css-minimizer-webpack-plugin e cssnano
155
+ # nel loader postcss eliminerebbe i commenti `purgecss ignore` prima di PurgeCSS
156
+ npm i -D mini-css-extract-plugin style-loader css-loader css-minimizer-webpack-plugin
108
157
  npm i -D responsive-loader
109
158
  npm i -D process dotenv-webpack
110
159
  npm i -D svgo svg-url-loader svgo-loader svgo-add-viewbox mini-svg-data-uri
@@ -126,6 +175,7 @@ FILES=(
126
175
  'svg-rules.mjs'
127
176
  'svgo.config.mjs'
128
177
  'postcss.config.mjs'
178
+ 'purgecss-variables-safelist.mjs'
129
179
  )
130
180
 
131
181
  # se la cartella esiste già, i moduli vengono copiati in NEW-webpack-modules
@@ -149,6 +199,12 @@ mkdir -p "$TEMPLATE_DIR_NAME"
149
199
  cd "$TEMPLATE_DIR_NAME"
150
200
  mkdir -p error-pages imgs icons src src/css favicons design-tokens
151
201
  cd ..
202
+
203
+ # template delle entry css (globale/di pagina e critical): ogni entry deve
204
+ # importare direttamente custom-properties.css (vedi commenti nei file stessi)
205
+ safe_cat "${TEMPLATES_DIR}/entry-tpl.css" "${TEMPLATE_DIR_NAME}/src/entry-tpl.css"
206
+ safe_cat "${TEMPLATES_DIR}/entry-tpl.critical.css" "${TEMPLATE_DIR_NAME}/src/entry-tpl.critical.css"
207
+
152
208
  set +C
153
209
 
154
210
  # Riepilogo finale dei file già esistenti
@@ -0,0 +1,68 @@
1
+
2
+ # no cache
3
+ # <IfModule mod_headers.c>
4
+ # Header set Cache-Control "no-cache, no-store, must-revalidate"
5
+ # Header set Pragma "no-cache"
6
+ # Header set Expires 0
7
+ # </IfModule>
8
+
9
+ # blocco iframe
10
+ # "deny" = tutti siti
11
+ # "sameorigin" tutti tranne quelli che vengono dalla stessa origine
12
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
13
+ Header set X-Frame-Options "sameorigin"
14
+
15
+ <IfModule mod_expires.c>
16
+ ExpiresActive On
17
+ AddType application/vnd.ms-fontobject .eot
18
+ AddType application/x-font-ttf .ttf
19
+ AddType application/x-font-opentype .otf
20
+ AddType application/x-font-woff .woff
21
+ AddType application/x-font-woff2 .woff2
22
+ AddType application/font-sfnt otf ttf
23
+ AddType application/font-woff woff
24
+ AddType application/font-woff2 woff2
25
+ AddType image/svg+xml .svg
26
+
27
+ ExpiresByType application/vnd.ms-fontobject "access plus 1 year"
28
+ ExpiresByType application/x-font-ttf "access plus 1 year"
29
+ ExpiresByType application/x-font-opentype "access plus 1 year"
30
+ ExpiresByType application/x-font-woff "access plus 1 year"
31
+ ExpiresByType application/x-font-woff2 "access plus 1 year"
32
+ ExpiresByType application/font-sfnt "access plus 1 year"
33
+ ExpiresByType application/font-woff "access plus 1 year"
34
+ ExpiresByType application/font-woff2 "access plus 1 year"
35
+
36
+ ExpiresByType text/html "access plus 1 hour"
37
+ ExpiresByType text/css "access plus 1 year"
38
+ ExpiresByType text/x-javascript "access plus 1 year"
39
+ ExpiresByType application/javascript "access plus 1 year"
40
+ ExpiresByType application/x-javascript "access plus 1 year"
41
+
42
+ ExpiresByType image/svg+xml "access plus 1 year"
43
+ ExpiresByType image/gif "access plus 1 year"
44
+ ExpiresByType image/png "access plus 1 year"
45
+ ExpiresByType image/jpeg "access plus 1 year"
46
+ ExpiresByType image/pjpeg "access plus 1 year"
47
+ ExpiresByType image/webp "access plus 1 year"
48
+ ExpiresByType image/avif "access plus 1 year"
49
+
50
+ ExpiresByType application/pdf "access plus 1 year"
51
+
52
+ ExpiresByType video/mp4 "access plus 1 year"
53
+ ExpiresByType video/webm "access plus 1 year"
54
+ ExpiresByType video/x-m4v "access plus 1 year"
55
+ ExpiresByType video/quicktime "access plus 1 year"
56
+ ExpiresByType audio/mpeg "access plus 1 year"
57
+ ExpiresByType audio/x-wav "access plus 1 year"
58
+
59
+ ExpiresByType image/x-icon "access plus 1 year"
60
+ ExpiresDefault "access plus 1 year"
61
+ </IfModule>
62
+
63
+ <IfModule !mod_expires.c>
64
+ <IfModule mod_headers.c>
65
+ # 1 year
66
+ Header set Cache-Control 'public, max-age 31536000'
67
+ </IfModule>
68
+ </IfModule>
@@ -0,0 +1,5 @@
1
+ User-agent: *
2
+ # Allow: /
3
+ Disallow: /
4
+
5
+ # Sitemap: https://www.example.com/sitemap.xml
@@ -0,0 +1,18 @@
1
+ /*
2
+ Template di entry css critical — rinominare e adattare.
3
+
4
+ Le entry con suffisso `.critical` generano asset destinati a essere
5
+ INLINATI nei template html: devono restare AUTOSUFFICIENTI e minimi.
6
+ Il suffisso attiva l'istanza PurgeCSS dedicata con purge stretto, in dev
7
+ e in prod (vedi purgeCSSPluginCritical in webpack.config.mjs), e la regola
8
+ dedicata in css-rules.mjs.
9
+
10
+ Anche qui va importato direttamente custom-properties.css: PurgeCSS
11
+ elabora gli asset singolarmente e mantiene in questo asset solo le
12
+ custom properties usate in questo file, quindi l'asset resta minimo
13
+ senza dipendere dal css globale della pagina.
14
+ */
15
+
16
+ @import '@css/custom-properties.css';
17
+
18
+ /* stili above-the-fold della pagina (layout iniziale, spinner, ecc.) */
@@ -0,0 +1,24 @@
1
+ /*
2
+ Template di entry css (globale o di pagina) — rinominare e adattare.
3
+
4
+ NB: OGNI entry css (globale, di pagina, `.critical`) deve importare
5
+ direttamente custom-properties.css: PurgeCSS elabora gli asset css
6
+ SINGOLARMENTE e mantiene in ciascuno solo le custom properties
7
+ effettivamente usate nell'asset stesso (`variables: true`, che
8
+ sostituisce postcss-jit-props). L'import diretto non produce quindi
9
+ duplicazioni nel css finale.
10
+ */
11
+
12
+ @import '@css/custom-properties.css';
13
+ /* @import '@css/fonts.css'; */
14
+
15
+ /* @import '@css/custom-media.css'; */ /* NON necessario: iniettato da postcss-global-data */
16
+
17
+ /* moduli minimo: importare solo quelli effettivamente usati */
18
+ /* @import '@minimo/css/reset.css'; */
19
+ /* @import '@minimo/css/layout-base.css'; */
20
+ /* @import '@minimo/css/container.css'; */
21
+ /* @import '@minimo/css/forms/forms.css'; */
22
+ /* @import '@minimo/css/buttons/buttons.css'; */
23
+ /* @import '@minimo/css/alerts.css'; */
24
+ /* ... */
@@ -32,6 +32,6 @@
32
32
  "build tokens": "npx buildTokens --config ./path/to/tokens-config.mjs",
33
33
  "check unresolved props": "npx checkUnresolvedProps --config ./path/to/tokens-config.mjs"
34
34
  },
35
- "dependencies": {},
36
- "devDependencies": {}
35
+ "devDependencies": {},
36
+ "dependencies": {}
37
37
  }
@@ -0,0 +1,33 @@
1
+
2
+ // import path from 'path';
3
+
4
+ // path relativi a questo file
5
+
6
+ const config = {
7
+ // build tokens
8
+ stylelintConfigPath: '../../stylelint.config.mjs',
9
+ buildPath: '../src/css',
10
+ destFile: 'custom-properties.css',
11
+ source: [
12
+ '../../node_modules/@massimo-cassandro/minimo/design-tokens/_src/**/*.tokens.{mjs,jsonc}',
13
+ './figma-tokens-OK/*.{jsonc,mjs}',
14
+ ],
15
+
16
+ colorScalePrefixes: ['primary', 'secondary', 'neutral'],
17
+
18
+ penpotFormat: 'jsonc',
19
+ penpotBuildPath: null,
20
+ penpotDestFile: null,
21
+ penpotExpressions: 'resolve',
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // check-unresolved-custom-props.mjs settings
25
+ // ---------------------------------------------------------------------------
26
+ extra_custom_props_files: [],
27
+ dir_to_check: '../src', // percorso della dir che include i css da analizzare
28
+ exclude_pattern: [ // patterns per l'esclusione dal controllo unresolved-props
29
+ /^--_/,
30
+ ]
31
+ };
32
+
33
+ export default config;
@@ -146,7 +146,12 @@ function css_loaders({
146
146
  modules: css_modules
147
147
  ? {
148
148
  auto: true,
149
- localIdentName: isDevelopment ? '[local]_[hash:base64:6]' : '[hash:base64:10]',
149
+ // il prefisso fisso `m_`, presente in ENTRAMBE le modalità, consente
150
+ // il safelist delle classi dei css modules in PurgeCSS (safelist.deep
151
+ // [/^m_/], vedi webpack.config.mjs): le classi hashate non possono
152
+ // comparire nei file di contenuto scansionati, e il purge è attivo
153
+ // anche in dev
154
+ localIdentName: isDevelopment ? 'm_[local]_[hash:base64:6]' : 'm_[hash:base64:8]',
150
155
  exportLocalsConvention: 'as-is',
151
156
 
152
157
  // namedExport: false,
@@ -4,40 +4,34 @@ import { fileURLToPath } from 'url';
4
4
  import globalData from '@csstools/postcss-global-data';
5
5
  import customMedia from 'postcss-custom-media';
6
6
  import autoprefixer from 'autoprefixer';
7
- import cssnano from 'cssnano';
8
- import jitProps from 'postcss-jit-props';
9
- // import openProps from './modulo_cjs.cjs';
10
7
 
11
8
  const __filename = fileURLToPath(import.meta.url);
12
9
  const __dirname = path.dirname(__filename);
13
10
 
11
+ // NB: le custom properties NON sono gestite qui (postcss-jit-props rimosso):
12
+ // ogni entry css importa direttamente custom-properties.css e le props
13
+ // non utilizzate vengono rimosse in build da PurgeCSS (`variables: true`,
14
+ // vedi webpack.config.mjs)
15
+
16
+ // NB: la minificazione è gestita da CssMinimizerPlugin (webpack.config.mjs):
17
+ // cssnano NON va aggiunto qui, oltre a duplicare la minificazione
18
+ // rimuoverebbe i commenti speciali `/* purgecss start/end ignore */`
19
+ // prima dell'esecuzione di PurgeCSS
20
+
14
21
  export default (loaderContext) => {
15
22
  const isDevelopment = loaderContext.mode === 'development';
16
23
 
17
24
  const plugins = [
25
+ // rende disponibili le @custom-media a tutti i file css
26
+ // (il contenuto iniettato viene rimosso a fine elaborazione;
27
+ // NON aggiungere qui custom-properties.css: duplicherebbe le props
28
+ // in ogni asset e interferirebbe con il purge, vedi sopra)
18
29
  globalData({
19
30
  files: [
20
- // openProps.
21
- path.resolve(__dirname, '../src/css/custom-properties.css'),
22
31
  path.resolve(__dirname, '../src/css/custom-media.css'),
23
32
  ]
24
33
  }),
25
34
 
26
- // https://github.com/GoogleChromeLabs/postcss-jit-props
27
- // https://github.com/argyleink/open-props
28
- // require('postcss-jit-props')(require('open-props')),
29
- // require('postcss-jit-props')({
30
- // ...require('open-props'),
31
- // custom_selector: ':where(html)'
32
- // }),
33
- jitProps({
34
- files: [
35
- // ...openProps,
36
- path.resolve(__dirname, '../src/css/custom-properties.css')
37
- ],
38
- custom_selector: ':where(html)'
39
- }),
40
-
41
35
  // per IOS < 17 (test)
42
36
  // https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting
43
37
  // https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-light-dark-function#readme
@@ -47,7 +41,6 @@ export default (loaderContext) => {
47
41
  //}),
48
42
  // require('@csstools/postcss-light-dark-function')({preserve: false}), /* NB non funziona benissimo... */
49
43
 
50
-
51
44
  // https://github.com/csstools/postcss-plugins/
52
45
 
53
46
  customMedia({
@@ -58,19 +51,6 @@ export default (loaderContext) => {
58
51
  autoprefixer(),
59
52
  ];
60
53
 
61
- if (!isDevelopment) {
62
- // per IOS < 17
63
- // require('postcss-nesting')({
64
- // edition: '2021',
65
- // noIsPseudoSelector: true
66
- //}),
67
- // require('@csstools/postcss-light-dark-function'), /* NB non funziona benissimo... */
68
-
69
-
70
- plugins.push(cssnano({ preset: 'default' }));
71
- }
72
-
73
-
74
54
  return {
75
55
  plugins: plugins
76
56
  };
@@ -0,0 +1,92 @@
1
+ // webpack-modules/purgecss-variables-safelist.mjs
2
+ import fs from 'fs';
3
+ import { globSync } from 'glob';
4
+
5
+ /*
6
+ Genera l'elenco di custom properties da passare a `safelist.variables` di PurgeCSS.
7
+ L'elenco viene ricalcolato in memoria a ogni avvio di webpack: nessun file su disco.
8
+
9
+ Perché serve: PurgeCSS (`variables: true`) registra l'uso delle variabili solo
10
+ nelle regole che effettivamente elabora, all'interno del singolo asset css.
11
+ Restano quindi "invisibili" gli usi di var() che avvengono:
12
+
13
+ 1. nei blocchi racchiusi tra i commenti `purgecss start/end ignore`
14
+ (presenti ad es. nei sorgenti di minimo): le regole vengono mantenute,
15
+ ma le custom properties che consumano verrebbero rimosse dal :root;
16
+ 2. nei css caricati fuori dagli asset purgati (shadow DOM dei web components,
17
+ import con `?raw`): consumano le props del :root della pagina;
18
+ 3. nelle dichiarazioni di un asset consumate in un altro asset
19
+ (es. override di tema definiti nel css di una pagina e consumati
20
+ dal css globale): vanno indicate tramite `seeds`.
21
+
22
+ Inoltre `safelist.variables` NON segue le catene di dipendenze (una prop
23
+ safelisted resta nel css, ma i suoi riferimenti var() non vengono marcati
24
+ come usati: `--a: var(--b)` in safelist non salva `--b`): l'elenco viene
25
+ quindi espanso con la chiusura transitiva calcolata sul grafo di tutte le
26
+ dichiarazioni `--x: ...` presenti nei sorgenti.
27
+ */
28
+
29
+ const VAR_REFS_REGEX = /var\(\s*(--[A-Za-z0-9_-]+)/g;
30
+ const DECLARATION_REGEX = /(--[A-Za-z0-9_-]+)\s*:\s*([^;}]+)/g;
31
+ const IGNORE_START_REGEX = /\/\*\s*purgecss start ignore\s*\*\//;
32
+ const IGNORE_END_REGEX = /\/\*\s*purgecss end ignore\s*\*\//;
33
+
34
+ const varRefs = (cssText) => [...cssText.matchAll(VAR_REFS_REGEX)].map(m => m[1]);
35
+
36
+ /**
37
+ * @param {object} params
38
+ * @param {string[]} params.declarationGlobs glob dei css che contribuiscono al grafo
39
+ * delle dipendenze; i loro blocchi `purgecss ignore` contano come usi
40
+ * @param {string[]} [params.shadowGlobs] glob dei css usati fuori dagli asset purgati
41
+ * (shadow DOM / `?raw`): TUTTI i loro usi di var() contano
42
+ * @param {(string|RegExp)[]} [params.seeds] props da mantenere comunque (nomi o pattern
43
+ * risolti sui nomi dichiarati), es. override consumati in un altro asset
44
+ * @returns {string[]} elenco ordinato di nomi di custom properties
45
+ */
46
+ export function purgecssVariablesSafelist({ declarationGlobs, shadowGlobs = [], seeds = [] }) {
47
+
48
+ const declaredRefs = new Map(); // nome → Set(var() referenziate nelle sue dichiarazioni)
49
+ const usages = new Set(); // usi da mantenere (blocchi ignore + shadow css)
50
+
51
+ for (const file of globSync(declarationGlobs, { nodir: true })) {
52
+ const src = fs.readFileSync(file, 'utf8');
53
+
54
+ // grafo delle dipendenze: ogni dichiarazione `--x: ...` con le var() nel valore
55
+ for (const declaration of src.matchAll(DECLARATION_REGEX)) {
56
+ const [, name, value] = declaration;
57
+ if (!declaredRefs.has(name)) {
58
+ declaredRefs.set(name, new Set());
59
+ }
60
+ varRefs(value).forEach(ref => declaredRefs.get(name).add(ref));
61
+ }
62
+
63
+ // usi di var() nei blocchi `purgecss start/end ignore`
64
+ src.split(IGNORE_START_REGEX).slice(1).forEach(chunk => {
65
+ varRefs(chunk.split(IGNORE_END_REGEX)[0]).forEach(ref => usages.add(ref));
66
+ });
67
+ }
68
+
69
+ // shadow css: ogni uso di var() conta
70
+ for (const file of globSync(shadowGlobs, { nodir: true })) {
71
+ varRefs(fs.readFileSync(file, 'utf8')).forEach(ref => usages.add(ref));
72
+ }
73
+
74
+ // risoluzione dei seeds sui nomi dichiarati
75
+ const declaredNames = [...declaredRefs.keys()];
76
+ const seedNames = seeds.flatMap(seed =>
77
+ typeof seed === 'string' ? [seed] : declaredNames.filter(name => seed.test(name))
78
+ );
79
+
80
+ // chiusura transitiva delle dipendenze
81
+ const result = new Set();
82
+ const queue = [...seedNames, ...usages];
83
+ while (queue.length > 0) {
84
+ const name = queue.pop();
85
+ if (!result.has(name)) {
86
+ result.add(name);
87
+ queue.push(...(declaredRefs.get(name) ?? []));
88
+ }
89
+ }
90
+
91
+ return [...result].sort();
92
+ }
@@ -1,4 +1,4 @@
1
- // webpack.config.mjs
1
+ // webpack.config.mjs __project_name__
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import webpack from 'webpack';
@@ -22,6 +22,7 @@ import { PurgeCSSPlugin } from 'purgecss-webpack-plugin';
22
22
  import { globSync } from 'glob';
23
23
 
24
24
  import { cssRules } from './webpack-modules/css-rules.mjs';
25
+ import { purgecssVariablesSafelist } from './webpack-modules/purgecss-variables-safelist.mjs';
25
26
  import { getJsConfigAliases } from './webpack-modules/get-jsConfig-aliases.mjs';
26
27
  import { svgRules } from './webpack-modules/svg-rules.mjs';
27
28
 
@@ -41,6 +42,7 @@ const isDevelopment = process.env.NODE_ENV === 'development'
41
42
  ,favicons_path_regexp = new RegExp(favicons_path) // source pattern per le favicons (regexp o null)
42
43
  ,jsConfigAliases = getJsConfigAliases(path.resolve(__dirname, './jsconfig.json'))
43
44
  ,packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, './package.json'), 'utf-8'))
45
+ ,purgeCSSDebug = false // true per loggare i selettori rimossi da PurgeCSS a fine build
44
46
  // ,manifest_shared_seed = {}
45
47
  ;
46
48
 
@@ -87,6 +89,111 @@ const CopyWebpackPluginPatterns = [
87
89
  // }
88
90
  ];
89
91
 
92
+ // =>> PurgeCSS: opzioni e istanze (usate nei plugins e dal log di debug)
93
+ //
94
+ // Vengono create DUE istanze, attive in ENTRAMBE le modalità (dev e prod),
95
+ // così i problemi di purge emergono subito durante lo sviluppo:
96
+ // - `purgeCSSPluginCritical`: solo le entry `*.critical` (purge stretto, gli asset
97
+ // devono essere autosufficienti e minimi perché inlinati nei template html)
98
+ // - `purgeCSSPlugin`: tutti gli asset (il doppio passaggio sui critical è idempotente)
99
+ //
100
+ // NB (watch mode): i template twig NON sono osservati da webpack: dopo aver
101
+ // aggiunto una classe solo in un twig occorre rilanciare la build (o toccare
102
+ // un file js/css) perché il purge venga ricalcolato.
103
+ // NB (dev): il plugin agisce solo sugli asset css emessi; con
104
+ // `inlineCssInDevMode: true` i css non-critical in dev sono iniettati da
105
+ // style-loader e non vengono purgati (i critical passano comunque da
106
+ // MiniCssExtract e sono sempre purgati)
107
+
108
+ // `paths`: SOLO file che generano markup o classi (html/ejs, twig, php, js);
109
+ // NIENTE file css tra i paths: verrebbero letti come "contenuto" e ogni selettore
110
+ // risulterebbe usato, vanificando il purge
111
+ const purgeCSSCommonOptions = {
112
+ paths: () => globSync(
113
+ [
114
+ path.resolve(__dirname, './src/**/*.{js,mjs,jsx}'),
115
+ path.resolve(__dirname, './*.ejs'), // template html di webpack
116
+ path.resolve(__dirname, './src/**/*.ejs'),
117
+ // progetti symfony: template twig e classi generate lato php
118
+ // path.resolve(__dirname, '../templates/**/*.twig'),
119
+ // path.resolve(__dirname, '../src/**/*.php'),
120
+ // il js di minimo genera markup con classi proprie (snackbar, unsplash-page, ecc.)
121
+ // NB: adattare il percorso di node_modules se questo config è in una
122
+ // sottodirectory del progetto (es. frontend/ → '../node_modules/...')
123
+ path.resolve(__dirname, './node_modules/@massimo-cassandro/minimo/src/**/*.{js,mjs}'),
124
+ ],
125
+ { nodir: true }
126
+ ),
127
+
128
+ // extractor di default (/[A-Za-z0-9_-]+/g): adeguato a classi/tag/id,
129
+ // NON definire extractors custom (il vecchio /[A-z0-9-:/]+/ era buggato:
130
+ // il range [A-z] include caratteri non voluti come `[`, `\`, `^`, ...)
131
+
132
+ variables: true, // rimuove le custom properties non usate (sostituisce postcss-jit-props)
133
+ keyframes: true, // rimuove i @keyframes non referenziati
134
+ // fontFace resta false (default): i font sono referenziati solo tramite
135
+ // var(--font-family) e purgecss non risolve le custom properties nei valori,
136
+ // quindi con `true` i @font-face verrebbero rimossi anche se usati
137
+
138
+ rejected: purgeCSSDebug // popola purgedStats per il log di debug
139
+ };
140
+
141
+ // safelist base minima, comune alle due istanze
142
+ const purgeCSSSafelistBase = {
143
+ standard: [
144
+ // classi costruite dinamicamente nei template o lato server, invisibili
145
+ // all'analisi statica dei paths. Esempio (da adattare al progetto):
146
+ // flash messages twig con class="alert alert-{{ label }}"
147
+ /^alert-(success|notice|error|warning)$/
148
+ ],
149
+ deep: [
150
+ // css modules: prefisso `m_` presente in dev e prod (vedi css-rules.mjs),
151
+ // le classi hashate non possono comparire nei file scansionati
152
+ /^m_/
153
+ ],
154
+ greedy: [
155
+ // INDISPENSABILE: purgecss non riconosce il nesting selector nativo e senza
156
+ // questo pattern i selettori annidati composti solo da `&` + pseudo classi
157
+ // (&:hover, &:focus-visible, `&` semplice, ecc.) verrebbero SEMPRE rimossi
158
+ /^&$/
159
+ ]
160
+ };
161
+
162
+ // css della pagina critici (inlinati nei template): purge stretto,
163
+ // devono essere autosufficienti e minimi
164
+ const purgeCSSPluginCritical = new PurgeCSSPlugin({
165
+ ...purgeCSSCommonOptions,
166
+ only: ['.critical'],
167
+ safelist: purgeCSSSafelistBase
168
+ });
169
+
170
+ // tutti gli altri asset (il purge è idempotente: il secondo passaggio
171
+ // sui critical non rimuove altro)
172
+ const purgeCSSPlugin = new PurgeCSSPlugin({
173
+ ...purgeCSSCommonOptions,
174
+ safelist: {
175
+ ...purgeCSSSafelistBase,
176
+ // elenco generato: usi di var() nei blocchi `purgecss ignore` e nei css
177
+ // shadow-DOM/`?raw`, override cross-asset (seeds), con chiusura transitiva
178
+ // delle dipendenze (vedi webpack-modules/purgecss-variables-safelist.mjs)
179
+ variables: purgecssVariablesSafelist({
180
+ declarationGlobs: [
181
+ path.resolve(__dirname, './src/**/*.css'),
182
+ path.resolve(__dirname, './node_modules/@massimo-cassandro/minimo/src/**/*.css'),
183
+ ],
184
+ shadowGlobs: [
185
+ path.resolve(__dirname, './src/web-components/**/*.css'),
186
+ path.resolve(__dirname, './node_modules/@massimo-cassandro/minimo/src/web-components/**/*.css'),
187
+ ],
188
+ seeds: [
189
+ // props dichiarate in un asset ma consumate in un altro (es. override
190
+ // di tema definiti nel css di una pagina e consumati dal css globale):
191
+ // indicarle qui per nome ('--btn-primary-bg') o pattern (/^--btn-/)
192
+ ]
193
+ })
194
+ }
195
+ });
196
+
90
197
  // recupero metadata immagini
91
198
  // const require = createRequire(import.meta.url);
92
199
  // responsive-loader adapter (CJS) — use createRequire
@@ -96,8 +203,14 @@ const CopyWebpackPluginPatterns = [
96
203
  /** CONFIG **/
97
204
 
98
205
  // =>> entries
206
+ // NB: percorsi dalla root del progetto
99
207
  const entries = {
100
- 'xxxxxx': './src/index.js' // NB percorso dalla root
208
+ 'xxxxxx': './src/index.js'
209
+
210
+ // css critici inlinati nei template html: il suffisso `.critical` nel nome
211
+ // della entry attiva l'istanza PurgeCSS dedicata con purge stretto
212
+ // (vedi purgeCSSPluginCritical) e la regola dedicata in css-rules.mjs
213
+ // ,'xxxxxx.critical': './src/xxxxxx.critical.css'
101
214
  };
102
215
 
103
216
 
@@ -106,7 +219,14 @@ const config = {
106
219
 
107
220
  // watch: isDevelopment, // necessario se non si usa devServer
108
221
  watchOptions: {
109
- ignored: [ 'build', '**/node_modules', '.git', '_private' ],
222
+ ignored: [
223
+ '**/build/**',
224
+ '**/_dev/**',
225
+ '**/.git/**',
226
+ '**/_private/**',
227
+ '**/node_modules/**',
228
+ '!**/node_modules/@massimo-cassandro/**'
229
+ ],
110
230
  // poll: 1000, // se necessario, risolve il problema `Error: EMFILE: too many open files, watch`
111
231
  },
112
232
 
@@ -153,7 +273,7 @@ const config = {
153
273
  extractComments: false
154
274
  })
155
275
  ],
156
- runtimeChunk: true,
276
+ runtimeChunk: 'single', // true
157
277
  splitChunks: {
158
278
  cacheGroups: {
159
279
 
@@ -172,7 +292,18 @@ const config = {
172
292
  },
173
293
 
174
294
  shared: {
175
- test: shared_chunk_paths,
295
+ // solo moduli js (module.type !== 'css/mini-extract'): il css estratto
296
+ // resta nel chunk della entry che lo importa, così ogni asset css è
297
+ // autosufficiente. Necessario per PurgeCSS (`variables: true` funziona
298
+ // per singolo asset) e per i critical css inlinati nei template
299
+ //
300
+ // TODO: se in futuro la duplicazione di css tra le pagine dovesse crescere
301
+ // (componenti importati dal js di più entry), valutare un cacheGroup `styles`
302
+ // dedicato ai css condivisi: andranno escluse le entry `.critical` (devono
303
+ // restare autosufficienti) e gestite in safelist.variables le custom properties
304
+ // definite/consumate tra asset diversi (vedi purgecss-variables-safelist.mjs)
305
+ test: (module) => module.type !== 'css/mini-extract'
306
+ && shared_chunk_paths.test(module.nameForCondition?.() ?? ''),
176
307
  // test: /[\\/]node_modules[\\/]/,
177
308
 
178
309
  // alternativa: non include nel chunk shared i path che contengono la stringa `blurhash`
@@ -281,7 +412,7 @@ const config = {
281
412
  // removeKeyHash: true, // /([a-f0-9]{32}\.?)/gi, // /(\?as_asset)$/,
282
413
  // // rimuove i font dal manifest. Non necessari, rendono il file inutilmente grande
283
414
  // filter: isDevelopment? undefined : (FileDescriptor) => {
284
- // return /_fonts/.test(FileDescriptor.path)? false : true;
415
+ // return /fonts/.test(FileDescriptor.path)? false : true;
285
416
  // },
286
417
  // sort: isDevelopment? undefined : (a, b) => a.name.localeCompare(b.name)
287
418
  // }),
@@ -368,41 +499,42 @@ const config = {
368
499
  );
369
500
  },
370
501
  raw: true,
371
- // exclude: /\.critical/ // chunk name
502
+ // niente banner nei critical css: vengono inlinati nei template html
503
+ exclude: /\.critical/ // chunk name
372
504
  }),
373
505
 
374
- // =>> plugins: PurgeCSSPlugin (per ultimo)
506
+ // =>> plugins: PurgeCSSPlugin (per ultimo, attivo in dev e prod: i problemi
507
+ // di purge devono emergere subito durante lo sviluppo, non solo in build)
375
508
  // https://github.com/FullHuman/purgecss/tree/main/packages/purgecss-webpack-plugin
376
509
  // https://purgecss.com/configuration.html
377
- // https://github.com/isaacs/node-glob#readme
378
- new PurgeCSSPlugin({
379
-
380
- // css da pulire
381
- paths: globSync(
382
-
383
- // path.resolve(__dirname, '@minimo/**/!(*.module).css'),
384
- path.resolve(__dirname, '@src/**/*.{css,js,ejs}'),
385
- // path.resolve(__dirname, '@src/**/*.js'),
386
- // path.resolve(__dirname, '@app/**/*.ejs'),
387
- // path.join(__dirname, './templates/**/*.twig')
388
-
389
- { nodir: true }
390
- ), // .filter(filePath => !filePath.endsWith('.module.css'));
391
-
392
-
393
-
394
- variables: false, // Analizza le variabili custom
395
- safelist: {
396
- standard: ['body', 'html', 'button', 'a'],
397
- deep: [/^btn/],
398
- greedy: []
399
-
400
- },
401
-
402
- // verbose: true,
403
- // rejected: true,
404
- // stdout: true
405
- })
510
+ //
511
+ // NB: il plugin elabora ogni asset css SINGOLARMENTE, quindi con `variables: true`
512
+ // una custom property viene mantenuta solo se definita e usata nello stesso asset.
513
+ // Per questo:
514
+ // - ogni entry css importa direttamente custom-properties.css (che sostituisce
515
+ // postcss-jit-props: le props non usate vengono rimosse qui)
516
+ // - il chunk `shared` contiene solo js (vedi optimization.splitChunks)
517
+ // - le props dichiarate in un asset ma consumate altrove (override di tema,
518
+ // shadow DOM dei web components) sono gestite dalla safelist generata
519
+ // (vedi purgecss-variables-safelist.mjs)
520
+ purgeCSSPluginCritical,
521
+ purgeCSSPlugin,
522
+
523
+ // log dei selettori rimossi (richiede `rejected: true` nei plugin, vedi purgeCSSDebug).
524
+ // NB: legge `purgedStats` dalle ISTANZE del plugin, non da compilation.purgedStats
525
+ // (proprietà inesistente: il vecchio log basato su di essa era codice morto)
526
+ ...(purgeCSSDebug ? [{
527
+ apply: (compiler) => {
528
+ compiler.hooks.afterEmit.tap('PurgeCSSLog', () => {
529
+ /* eslint-disable no-console */
530
+ console.log('\n--- [PurgeCSS] Selettori rimossi (critical) ---');
531
+ console.dir(purgeCSSPluginCritical.purgedStats, { depth: null, maxArrayLength: null });
532
+ console.log('\n--- [PurgeCSS] Selettori rimossi ---');
533
+ console.dir(purgeCSSPlugin.purgedStats, { depth: null, maxArrayLength: null });
534
+ /* eslint-enable no-console */
535
+ });
536
+ }
537
+ }] : [])
406
538
 
407
539
  ], // end plugins
408
540
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.15.1",
3
+ "version": "1.16.1",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -24,7 +24,8 @@
24
24
  "update-version": "./dev-tools/update-version/index.mjs",
25
25
  "upd@m": "./dev-tools/upd@m/index.mjs",
26
26
  "svgIconsTools": "./dev-tools/svg-icons-tools/index.mjs",
27
- "create-favicons": "./dev-tools/create-favicons/index.mjs"
27
+ "create-favicons": "./dev-tools/create-favicons/index.mjs",
28
+ "starter-kit": "./dev-tools/starter-kit/starter-install.sh"
28
29
  },
29
30
  "repository": {
30
31
  "type": "git",