@elia97/officina 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,6 +79,8 @@ La radice del pacchetto esporta `defineConfig` e i tipi; i pezzi del motore stan
79
79
 
80
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
81
 
82
+ `placeholders` dice a `check placeholders` cosa guardare: `sources` sono i file da scandire, `contactEnvKeys` le variabili che `--env` pretende. Una lista dichiarata **sostituisce** quella del pacchetto, dizionari compresi: senza dichiarazione i sorgenti sono `src/lib/site.ts`, `src/lib/company.ts` e i dizionari di `src/i18n/strings`, quando la cartella esiste. Serve ai progetti che non nascono dallo scaffold: se partita IVA, recapiti e destinatario dei form stanno nel contenuto invece che in un modulo, i sorgenti del template non sono i suoi, e crearli per far passare il gate terrebbe gli stessi dati in due sedi. Un sorgente elencato che non esiste è un ritrovamento con il suo percorso, non un errore che ferma il comando.
83
+
82
84
  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
85
 
84
86
  `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.
@@ -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>;
@@ -2,30 +2,36 @@
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import process from 'node:process';
4
4
  import { parseArgs } from 'node:util';
5
- import { envFindings, placeholderSources, sourceFindings } from '../lib/check-placeholders.js';
5
+ import { contactEnvKeys, envFindings, placeholderSources, sourceFindings } from '../lib/check-placeholders.js';
6
6
  import { cliOptions, exitCode, printFindings } from '../lib/cli.js';
7
+ import { loadConfig } from '../lib/config.js';
8
+ const MISSING_SOURCE = 'sorgente assente: `placeholders.sources` in officina.config.ts dice quali file guardare';
7
9
  function sourceScan(paths) {
8
- return paths.flatMap((path) => sourceFindings(readFileSync(path, 'utf8')).map((hit) => ({ path, ...hit, severity: 'error' })));
10
+ return paths.flatMap((path) => {
11
+ if (!existsSync(path))
12
+ return [{ path, message: MISSING_SOURCE, severity: 'error' }];
13
+ return sourceFindings(readFileSync(path, 'utf8')).map((hit) => ({ path, ...hit, severity: 'error' }));
14
+ });
9
15
  }
10
- function envScan(path) {
16
+ function envScan(path, keys) {
11
17
  if (!existsSync(path))
12
18
  return [{ path, message: "file dell'ambiente assente: lo scrive vercel pull", severity: 'error' }];
13
- return envFindings(readFileSync(path, 'utf8')).map((hit) => ({ path, ...hit, severity: 'error' }));
19
+ return envFindings(readFileSync(path, 'utf8'), keys).map((hit) => ({ path, ...hit, severity: 'error' }));
14
20
  }
15
- function scan(env) {
21
+ function scan(env, config) {
16
22
  if (env !== undefined) {
17
23
  return {
18
24
  scope: `ambiente in ${env}`,
19
- findings: envScan(env),
25
+ findings: envScan(env, contactEnvKeys(config)),
20
26
  fix: 'docs/guides/deploy-ops.md § Checklist per il go-live',
21
27
  };
22
28
  }
23
- const paths = placeholderSources();
29
+ const paths = placeholderSources(config);
24
30
  return { scope: `${paths.length} sorgenti`, findings: sourceScan(paths), fix: 'README.md § Cosa tocca il rebranding' };
25
31
  }
26
- export function main(args = process.argv.slice(2)) {
32
+ export async function main(args = process.argv.slice(2)) {
27
33
  const { env } = parseArgs({ args, options: { env: { type: 'string' } } }).values;
28
- const { scope, findings, fix } = scan(env);
34
+ const { scope, findings, fix } = scan(env, await loadConfig(process.cwd()));
29
35
  console.log(`\ncheck:placeholders — ${scope}\n`);
30
36
  printFindings(findings, cliOptions([]).format);
31
37
  if (findings.length === 0) {
@@ -37,4 +43,4 @@ export function main(args = process.argv.slice(2)) {
37
43
  return exitCode(findings, false);
38
44
  }
39
45
  if (import.meta.main)
40
- process.exit(main());
46
+ process.exit(await main());
@@ -0,0 +1,9 @@
1
+ import { type Hit } from './cli.ts';
2
+ export interface Manifest {
3
+ path: string;
4
+ source: string;
5
+ }
6
+ export interface ManifestHit extends Hit {
7
+ path: string;
8
+ }
9
+ export declare function manifestFindings(manifests: readonly Manifest[]): ManifestHit[];
@@ -0,0 +1,25 @@
1
+ import { readLines } from './cli.js';
2
+ import { isLive } from './workflows.js';
3
+ const EXPRESSION = /\$\{\{[^}]*\}\}/g;
4
+ const DESCRIPTION = /^\s*description:/;
5
+ const FORBIDDEN = ['secrets', 'needs', 'matrix'];
6
+ const contextIn = (expression) => FORBIDDEN.find((name) => new RegExp(`(^|[^\\w.])${name}\\.`).test(expression));
7
+ function lineFindings(text) {
8
+ const expressions = [...text.matchAll(EXPRESSION)];
9
+ if (expressions.length === 0)
10
+ return [];
11
+ if (DESCRIPTION.test(text)) {
12
+ return ['`description` non ammette espressioni: il runner la valuta e il manifesto non si carica'];
13
+ }
14
+ return expressions.flatMap(([expression]) => {
15
+ const context = contextIn(expression);
16
+ return context === undefined ? [] : [`\`${context}\` non è un contesto di una composite action: passalo come input`];
17
+ });
18
+ }
19
+ // Il runner valuta le espressioni del manifesto prima di eseguire qualunque passo: un contesto che una
20
+ // composite action non conosce non è un valore vuoto, è un'action che non si carica.
21
+ export function manifestFindings(manifests) {
22
+ return manifests.flatMap(({ path, source }) => readLines(source)
23
+ .filter(({ text }) => isLive(text))
24
+ .flatMap(({ n, text }) => lineFindings(text).map((message) => ({ path, line: n, message }))));
25
+ }
@@ -1,6 +1,8 @@
1
1
  import { type Finding, type Hit } from './cli.ts';
2
+ import type { OfficinaConfig } from './config.ts';
2
3
  type EnvHit = Omit<Finding, 'path' | 'severity'>;
3
- export declare function placeholderSources(): string[];
4
+ export declare function placeholderSources(config?: OfficinaConfig): string[];
5
+ export declare const contactEnvKeys: (config?: OfficinaConfig) => readonly string[];
4
6
  export declare function sourceFindings(source: string): Hit[];
5
- export declare function envFindings(content: string): EnvHit[];
7
+ export declare function envFindings(content: string, keys: readonly string[]): EnvHit[];
6
8
  export {};
@@ -1,8 +1,9 @@
1
- import { readdirSync } from 'node:fs';
1
+ import { existsSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { parseEnv } from 'node:util';
4
4
  import { readLines } from './cli.js';
5
5
  const DICTIONARIES = 'src/i18n/strings';
6
+ const DEFAULT_SOURCES = ['src/lib/site.ts', 'src/lib/company.ts'];
6
7
  const CONTACT_ENV_KEYS = ['CONTACT_FROM_EMAIL', 'CONTACT_FROM_NAME', 'CONTACT_TO_EMAIL'];
7
8
  const TOKEN = /<[A-Z][A-Z_]{2,}>/;
8
9
  const DOMAIN = /\bexample\.com\b/;
@@ -10,13 +11,19 @@ const STRING_LITERAL = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g;
10
11
  const COMMENT_ONLY = /^\s*(?:\/\/|\/\*|\*)/;
11
12
  const ZERO_POSTAL_CODE = /\bpostalCode:\s*(['"])00000\1/;
12
13
  const EMPTY_HREF = /\bhref:\s*(['"])#\1/;
13
- export function placeholderSources() {
14
- const dictionaries = readdirSync(DICTIONARIES)
14
+ const dictionaries = () => existsSync(DICTIONARIES)
15
+ ? readdirSync(DICTIONARIES)
15
16
  .filter((name) => name.endsWith('.ts'))
16
17
  .sort()
17
- .map((name) => join(DICTIONARIES, name));
18
- return ['src/lib/site.ts', 'src/lib/company.ts', ...dictionaries];
18
+ .map((name) => join(DICTIONARIES, name))
19
+ : [];
20
+ export function placeholderSources(config = {}) {
21
+ const declared = config.placeholders?.sources;
22
+ if (declared !== undefined)
23
+ return [...declared];
24
+ return [...DEFAULT_SOURCES, ...dictionaries()];
19
25
  }
26
+ export const contactEnvKeys = (config = {}) => config.placeholders?.contactEnvKeys ?? CONTACT_ENV_KEYS;
20
27
  function literalFindings(value) {
21
28
  const messages = [];
22
29
  const token = value.match(TOKEN);
@@ -40,10 +47,10 @@ export function sourceFindings(source) {
40
47
  return messages.map((message) => ({ line: n, message }));
41
48
  });
42
49
  }
43
- export function envFindings(content) {
50
+ export function envFindings(content, keys) {
44
51
  const env = parseEnv(content);
45
52
  const lines = readLines(content);
46
- return CONTACT_ENV_KEYS.flatMap((key) => {
53
+ return keys.flatMap((key) => {
47
54
  const value = env[key]?.trim() ?? '';
48
55
  const declaration = lines.find(({ text }) => new RegExp(`^\\s*${key}\\s*=`).test(text));
49
56
  const at = declaration ? { line: declaration.n } : {};
@@ -26,6 +26,10 @@ export interface OfficinaConfig {
26
26
  analytics?: FeatureSetting;
27
27
  roadmap?: FeatureSetting;
28
28
  };
29
+ placeholders?: {
30
+ sources?: readonly string[];
31
+ contactEnvKeys?: readonly string[];
32
+ };
29
33
  /** Per ogni pattern dinamico di `src/pages`, un percorso vero che smoke e Lighthouse visitano. */
30
34
  routes?: {
31
35
  representatives?: Representatives;
@@ -27,6 +27,7 @@ const SHAPE = aShape({
27
27
  }),
28
28
  analytics: aShape({ linkTracking: aString }),
29
29
  features: aShape({ analytics: feature, roadmap: feature }),
30
+ placeholders: aShape({ sources: anArrayOf(aString), contactEnvKeys: anArrayOf(aString) }),
30
31
  routes: aShape({ representatives: aRecordOf(aString) }),
31
32
  });
32
33
  const CONFIG_FILES = ['officina.config.ts', 'officina.config.mjs', 'officina.config.js'];
@@ -1,5 +1,6 @@
1
1
  import type { ProjectFiles } from './alignment.ts';
2
2
  import type { ContractGap } from './contract.ts';
3
+ export declare const isLive: (line: string) => boolean;
3
4
  export declare const mentions: (source: string | undefined, needle: string) => boolean;
4
5
  /** Le action del progetto devono stare alla stessa versione del pacchetto installato. */
5
6
  export declare function workflowGaps({ read }: ProjectFiles, version: string): ContractGap[];
@@ -1,7 +1,7 @@
1
- // I workflow restano una ricerca testuale — non c'è un parser YAML nel pacchetto — ma una riga
2
- // commentata non deve soddisfare niente: è il modo più facile per avere un progetto «a posto»
3
- // che in CI non lancia nessuno di questi passi.
4
- const liveLines = (source) => source.split(/\r?\n/).filter((line) => !line.trimStart().startsWith('#'));
1
+ // Niente parser YAML nel pacchetto: la ricerca è testuale. Una riga commentata non soddisfa nessun
2
+ // controllo, altrimenti basta commentare un passo per avere un progetto «a posto» che non lo lancia.
3
+ export const isLive = (line) => !line.trimStart().startsWith('#');
4
+ const liveLines = (source) => source.split(/\r?\n/).filter(isLive);
5
5
  export const mentions = (source, needle) => source !== undefined && liveLines(source).some((line) => line.includes(needle));
6
6
  const ACTIONS = [
7
7
  { workflow: '.github/workflows/ci.yml', action: 'ci' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elia97/officina",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Gli strumenti dei progetti nati dai template di Elia: gate di qualità, generatori e verifiche di build e di produzione.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@biomejs/biome": "2.5.10",
58
- "@types/node": "^24.13.5",
58
+ "@types/node": "^26.5.1",
59
59
  "@vitest/coverage-v8": "^4.1.11",
60
60
  "fallow": "^3.26.0",
61
61
  "typescript": "^7.0.2",