@elia97/officina 0.3.0 → 0.5.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/README.md +103 -10
- package/dist/bin.js +0 -0
- package/dist/bundle.d.ts +1 -0
- package/dist/bundle.js +1 -0
- package/dist/checks/analytics.js +14 -8
- package/dist/checks/bundle.js +18 -10
- package/dist/checks/doctor.d.ts +1 -1
- package/dist/checks/doctor.js +72 -8
- package/dist/checks/lighthouse.d.ts +1 -1
- package/dist/checks/lighthouse.js +9 -5
- package/dist/checks/roadmap.d.ts +1 -1
- package/dist/checks/roadmap.js +11 -5
- package/dist/checks/smoke.js +11 -10
- package/dist/checks/vercel-cli.js +33 -17
- package/dist/gen/hook-points.d.mts +6 -0
- package/dist/gen/hook-points.mjs +13 -0
- package/dist/gen/inject-section.d.mts +25 -2
- package/dist/gen/inject-section.mjs +13 -13
- package/dist/gen/inject-strings.d.mts +3 -0
- package/dist/gen/inject-strings.mjs +7 -3
- package/dist/gen/section-targets.d.mts +5 -0
- package/dist/gen/section-targets.mjs +23 -0
- package/dist/icons.d.ts +1 -0
- package/dist/icons.js +2 -0
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -4
- package/dist/lib/alignment.d.ts +19 -0
- package/dist/lib/alignment.js +139 -0
- package/dist/lib/anchors.d.ts +2 -0
- package/dist/lib/anchors.js +35 -0
- package/dist/lib/bundle-budget.d.ts +6 -1
- package/dist/lib/bundle-budget.js +9 -6
- package/dist/lib/config-gaps.d.ts +7 -0
- package/dist/lib/config-gaps.js +29 -0
- package/dist/lib/config.d.ts +17 -1
- package/dist/lib/config.js +36 -3
- package/dist/lib/contract.d.ts +8 -0
- package/dist/lib/contract.js +51 -6
- package/dist/lib/pins.d.ts +10 -0
- package/dist/lib/pins.js +48 -0
- package/dist/lib/routes.d.ts +6 -2
- package/dist/lib/routes.js +19 -10
- package/dist/lib/smoke-production.js +17 -17
- package/dist/lib/tooling.d.ts +3 -0
- package/dist/lib/tooling.js +24 -0
- package/dist/lib/validate.d.ts +11 -0
- package/dist/lib/validate.js +40 -0
- package/dist/lib/versions.d.ts +5 -0
- package/dist/lib/versions.js +38 -0
- package/dist/lib/workflows.d.ts +6 -0
- package/dist/lib/workflows.js +70 -0
- package/dist/sh/lhci-local.sh +12 -12
- package/dist/smoke.d.ts +2 -0
- package/dist/smoke.js +4 -0
- package/package.json +31 -13
- package/presets/biome.base.json +100 -0
- package/presets/lefthook.yml +11 -0
- package/dist/lib/vercel-cli-pin.d.ts +0 -2
- package/dist/lib/vercel-cli-pin.js +0 -29
package/README.md
CHANGED
|
@@ -18,7 +18,6 @@ pnpm add -D @elia97/officina
|
|
|
18
18
|
"check:routes": "officina check routes",
|
|
19
19
|
"check:roadmap": "officina check roadmap",
|
|
20
20
|
"check:placeholders": "officina check placeholders",
|
|
21
|
-
"check:vercel-cli": "officina check vercel-cli",
|
|
22
21
|
"perf:bundle": "officina check bundle",
|
|
23
22
|
"smoke:prod": "officina check smoke",
|
|
24
23
|
"analytics:verify": "officina check analytics",
|
|
@@ -43,14 +42,15 @@ I gate leggono dalla cartella corrente, che deve essere la radice del repository
|
|
|
43
42
|
|---|---|---|
|
|
44
43
|
| `check bundle` | il JavaScript e il CSS di `dist/client`, gzip, contro un budget per rotta | `src/pages` |
|
|
45
44
|
| `check smoke [url]` | la produzione viva: pagine, header di sicurezza, BotID, host canonico, barra finale | `src/pages`, più le rotte non HTML |
|
|
46
|
-
| `check analytics [GTM-…]` | il container GTM pubblico contro gli eventi di
|
|
45
|
+
| `check analytics [GTM-…]` | il container GTM pubblico contro gli eventi del modulo di link-tracking del progetto (`analytics.linkTracking`) | — |
|
|
47
46
|
| `check lighthouse [--local]` | Lighthouse CI sul `.lighthouserc.json` del progetto; `--local` fa build, server e Chrome da sé | `src/pages` |
|
|
48
|
-
| `gen icons` | le icone del manifest, disegnate
|
|
47
|
+
| `gen icons` | le icone del manifest, disegnate dal favicon SVG in `public/` del progetto | — |
|
|
49
48
|
|
|
50
49
|
Il motore è uguale per tutti; ciò che cambia da un progetto all'altro sta in un file solo, `officina.config.ts` nella radice:
|
|
51
50
|
|
|
52
51
|
```ts
|
|
53
|
-
import { type Budget,
|
|
52
|
+
import { type Budget, defineConfig, type SmokeCheck } from '@elia97/officina'
|
|
53
|
+
import { DEFAULT_CHECKS } from '@elia97/officina/smoke'
|
|
54
54
|
|
|
55
55
|
import { SITE } from './src/lib/site.ts'
|
|
56
56
|
|
|
@@ -62,12 +62,96 @@ export default defineConfig({
|
|
|
62
62
|
icons: { background: SITE.themeColor.light },
|
|
63
63
|
bundle: { budgets: [motion], cssMaxGzip: 14 * 1024 },
|
|
64
64
|
smoke: { checks: [...DEFAULT_CHECKS, checkLanguageRedirect] },
|
|
65
|
+
features: { analytics: 'required', roadmap: false },
|
|
66
|
+
routes: { representatives: { '/blog/[slug]': '/blog/ciao-mondo' } },
|
|
65
67
|
})
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
La radice del pacchetto esporta `defineConfig` e i tipi; i pezzi del motore stanno nei sottopercorsi, così la radice resta la sola cosa che serve conoscere per configurarsi.
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
| Sottopercorso | Cosa esporta |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `@elia97/officina/smoke` | `DEFAULT_CHECKS`, i cinque controlli, `SECURITY_HEADERS`, `NON_HTML_ROUTES` |
|
|
75
|
+
| `@elia97/officina/bundle` | `CSS_BUDGET_GZIP` |
|
|
76
|
+
| `@elia97/officina/icons` | `ICON_SPECS` |
|
|
77
|
+
|
|
78
|
+
`siteUrl` serve a `check smoke` e `icons.background` a `gen icons`. I budget del progetto vengono prima del default di 20 KB, nell'ordine in cui sono scritti; `smoke.checks` sostituisce la lista del pacchetto, quindi chi non ha BotID la ricompone senza `checkBotIdChallenge`; `smoke.securityHeaders` e `smoke.nonHtmlRoutes` sostituiscono `SECURITY_HEADERS` e `NON_HTML_ROUTES`.
|
|
79
|
+
|
|
80
|
+
`features` dice quali controlli il progetto pretende: `'required'` fa fallire il gate quando ciò che deve guardare non c'è, `false` lo spegne dicendolo. Una voce non dichiarata vale `'required'`, e `doctor` la segnala: un gate che esce 0 su un container GTM che non esiste non afferma niente. `routes.representatives` dà a ogni pattern dinamico di `src/pages` un percorso vero, e da lì smoke e Lighthouse guardano anche quelle pagine.
|
|
81
|
+
|
|
82
|
+
Il file lo carica Node, che fuori da `node_modules` toglie i tipi da sé: niente `enum`, niente alias `@/`. Due file `officina.config.*` insieme sono un errore, come un file senza default export; il resto lo valida `loadConfig` a runtime, nominando il percorso della voce sbagliata — `bundle.cssMaxGzip: atteso un numero, ricevuto una stringa` — e una voce sconosciuta, perché `sitUrl` al posto di `siteUrl` non deve passare in silenzio.
|
|
83
|
+
|
|
84
|
+
`sharp` è una dipendenza facoltativa del progetto, non del pacchetto: serve solo a `gen icons`, che senza lo dice ed esce 1. `@biomejs/biome` invece è una peer dependency con l'intervallo che il preset regge, e `doctor` confronta la versione del progetto con quello.
|
|
85
|
+
|
|
86
|
+
## Preset di configurazione
|
|
87
|
+
|
|
88
|
+
Le regole che valgono per tutti i progetti stanno nel pacchetto; il progetto estende e tiene solo le proprie eccezioni.
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
|
|
93
|
+
"extends": ["@elia97/officina/biome"]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
# lefthook.yml
|
|
99
|
+
extends:
|
|
100
|
+
- node_modules/@elia97/officina/presets/lefthook.yml
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Action per i workflow
|
|
104
|
+
|
|
105
|
+
I passi dei workflow stanno in `actions/` di questo repository, come composite action: girano dentro il job di chi le chiama, quindi il nome del check e l'`environment` restano del progetto. Il riferimento va fissato a un tag di versione o a uno SHA, mai a `@main`, e deve essere la stessa versione del pacchetto installato — `doctor` lo verifica.
|
|
106
|
+
|
|
107
|
+
| Action | Passi | Cosa resta nel workflow del progetto |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `actions/ci` | checkout, node, install, `pnpm run ci`, build, `perf:bundle`; con `e2e: 'true'` anche Playwright | trigger, permessi, il job `ci` |
|
|
110
|
+
| `actions/review` | `fallow review` sul diff contro il merge-base | il job informativo |
|
|
111
|
+
| `actions/deploy` | risoluzione del tag, gate, `vercel pull`, `build`, `deploy`, smoke; espone `url` | trigger, `environment`, i tre segreti Vercel passati in `with:`, il job che controlla se i segreti ci sono |
|
|
112
|
+
| `actions/lighthouse` | build equivalente alla produzione e `pnpm run lhci` | trigger, etichetta, `continue-on-error` |
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
jobs:
|
|
116
|
+
deploy:
|
|
117
|
+
runs-on: ubuntu-latest
|
|
118
|
+
environment:
|
|
119
|
+
name: production
|
|
120
|
+
url: ${{ steps.deploy.outputs.url }}
|
|
121
|
+
steps:
|
|
122
|
+
- uses: Elia97/officina/actions/deploy@v0.4.0
|
|
123
|
+
id: deploy
|
|
124
|
+
with:
|
|
125
|
+
ref: ${{ inputs.ref }}
|
|
126
|
+
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
|
127
|
+
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
|
|
128
|
+
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
I tre segreti sono input obbligatori e l'action li mette nell'`env` dei soli tre passi che chiamano `vercel`. Nell'`env` del job li vedrebbero anche `pnpm install` e gli script di installazione di ogni dipendenza, che girano codice di terzi con in mano un token di deploy.
|
|
132
|
+
|
|
133
|
+
Quello che si scarica al volo è fissato a una versione esatta, in un posto solo: la CLI di Vercel in `actions/deploy/action.yml`, `@lhci/cli` in `src/checks/lighthouse.ts`, `serve` in `src/sh/lhci-local.sh`. Che i primi due siano esatti, uguali ovunque e non indietro di una major su npm lo guarda ogni lunedì `.github/workflows/vercel-cli.yml`, con `officina check vercel-cli`.
|
|
134
|
+
|
|
135
|
+
Perché le action non restino ferme per sempre, Dependabot di questo repository copre l'ecosistema `github-actions` anche dentro `/actions/*`, dove le action di terze parti sono fissate allo SHA intero con la versione nel commento.
|
|
136
|
+
|
|
137
|
+
## doctor
|
|
138
|
+
|
|
139
|
+
`officina doctor` dice, in qualunque repository, cosa manca perché il progetto prenda tutto da fuori: i punti di aggancio dei generatori, gli ancoraggi delle pagine a sezioni e dei dizionari, gli script, le dipendenze e gli strumenti di `package.json`, i residui di ciò che è uscito (copia del metodo, script, documenti commerciali), i preset, i workflow di GitHub e Dependabot, `officina.config.ts`. Sta dentro `ci`, quindi un progetto allineato non torna indietro senza che il gate lo dica.
|
|
140
|
+
|
|
141
|
+
Le prime due sezioni rispondono a due domande diverse. I punti di aggancio sono l'elenco di `src/lib/contract.ts`, e chiedono che una cosa esista: i moduli che il codice generato importa, le cartelle in cui i generatori scrivono, `class-variance-authority` fra le dipendenze, lo script `check` che il post-gen lancia. Gli ancoraggi guardano dentro quei file, e chiedono che abbiano ancora la forma su cui l'iniezione conta:
|
|
142
|
+
|
|
143
|
+
- per ogni collection a sezioni registrata in `src/content.config.ts` — riconosciuta dall'import di `<nome>CollectionSchema` da `@/lib/schemas/<nome>`, non dai marcatori, che sono proprio ciò che può sparire — la funzione `<nome>CollectionSchema`, la sua `z.discriminatedUnion` sopra un array letterale, il parametro non destrutturato che una sezione con immagine riceve, il `return { … }` di primo livello di `get<Nome>Sections`, e una sola pagina sotto `src/pages/` che porti `{/* @gen:<nome>-sections */}` e, su quella, `// @gen:<nome>-imports`;
|
|
144
|
+
- per ogni dizionario di `src/i18n/strings/`, il suo `export const <lingua> = { … } as const`.
|
|
145
|
+
|
|
146
|
+
Sono gli stessi controlli del pre-volo dei generatori, non una copia: `doctor` chiama `assertSectionAnchors` e `assertDictionaries`, che vivono in `src/gen/` insieme all'iniezione. Al pre-volo resta ciò che dipende dal nome di quello che sta per nascere — la sezione già nell'unione, l'identificatore già preso nel barrel o nel frontmatter, la chiave già nel dizionario, il file che esiste già — e che quindi si può chiedere solo al lancio del generatore.
|
|
147
|
+
|
|
148
|
+
Le altre sezioni misurano che il progetto e il pacchetto si muovano insieme:
|
|
149
|
+
|
|
150
|
+
- **stessa versione**: il riferimento delle action nei workflow — `@v<x.y.z>`, oppure uno SHA con `# v<x.y.z>` nel commento — deve essere la versione del pacchetto installato. Un progetto col pacchetto alla 0.8 e le action alla 0.6 non è un progetto allineato;
|
|
151
|
+
- **una PR sola**: il `.github/dependabot.yml` del progetto deve avere un gruppo `multi-ecosystem-groups` che prende `@elia97/officina` da npm e `Elia97/officina/*` da `github-actions`. Senza, Dependabot ne apre due e il controllo qui sopra le boccia entrambe;
|
|
152
|
+
- **gli strumenti che preset e action danno per scontati**: `@biomejs/biome`, `lefthook`, `@commitlint/cli`, `@commitlint/config-conventional` e `fallow` fra le `devDependencies`, con la versione di Biome dentro l'intervallo che il preset dichiara di reggere.
|
|
153
|
+
|
|
154
|
+
`package.json`, `.claude/settings.json`, `biome.json` e `.mcp.json` si leggono come JSON, non cercandoci dentro delle stringhe: `"hooks"` in un valore qualunque non è un blocco `hooks`. I workflow restano una ricerca testuale — non c'è un parser YAML nel pacchetto — ma una riga commentata non soddisfa nessun controllo.
|
|
71
155
|
|
|
72
156
|
## Sviluppo
|
|
73
157
|
|
|
@@ -76,24 +160,33 @@ pnpm install
|
|
|
76
160
|
pnpm test # vitest; i test che leggono dal disco girano in test/fixture-project/
|
|
77
161
|
pnpm typecheck
|
|
78
162
|
pnpm build # tsc → dist/, l'unica cosa che viene pubblicata
|
|
163
|
+
pnpm test:pack # pnpm pack, poi il tarball installato in un progetto vuoto
|
|
79
164
|
```
|
|
80
165
|
|
|
166
|
+
`ci`, `build` e `test:pack` girano anche in `.github/workflows/ci.yml`, su ogni pull request e su ogni push a `main`.
|
|
167
|
+
|
|
81
168
|
I sorgenti sono TypeScript, ma il pacchetto pubblica JavaScript: Node toglie i tipi al volo solo fuori da `node_modules`. I test con `--diff` hanno bisogno di almeno un commit su `main`.
|
|
82
169
|
|
|
83
170
|
## Pubblicare
|
|
84
171
|
|
|
85
172
|
```sh
|
|
86
|
-
pnpm version <patch|minor|major>
|
|
87
|
-
|
|
173
|
+
pnpm version <patch|minor|major> # scrive package.json e crea il tag
|
|
174
|
+
git push --follow-tags
|
|
88
175
|
```
|
|
89
176
|
|
|
177
|
+
Il resto lo fa `.github/workflows/release.yml`, sul push di un tag `v*`: controlla che il tag corrisponda a `version`, rilancia il gate, la build e il test del tarball, pubblica su npm con trusted publishing (OIDC) e provenance, e crea la release su GitHub con le note generate. Niente `pnpm publish` dal terminale: quello che finisce su npm esce da un tag, e da nient'altro.
|
|
178
|
+
|
|
179
|
+
Le action si richiamano per tag, quindi **una versione senza tag non è adottabile**: `doctor` chiede ai progetti la stessa versione del pacchetto installato, e un tag che non esiste non si può scrivere in un workflow.
|
|
180
|
+
|
|
181
|
+
Una volta sola, a mano: su npmjs.com il trusted publisher per `Elia97/officina` e `release.yml`; su GitHub un ruleset sui tag `v*` che ne vieta modifica e cancellazione.
|
|
182
|
+
|
|
90
183
|
## Generatori
|
|
91
184
|
|
|
92
185
|
`officina gen` avvolge plop con i generatori e i template del pacchetto, e scrive nel progetto da cui lo lanci: `section`, `page`, `component`, `collection`. `plop` e `ts-morph` sono dipendenze del pacchetto, non del progetto.
|
|
93
186
|
|
|
94
|
-
I generatori scrivono codice che deve incastrarsi nello scaffold, quindi il progetto deve avere i punti di aggancio che si aspettano: i moduli che il codice generato importa
|
|
187
|
+
I generatori scrivono codice che deve incastrarsi nello scaffold, quindi il progetto deve avere i punti di aggancio che si aspettano: i moduli che il codice generato importa, l'elenco è `src/lib/contract.ts`, e i file in cui iniettano, con la forma su cui l'iniezione conta. Sono le prime due sezioni di `officina doctor`. Finché manca qualcosa il pre-volo del generatore si ferma prima di scrivere un solo file.
|
|
95
188
|
|
|
96
|
-
Un progetto aggiunge i propri generatori con un file `officina.generators.mjs` nella radice, con la firma di un plopfile: `export default function (plop)`.
|
|
189
|
+
Un progetto aggiunge i propri generatori con un file `officina.generators.mjs` nella radice, con la firma di un plopfile: `export default function (plop)`. È un'estensione avanzata e non è parte dell'API del pacchetto: è legata alla firma di plop, quindi una major di plop può romperla.
|
|
97
190
|
|
|
98
191
|
I test dei generatori girano sui punti di aggancio del progetto di prova, copiati da `vetrina`. Che un progetto vero li abbia ancora lo verifica `doctor`, non i test.
|
|
99
192
|
|
package/dist/bin.js
CHANGED
|
File without changes
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CSS_BUDGET_GZIP } from './lib/bundle-budget.ts';
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CSS_BUDGET_GZIP } from './lib/bundle-budget.js';
|
package/dist/checks/analytics.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import { coverageOf, extractLinkEvents } from '../lib/analytics-coverage.js';
|
|
7
|
-
import { loadConfig } from '../lib/config.js';
|
|
7
|
+
import { isRequired, loadConfig } from '../lib/config.js';
|
|
8
8
|
import { extractTriggers, googleTagIds, parseContainerData } from '../lib/gtm-container.js';
|
|
9
9
|
const LINK_TRACKING = 'src/lib/analytics/link-tracking.ts';
|
|
10
10
|
const TIMEOUT_MS = 15_000;
|
|
@@ -18,30 +18,36 @@ function reportCoverage(coverage) {
|
|
|
18
18
|
}
|
|
19
19
|
const missing = coverage.filter(({ covered }) => !covered);
|
|
20
20
|
if (missing.length > 0) {
|
|
21
|
-
console.error(`\n✗ ${missing.length}
|
|
21
|
+
console.error(`\n✗ ${missing.length} evento/i che il sito emette e nessun trigger ascolta.`);
|
|
22
22
|
return 1;
|
|
23
23
|
}
|
|
24
|
-
console.log('\n✓
|
|
24
|
+
console.log('\n✓ Ogni evento che il sito emette ha un trigger.');
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
export async function main(args = []) {
|
|
28
|
-
const
|
|
28
|
+
const config = await loadConfig(process.cwd());
|
|
29
|
+
const { analytics = {} } = config;
|
|
29
30
|
const gtmId = resolveGtmId(args);
|
|
30
31
|
if (gtmId === null) {
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
if (!isRequired(config, 'analytics')) {
|
|
33
|
+
console.log('check:analytics — spento da `features.analytics: false`.');
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
console.error('\n✗ Nessun container GTM da controllare: passane uno come argomento, imposta PUBLIC_GTM_ID,\n' +
|
|
37
|
+
' oppure dichiara `features.analytics: false` se il progetto non ha analytics.\n');
|
|
38
|
+
return 1;
|
|
33
39
|
}
|
|
34
40
|
const response = await fetch(`https://www.googletagmanager.com/gtm.js?id=${gtmId}`, {
|
|
35
41
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
36
42
|
});
|
|
37
43
|
if (!response.ok) {
|
|
38
|
-
console.error(`✗ container ${gtmId}
|
|
44
|
+
console.error(`✗ container ${gtmId} illeggibile: HTTP ${response.status}`);
|
|
39
45
|
return 1;
|
|
40
46
|
}
|
|
41
47
|
const container = parseContainerData(await response.text());
|
|
42
48
|
const source = readFileSync(analytics.linkTracking ?? LINK_TRACKING, 'utf8');
|
|
43
49
|
const coverage = coverageOf(extractLinkEvents(source), extractTriggers(container));
|
|
44
|
-
console.log(`\nGTM ${gtmId} —
|
|
50
|
+
console.log(`\nGTM ${gtmId} — ID di misurazione: ${googleTagIds(container).join(', ') || '—'}\n`);
|
|
45
51
|
return reportCoverage(coverage);
|
|
46
52
|
}
|
|
47
53
|
if (import.meta.main)
|
package/dist/checks/bundle.js
CHANGED
|
@@ -29,16 +29,24 @@ function measurePages(chunks, budgets) {
|
|
|
29
29
|
return filesWithExtension(DIST, '.html')
|
|
30
30
|
.map((htmlPath) => {
|
|
31
31
|
const route = routeOf(htmlPath, DIST);
|
|
32
|
-
const reached = staticClosure(htmlEntries(readFileSync(htmlPath, 'utf8')), chunks);
|
|
32
|
+
const { reached, unknown } = staticClosure(htmlEntries(readFileSync(htmlPath, 'utf8')), chunks);
|
|
33
33
|
return {
|
|
34
34
|
route,
|
|
35
35
|
gzip: total(reached),
|
|
36
36
|
deferredGzip: total(deferredClosure(reached, chunks)),
|
|
37
37
|
budget: budgetFor(route, budgets),
|
|
38
|
+
unknown: [...unknown].sort(),
|
|
38
39
|
};
|
|
39
40
|
})
|
|
40
41
|
.sort((a, b) => b.gzip - a.gzip);
|
|
41
42
|
}
|
|
43
|
+
// Zero JavaScript su una rotta è un esito normale in Astro; un nome citato che fra i chunk non c'è
|
|
44
|
+
// vuol dire che la misura non copre tutto il bundle, e allora non afferma niente.
|
|
45
|
+
function unknownChunkFailures(pages) {
|
|
46
|
+
return pages
|
|
47
|
+
.filter((page) => page.unknown.length > 0)
|
|
48
|
+
.map((page) => `${page.route}: formato del bundle non riconosciuto — ${page.unknown.join(', ')} non è in ${ASSETS}`);
|
|
49
|
+
}
|
|
42
50
|
function overBudgetFailures(pages) {
|
|
43
51
|
return pages
|
|
44
52
|
.filter((page) => page.gzip > page.budget.maxGzip)
|
|
@@ -48,18 +56,18 @@ function overBudgetFailures(pages) {
|
|
|
48
56
|
});
|
|
49
57
|
}
|
|
50
58
|
function printPages(pages, width) {
|
|
51
|
-
console.log('\
|
|
52
|
-
console.log(`${'
|
|
59
|
+
console.log('\nBudget di bundle — JS del client per rotta (gzip, chiusura statica)\n');
|
|
60
|
+
console.log(`${'ROTTA'.padEnd(width)} ${'STATICO'.padStart(9)} ${'BUDGET'.padStart(9)} ${'DIFFERITO'.padStart(9)}`);
|
|
53
61
|
for (const page of pages) {
|
|
54
62
|
const deferred = page.deferredGzip > 0 ? gz(page.deferredGzip) : '—';
|
|
55
63
|
const line = `${page.route.padEnd(width)} ${gz(page.gzip).padStart(9)} ${gz(page.budget.maxGzip).padStart(9)} ${deferred.padStart(9)}`;
|
|
56
64
|
console.log(page.gzip > page.budget.maxGzip ? `${line} ✗` : line);
|
|
57
65
|
}
|
|
58
|
-
console.log('\
|
|
66
|
+
console.log('\nDIFFERITO = raggiungibile solo da `await import()` (caricato dopo il primo paint, dietro una guardia a runtime).');
|
|
59
67
|
}
|
|
60
68
|
function printStylesheets(stylesheets, cssMaxGzip, failed, width) {
|
|
61
69
|
const worst = heaviestStylesheet(stylesheets);
|
|
62
|
-
console.log(`\nCSS (
|
|
70
|
+
console.log(`\nCSS (blocca il rendering: il foglio più pesante che una rotta collega)`);
|
|
63
71
|
for (const sheet of [...stylesheets].sort((a, b) => b.gzip - a.gzip)) {
|
|
64
72
|
const mark = sheet === worst && failed ? ' ✗' : '';
|
|
65
73
|
console.log(` ${sheet.file.padEnd(width - 2)} ${gz(sheet.gzip).padStart(9)} ${gz(cssMaxGzip).padStart(9)}${mark}`);
|
|
@@ -68,7 +76,7 @@ function printStylesheets(stylesheets, cssMaxGzip, failed, width) {
|
|
|
68
76
|
function printSsrNote(expected) {
|
|
69
77
|
if (expected.ssr.length === 0)
|
|
70
78
|
return;
|
|
71
|
-
console.log(`\
|
|
79
|
+
console.log(`\nNOTA ${expected.ssr.length} pagina/e con \`export const prerender = false\`, fuori da questo budget:`);
|
|
72
80
|
for (const file of expected.ssr)
|
|
73
81
|
console.log(` - ${file}`);
|
|
74
82
|
}
|
|
@@ -78,8 +86,8 @@ export async function main() {
|
|
|
78
86
|
const pages = measurePages(readChunks(), bundle.budgets ?? []);
|
|
79
87
|
const expected = expectedRoutes(readPageFiles(PAGES), PAGES);
|
|
80
88
|
const failures = missingRouteFailures(expected, pages.map((page) => page.route), DIST);
|
|
81
|
-
failures.push(...overBudgetFailures(pages));
|
|
82
|
-
const width = Math.max('
|
|
89
|
+
failures.push(...unknownChunkFailures(pages), ...overBudgetFailures(pages));
|
|
90
|
+
const width = Math.max('ROTTA'.length, ...pages.map((p) => p.route.length));
|
|
83
91
|
printPages(pages, width);
|
|
84
92
|
const stylesheets = readStylesheets();
|
|
85
93
|
const cssFailure = cssBudgetFailure(stylesheets, cssMaxGzip);
|
|
@@ -88,10 +96,10 @@ export async function main() {
|
|
|
88
96
|
printStylesheets(stylesheets, cssMaxGzip, cssFailure !== null, width);
|
|
89
97
|
printSsrNote(expected);
|
|
90
98
|
if (failures.length > 0) {
|
|
91
|
-
console.error(`\n✗
|
|
99
|
+
console.error(`\n✗ Budget di bundle:\n${failures.map((f) => ` - ${f}`).join('\n')}\n`);
|
|
92
100
|
return 1;
|
|
93
101
|
}
|
|
94
|
-
console.log('\n✓
|
|
102
|
+
console.log('\n✓ Budget di bundle rispettato su ogni rotta attesa.\n');
|
|
95
103
|
return 0;
|
|
96
104
|
}
|
|
97
105
|
if (import.meta.main)
|
package/dist/checks/doctor.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare function main(): number
|
|
2
|
+
export declare function main(): Promise<number>;
|
package/dist/checks/doctor.js
CHANGED
|
@@ -1,15 +1,79 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import process from 'node:process';
|
|
5
|
+
import { dependencyGaps, leftoverGaps, presetGaps, scriptGaps, } from '../lib/alignment.js';
|
|
6
|
+
import { anchorGaps } from '../lib/anchors.js';
|
|
3
7
|
import { cliOptions, exitCode, printFindings } from '../lib/cli.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import { findConfigFile, loadConfig } from '../lib/config.js';
|
|
9
|
+
import { configGaps } from '../lib/config-gaps.js';
|
|
10
|
+
import { contractGaps } from '../lib/contract.js';
|
|
11
|
+
import { trackedAndUntracked } from '../lib/git.js';
|
|
12
|
+
import { expectedRoutes, missingRepresentatives, readPageFiles } from '../lib/routes.js';
|
|
13
|
+
import { toolingGaps } from '../lib/tooling.js';
|
|
14
|
+
import { packageVersion } from '../lib/versions.js';
|
|
15
|
+
import { dependabotGaps, workflowGaps } from '../lib/workflows.js';
|
|
16
|
+
const PAGES = 'src/pages';
|
|
17
|
+
function projectFiles(root) {
|
|
18
|
+
return {
|
|
19
|
+
paths: trackedAndUntracked(),
|
|
20
|
+
read: (path) => (existsSync(join(root, path)) ? readFileSync(join(root, path), 'utf8') : undefined),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function readManifest(files) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(files.read('package.json') ?? '{}');
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function loadedConfig(root) {
|
|
32
|
+
if (findConfigFile(root) === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
try {
|
|
35
|
+
return await loadConfig(root);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function orphanPatterns(root, config) {
|
|
42
|
+
const pages = join(root, PAGES);
|
|
43
|
+
if (!existsSync(pages) || config === undefined || config instanceof Error)
|
|
44
|
+
return [];
|
|
45
|
+
return missingRepresentatives(expectedRoutes(readPageFiles(pages), pages), config.routes?.representatives);
|
|
46
|
+
}
|
|
47
|
+
export async function main() {
|
|
48
|
+
const root = process.cwd();
|
|
49
|
+
const files = projectFiles(root);
|
|
50
|
+
const manifest = readManifest(files);
|
|
51
|
+
const version = packageVersion();
|
|
52
|
+
const config = await loadedConfig(root);
|
|
53
|
+
const sections = [
|
|
54
|
+
['punti di aggancio dei generatori', contractGaps(root)],
|
|
55
|
+
['ancoraggi delle pagine a sezioni e dei dizionari', anchorGaps(root)],
|
|
56
|
+
[
|
|
57
|
+
'script, dipendenze e strumenti di package.json',
|
|
58
|
+
[...scriptGaps(manifest), ...dependencyGaps(manifest), ...toolingGaps(manifest)],
|
|
59
|
+
],
|
|
60
|
+
['residui di ciò che è uscito dal repository', leftoverGaps(files)],
|
|
61
|
+
['preset di configurazione', presetGaps(files)],
|
|
62
|
+
['workflow di GitHub e Dependabot', [...workflowGaps(files, version), ...dependabotGaps(files)]],
|
|
63
|
+
['officina.config.ts', configGaps(config, orphanPatterns(root, config))],
|
|
64
|
+
];
|
|
65
|
+
console.log(`\nofficina doctor ${version} — cosa manca al progetto per prendere tutto da fuori\n`);
|
|
66
|
+
const findings = [];
|
|
67
|
+
for (const [title, gaps] of sections) {
|
|
68
|
+
console.log(gaps.length === 0 ? ` ✓ ${title}` : ` ${title}: ${gaps.length}`);
|
|
69
|
+
const sectionFindings = gaps.map((gap) => ({ ...gap, severity: 'error' }));
|
|
70
|
+
printFindings(sectionFindings, cliOptions([]).format);
|
|
71
|
+
findings.push(...sectionFindings);
|
|
72
|
+
}
|
|
9
73
|
console.log(findings.length === 0
|
|
10
|
-
? ' Il progetto
|
|
11
|
-
: `\n ${findings.length}
|
|
74
|
+
? '\n Il progetto è allineato: metodo, gate, generatori e verifiche arrivano da fuori.\n'
|
|
75
|
+
: `\n ${findings.length} da sistemare.\n`);
|
|
12
76
|
return exitCode(findings, false);
|
|
13
77
|
}
|
|
14
78
|
if (import.meta.main)
|
|
15
|
-
process.exit(main());
|
|
79
|
+
process.exit(await main());
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare function main(args?: string[]): number
|
|
2
|
+
export declare function main(args?: string[]): Promise<number>;
|
|
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { loadConfig } from '../lib/config.js';
|
|
8
9
|
import { lighthouseConfig } from '../lib/lighthouse.js';
|
|
9
10
|
import { auditRoutes, expectedRoutes, readPageFiles } from '../lib/routes.js';
|
|
10
11
|
const PAGES_DIR = 'src/pages';
|
|
@@ -16,22 +17,25 @@ function runLocal() {
|
|
|
16
17
|
const env = { ...process.env, OFFICINA_BIN: process.argv[1] };
|
|
17
18
|
return spawnSync('bash', [script], { stdio: 'inherit', env }).status ?? 1;
|
|
18
19
|
}
|
|
19
|
-
export function main(args = []) {
|
|
20
|
+
export async function main(args = []) {
|
|
20
21
|
if (args.includes('--local'))
|
|
21
22
|
return runLocal();
|
|
23
|
+
const { routes: configured = {} } = await loadConfig(process.cwd());
|
|
22
24
|
const rc = JSON.parse(readFileSync(RC_FILE, 'utf8'));
|
|
23
|
-
const routes = auditRoutes(expectedRoutes(readPageFiles(PAGES_DIR), PAGES_DIR));
|
|
25
|
+
const routes = auditRoutes(expectedRoutes(readPageFiles(PAGES_DIR), PAGES_DIR), configured.representatives);
|
|
24
26
|
const resolved = lighthouseConfig(rc, routes, process.env);
|
|
25
|
-
console.log(`\nLighthouse CI — ${routes.length}
|
|
27
|
+
console.log(`\nLighthouse CI — ${routes.length} rotta/e derivate da ${PAGES_DIR}\n`);
|
|
26
28
|
for (const url of resolved.ci.collect.url ?? [])
|
|
27
29
|
console.log(` ${url}`);
|
|
28
30
|
console.log();
|
|
29
31
|
const dir = mkdtempSync(join(tmpdir(), 'lhci-'));
|
|
30
32
|
const config = join(dir, 'lighthouserc.json');
|
|
31
33
|
writeFileSync(config, JSON.stringify(resolved, null, 2));
|
|
32
|
-
const { status } = spawnSync('pnpm', ['dlx', '@lhci/cli', 'autorun', `--config=${config}`], {
|
|
34
|
+
const { status } = spawnSync('pnpm', ['dlx', '@lhci/cli@0.15.1', 'autorun', `--config=${config}`], {
|
|
35
|
+
stdio: 'inherit',
|
|
36
|
+
});
|
|
33
37
|
rmSync(dir, { recursive: true, force: true });
|
|
34
38
|
return status ?? 1;
|
|
35
39
|
}
|
|
36
40
|
if (import.meta.main)
|
|
37
|
-
process.exit(main(process.argv.slice(2)));
|
|
41
|
+
process.exit(await main(process.argv.slice(2)));
|
package/dist/checks/roadmap.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare function main(args?: string[]): number
|
|
2
|
+
export declare function main(args?: string[]): Promise<number>;
|
package/dist/checks/roadmap.js
CHANGED
|
@@ -3,15 +3,21 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { findingsFor } from '../lib/check-roadmap.js';
|
|
5
5
|
import { cliOptions, exitCode, printFindings } from '../lib/cli.js';
|
|
6
|
+
import { isRequired, loadConfig } from '../lib/config.js';
|
|
6
7
|
const ROADMAP = 'docs/ROADMAP.md';
|
|
7
8
|
// Esporta main ed esegue solo se lanciato direttamente: la copertura non segue i sottoprocessi,
|
|
8
9
|
// quindi un gate provato con `node scripts/...` risulterebbe non testato.
|
|
9
|
-
export function main(args) {
|
|
10
|
+
export async function main(args) {
|
|
10
11
|
const options = cliOptions(args);
|
|
11
|
-
//
|
|
12
|
+
// Che un progetto la roadmap non ce l'abbia è una scelta, e una scelta si dichiara: il silenzio
|
|
13
|
+
// vale `'required'`, e un gate che esce 0 su un file che non c'è non afferma niente.
|
|
12
14
|
if (!existsSync(ROADMAP)) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
if (!isRequired(await loadConfig(process.cwd()), 'roadmap')) {
|
|
16
|
+
console.log(`\ncheck:roadmap — spento da \`features.roadmap: false\`\n`);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
console.error(`\n✗ check:roadmap — ${ROADMAP} non c'è: scrivila, o dichiara \`features.roadmap: false\`\n`);
|
|
20
|
+
return 1;
|
|
15
21
|
}
|
|
16
22
|
const findings = findingsFor(readFileSync(ROADMAP, 'utf8')).map((f) => ({
|
|
17
23
|
path: ROADMAP,
|
|
@@ -30,4 +36,4 @@ export function main(args) {
|
|
|
30
36
|
return exitCode(findings, options.strict);
|
|
31
37
|
}
|
|
32
38
|
if (import.meta.main)
|
|
33
|
-
process.exit(main());
|
|
39
|
+
process.exit(await main());
|
package/dist/checks/smoke.js
CHANGED
|
@@ -10,26 +10,26 @@ function printResults(results) {
|
|
|
10
10
|
if (status === 'pass')
|
|
11
11
|
console.log(`✓ ${check}`);
|
|
12
12
|
else if (status === 'skip')
|
|
13
|
-
console.log(`- ${check} (
|
|
13
|
+
console.log(`- ${check} (saltato: ${detail})`);
|
|
14
14
|
else
|
|
15
15
|
console.error(`✗ ${check} — ${detail}`);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
function printFailures(failures) {
|
|
19
|
-
console.error(`\n✗ ${failures.length}
|
|
19
|
+
console.error(`\n✗ ${failures.length} controllo/i falliti:`);
|
|
20
20
|
console.error(`${failures.map(({ check, detail }) => ` - ${check} — ${detail}`).join('\n')}\n`);
|
|
21
|
-
console.error('
|
|
22
|
-
console.error('Deployments →
|
|
21
|
+
console.error('La produzione è online e rotta. Torna indietro dalla dashboard di Vercel:');
|
|
22
|
+
console.error('Deployments → l’ultimo deployment di produzione sano → Promote to Production.\n');
|
|
23
23
|
}
|
|
24
24
|
export async function main(args = []) {
|
|
25
|
-
const { siteUrl, smoke = {} } = await loadConfig(process.cwd());
|
|
25
|
+
const { siteUrl, smoke = {}, routes = {} } = await loadConfig(process.cwd());
|
|
26
26
|
const [url] = args;
|
|
27
27
|
if (siteUrl === undefined) {
|
|
28
|
-
console.error('\n✗ officina.config.ts
|
|
28
|
+
console.error('\n✗ officina.config.ts non dichiara siteUrl: il controllo dell’host canonico non ha con cosa confrontarsi.\n');
|
|
29
29
|
return 1;
|
|
30
30
|
}
|
|
31
31
|
if (url === undefined && new URL(siteUrl).host === 'example.com') {
|
|
32
|
-
console.error('\n✗ siteUrl
|
|
32
|
+
console.error('\n✗ siteUrl è ancora il segnaposto del template: passa un URL, pnpm smoke:prod https://…\n');
|
|
33
33
|
return 1;
|
|
34
34
|
}
|
|
35
35
|
// L'apice, non l'URL *.vercel.app che stampa `vercel deploy`: lì la regola `has: host` di vercel.json mette noindex.
|
|
@@ -40,8 +40,9 @@ export async function main(args = []) {
|
|
|
40
40
|
siteUrl,
|
|
41
41
|
securityHeaders: smoke.securityHeaders ?? SECURITY_HEADERS,
|
|
42
42
|
};
|
|
43
|
-
const
|
|
44
|
-
|
|
43
|
+
const expected = expectedRoutes(readPageFiles(PAGES_DIR), PAGES_DIR);
|
|
44
|
+
const pages = smokeRoutes(expected, smoke.nonHtmlRoutes, routes.representatives);
|
|
45
|
+
console.log(`\nSmoke di produzione — ${baseUrl}\n`);
|
|
45
46
|
await waitForAlias(context, (ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
46
47
|
const results = await runChecks(context, pages, smoke.checks);
|
|
47
48
|
printResults(results);
|
|
@@ -50,7 +51,7 @@ export async function main(args = []) {
|
|
|
50
51
|
printFailures(failures);
|
|
51
52
|
return 1;
|
|
52
53
|
}
|
|
53
|
-
console.log(`\n✓
|
|
54
|
+
console.log(`\n✓ Tutti i controlli passati su ${baseUrl}.\n`);
|
|
54
55
|
return 0;
|
|
55
56
|
}
|
|
56
57
|
if (import.meta.main)
|
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { cliOptions, exitCode, printFindings } from '../lib/cli.js';
|
|
5
|
-
import { pinFindings } from '../lib/
|
|
6
|
-
|
|
5
|
+
import { pinFindings } from '../lib/pins.js';
|
|
6
|
+
// I pin di ciò che si scarica al volo stanno nei file di questo repository: un progetto prende
|
|
7
|
+
// `vercel` dall'action di deploy e `@lhci/cli` dal gate, e non ne pinna nessuno per conto suo.
|
|
8
|
+
const PINNED = [
|
|
9
|
+
{ package: 'vercel', files: ['actions/deploy/action.yml'] },
|
|
10
|
+
{ package: '@lhci/cli', files: ['src/checks/lighthouse.ts'] },
|
|
11
|
+
];
|
|
7
12
|
// Il documento `latest` porta la sola versione pubblicata; quello del pacchetto intero pesa 8 MB.
|
|
8
|
-
const REGISTRY = 'https://registry.npmjs.org
|
|
13
|
+
const REGISTRY = 'https://registry.npmjs.org';
|
|
9
14
|
const TIMEOUT_MS = 15_000;
|
|
15
|
+
const read = (path) => ({ path, source: existsSync(path) ? readFileSync(path, 'utf8') : '' });
|
|
16
|
+
async function latestOf(pkg) {
|
|
17
|
+
const response = await fetch(`${REGISTRY}/${pkg.replace('/', '%2f')}/latest`, {
|
|
18
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
return undefined;
|
|
22
|
+
const { version } = (await response.json());
|
|
23
|
+
return version;
|
|
24
|
+
}
|
|
10
25
|
export async function main() {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
const findings = [];
|
|
27
|
+
const published = [];
|
|
28
|
+
for (const { package: pkg, files } of PINNED) {
|
|
29
|
+
const latest = await latestOf(pkg);
|
|
30
|
+
if (latest === undefined) {
|
|
31
|
+
findings.push({ path: files[0], message: `registry npm illeggibile per \`${pkg}\``, severity: 'error' });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
published.push(`${pkg}@${latest}`);
|
|
35
|
+
const sites = [read(files[0]), ...files.slice(1).map(read)];
|
|
36
|
+
findings.push(...pinFindings(pkg, latest, sites).map((hit) => ({ ...hit, severity: 'error' })));
|
|
15
37
|
}
|
|
16
|
-
|
|
17
|
-
const findings = pinFindings(readFileSync(WORKFLOW, 'utf8'), version).map((hit) => ({
|
|
18
|
-
path: WORKFLOW,
|
|
19
|
-
...hit,
|
|
20
|
-
severity: 'error',
|
|
21
|
-
}));
|
|
22
|
-
console.log(`\ncheck:vercel-cli — il pin contro la ${version} pubblicata su npm\n`);
|
|
38
|
+
console.log(`\ncheck:vercel-cli — i pin contro quello che npm pubblica (${published.join(', ')})\n`);
|
|
23
39
|
printFindings(findings, cliOptions([]).format);
|
|
24
40
|
console.log(findings.length === 0
|
|
25
|
-
? '
|
|
26
|
-
:
|
|
41
|
+
? ' Ogni pin è esatto e sulla major pubblicata.\n'
|
|
42
|
+
: '\n Alza i pin e allinea la CLI locale alla stessa versione.\n');
|
|
27
43
|
return exitCode(findings, false);
|
|
28
44
|
}
|
|
29
45
|
if (import.meta.main)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Un ancoraggio rotto ha un percorso e un problema. Il pre-volo li impagina nel messaggio che
|
|
2
|
+
// ferma il generatore; `doctor`, che gli stessi controlli li lancia senza generare niente, li
|
|
3
|
+
// rimette in un ritrovamento.
|
|
4
|
+
export class HookPointError extends Error {
|
|
5
|
+
constructor(message, path, problem) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.path = path;
|
|
8
|
+
this.problem = problem;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export const failing = (prefix, guide) => (path, problem) => {
|
|
12
|
+
throw new HookPointError(`${prefix} in ${path}: ${problem} (contract: ${guide})`, path, problem);
|
|
13
|
+
};
|