@elia97/officina 0.2.0 → 0.4.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 +93 -4
- package/dist/bin.d.ts +8 -0
- package/dist/bin.js +12 -3
- package/dist/checks/analytics.d.ts +2 -0
- package/dist/checks/analytics.js +48 -0
- package/dist/checks/bundle.d.ts +2 -0
- package/dist/checks/bundle.js +98 -0
- package/dist/checks/comments.d.ts +2 -0
- package/dist/checks/doctor.d.ts +2 -0
- package/dist/checks/doctor.js +45 -8
- package/dist/checks/language.d.ts +2 -0
- package/dist/checks/lighthouse.d.ts +2 -0
- package/dist/checks/lighthouse.js +37 -0
- package/dist/checks/placeholders.d.ts +2 -0
- package/dist/checks/roadmap.d.ts +2 -0
- package/dist/checks/routes.d.ts +2 -0
- package/dist/checks/smoke.d.ts +2 -0
- package/dist/checks/smoke.js +57 -0
- package/dist/checks/vercel-cli.d.ts +2 -0
- package/dist/checks/vercel-cli.js +8 -6
- package/dist/gen/collection.d.mts +1 -0
- package/dist/gen/component.d.mts +1 -0
- package/dist/gen/icons.d.ts +2 -0
- package/dist/gen/icons.js +27 -0
- package/dist/gen/identifier.d.mts +1 -0
- package/dist/gen/inject-config.d.mts +12 -0
- package/dist/gen/inject-section.d.mts +16 -0
- package/dist/gen/inject-strings.d.mts +8 -0
- package/dist/gen/page.d.mts +10 -0
- package/dist/gen/plopfile.d.mts +2 -0
- package/dist/gen/post-gen.d.mts +1 -0
- package/dist/gen/run.d.ts +2 -0
- package/dist/gen/section-targets.d.mts +23 -0
- package/dist/gen/section.d.mts +1 -0
- package/dist/gen/sectioned-collection.d.mts +26 -0
- package/dist/gen/ts-morph-utils.d.mts +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/lib/alignment.d.ts +23 -0
- package/dist/lib/alignment.js +166 -0
- package/dist/lib/analytics-coverage.d.ts +12 -0
- package/dist/lib/analytics-coverage.js +15 -0
- package/dist/lib/bundle-budget.d.ts +24 -0
- package/dist/lib/bundle-budget.js +50 -0
- package/dist/lib/check-comments.d.ts +10 -0
- package/dist/lib/check-language.d.ts +11 -0
- package/dist/lib/check-placeholders.d.ts +6 -0
- package/dist/lib/check-roadmap.d.ts +30 -0
- package/dist/lib/check-routes.d.ts +6 -0
- package/dist/lib/cli.d.ts +26 -0
- package/dist/lib/comment-syntax.d.ts +11 -0
- package/dist/lib/config.d.ts +25 -0
- package/dist/lib/config.js +22 -0
- package/dist/lib/contract.d.ts +22 -0
- package/dist/lib/git.d.ts +6 -0
- package/dist/lib/gtm-container.d.ts +41 -0
- package/dist/lib/gtm-container.js +145 -0
- package/dist/lib/icon-specs.d.ts +6 -0
- package/dist/lib/icon-specs.js +6 -0
- package/dist/lib/icons.d.ts +6 -0
- package/dist/lib/icons.js +32 -0
- package/dist/lib/language-words.d.ts +2 -0
- package/dist/lib/lighthouse.d.ts +17 -0
- package/dist/lib/lighthouse.js +23 -0
- package/dist/lib/past-tense.d.ts +1 -0
- package/dist/lib/routes.d.ts +31 -0
- package/dist/lib/routes.js +97 -0
- package/dist/lib/smoke-production.d.ts +35 -0
- package/dist/lib/smoke-production.js +132 -0
- package/dist/lib/vercel-cli-pin.d.ts +2 -0
- package/dist/sh/lhci-local.sh +106 -0
- package/package.json +19 -3
- package/presets/biome.base.json +100 -0
- package/presets/lefthook.yml +11 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function pathSegments(plop: any, value: any): any[];
|
|
2
|
+
export declare function pageStrings({ pageKey, pageTitle, dynamic }: {
|
|
3
|
+
dynamic: any;
|
|
4
|
+
pageKey: any;
|
|
5
|
+
pageTitle: any;
|
|
6
|
+
}): {
|
|
7
|
+
key: string;
|
|
8
|
+
value: any;
|
|
9
|
+
}[];
|
|
10
|
+
export default function pageGenerator(plop: any): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function postGenAction(root: any, recoveryHint: any): () => string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare function casings(plop: any, value: any): {
|
|
2
|
+
camel: any;
|
|
3
|
+
kebab: any;
|
|
4
|
+
pascal: any;
|
|
5
|
+
};
|
|
6
|
+
export declare function sectionTargets({ camel, kebab, pascal }: {
|
|
7
|
+
camel: any;
|
|
8
|
+
kebab: any;
|
|
9
|
+
pascal: any;
|
|
10
|
+
}): {
|
|
11
|
+
barrel: string;
|
|
12
|
+
schemaFunction: string;
|
|
13
|
+
dataLayer: string;
|
|
14
|
+
dataFunction: string;
|
|
15
|
+
schemaDir: string;
|
|
16
|
+
contentDir: string;
|
|
17
|
+
componentDir: string;
|
|
18
|
+
importsMarker: string;
|
|
19
|
+
sectionsMarker: string;
|
|
20
|
+
};
|
|
21
|
+
export declare const sectionFiles: (targets: any, kebab: any) => string[];
|
|
22
|
+
export declare function findSectionedPages(root: any, marker: any): string[];
|
|
23
|
+
export declare function validateSectionName(plop: any): (value: any) => string | true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function sectionGenerator(plop: any): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare const forSections: (answers: any) => boolean;
|
|
2
|
+
export declare function sectionedCollectionPrompts(plop: any): ({
|
|
3
|
+
type: string;
|
|
4
|
+
name: string;
|
|
5
|
+
message: string;
|
|
6
|
+
when: typeof forSections;
|
|
7
|
+
validate: (value: any) => string | true;
|
|
8
|
+
default?: never;
|
|
9
|
+
} | {
|
|
10
|
+
validate?: never;
|
|
11
|
+
type: string;
|
|
12
|
+
name: string;
|
|
13
|
+
message: string;
|
|
14
|
+
default: boolean;
|
|
15
|
+
when: typeof forSections;
|
|
16
|
+
})[];
|
|
17
|
+
export declare function sectionedCollectionActions(plop: any, answers: any, root: any): ((() => string) | {
|
|
18
|
+
type: string;
|
|
19
|
+
path: string | undefined;
|
|
20
|
+
templateFile: string;
|
|
21
|
+
data: {
|
|
22
|
+
collection: any;
|
|
23
|
+
pageKey: any;
|
|
24
|
+
};
|
|
25
|
+
})[];
|
|
26
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isNameTaken(sourceFile: any, name: any): any;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { Budget } from './lib/bundle-budget.ts';
|
|
2
|
+
export { CSS_BUDGET_GZIP } from './lib/bundle-budget.ts';
|
|
3
|
+
export type { OfficinaConfig } from './lib/config.ts';
|
|
4
|
+
export { defineConfig } from './lib/config.ts';
|
|
5
|
+
export type { IconSpec } from './lib/icon-specs.ts';
|
|
6
|
+
export { ICON_SPECS } from './lib/icon-specs.ts';
|
|
7
|
+
export type { VerifiedRoute } from './lib/routes.ts';
|
|
8
|
+
export { NON_HTML_ROUTES } from './lib/routes.ts';
|
|
9
|
+
export type { CheckResult, SmokeCheck, SmokeContext } from './lib/smoke-production.ts';
|
|
10
|
+
export { checkBotIdChallenge, checkCanonicalHost, checkPages, checkSecurityHeaders, checkTrailingSlash, DEFAULT_CHECKS, SECURITY_HEADERS, } from './lib/smoke-production.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CSS_BUDGET_GZIP } from './lib/bundle-budget.js';
|
|
2
|
+
export { defineConfig } from './lib/config.js';
|
|
3
|
+
export { ICON_SPECS } from './lib/icon-specs.js';
|
|
4
|
+
export { NON_HTML_ROUTES } from './lib/routes.js';
|
|
5
|
+
export { checkBotIdChallenge, checkCanonicalHost, checkPages, checkSecurityHeaders, checkTrailingSlash, DEFAULT_CHECKS, SECURITY_HEADERS, } from './lib/smoke-production.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OfficinaConfig } from './config.ts';
|
|
2
|
+
import type { ContractGap } from './contract.ts';
|
|
3
|
+
export interface ProjectFiles {
|
|
4
|
+
/** Tracciati e non tracciati, senza gli ignorati: un file locale fuori da git non è un residuo. */
|
|
5
|
+
paths: readonly string[];
|
|
6
|
+
read: (path: string) => string | undefined;
|
|
7
|
+
}
|
|
8
|
+
export interface Manifest {
|
|
9
|
+
scripts?: Record<string, string>;
|
|
10
|
+
dependencies?: Record<string, string>;
|
|
11
|
+
devDependencies?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export declare const EXPECTED_SCRIPTS: Readonly<Record<string, string>>;
|
|
14
|
+
export declare const CI_STEPS: readonly string[];
|
|
15
|
+
export declare function scriptGaps({ scripts }: Manifest): ContractGap[];
|
|
16
|
+
export declare function dependencyGaps({ dependencies, devDependencies }: Manifest): ContractGap[];
|
|
17
|
+
export declare function leftoverGaps({ paths, read }: ProjectFiles): ContractGap[];
|
|
18
|
+
export declare const BIOME_PRESET = "@elia97/officina/biome";
|
|
19
|
+
export declare const LEFTHOOK_PRESET = "node_modules/@elia97/officina/presets/lefthook.yml";
|
|
20
|
+
export declare function presetGaps({ read }: ProjectFiles): ContractGap[];
|
|
21
|
+
export declare function workflowGaps({ read }: ProjectFiles): ContractGap[];
|
|
22
|
+
/** `loaded` è la configurazione letta, l'errore che il suo caricamento ha sollevato, o `undefined` se il file non c'è. */
|
|
23
|
+
export declare function configGaps(loaded: OfficinaConfig | Error | undefined): ContractGap[];
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export const EXPECTED_SCRIPTS = {
|
|
2
|
+
'check:comments': 'officina check comments',
|
|
3
|
+
'check:language': 'officina check language',
|
|
4
|
+
'check:routes': 'officina check routes',
|
|
5
|
+
'check:roadmap': 'officina check roadmap',
|
|
6
|
+
'check:placeholders': 'officina check placeholders',
|
|
7
|
+
'perf:bundle': 'officina check bundle',
|
|
8
|
+
'smoke:prod': 'officina check smoke',
|
|
9
|
+
'analytics:verify': 'officina check analytics',
|
|
10
|
+
lhci: 'officina check lighthouse',
|
|
11
|
+
'lhci:local': 'officina check lighthouse --local',
|
|
12
|
+
doctor: 'officina doctor',
|
|
13
|
+
gen: 'officina gen',
|
|
14
|
+
'gen:section': 'officina gen section',
|
|
15
|
+
'gen:page': 'officina gen page',
|
|
16
|
+
'gen:component': 'officina gen component',
|
|
17
|
+
'gen:collection': 'officina gen collection',
|
|
18
|
+
'gen:icons': 'officina gen icons',
|
|
19
|
+
};
|
|
20
|
+
export const CI_STEPS = [
|
|
21
|
+
'check:language',
|
|
22
|
+
'check:routes',
|
|
23
|
+
'check:roadmap',
|
|
24
|
+
'check:comments',
|
|
25
|
+
'doctor',
|
|
26
|
+
];
|
|
27
|
+
const MANIFEST = 'package.json';
|
|
28
|
+
export function scriptGaps({ scripts = {} }) {
|
|
29
|
+
const gaps = [];
|
|
30
|
+
for (const [name, expected] of Object.entries(EXPECTED_SCRIPTS)) {
|
|
31
|
+
const actual = scripts[name];
|
|
32
|
+
if (actual === undefined)
|
|
33
|
+
gaps.push({ path: MANIFEST, message: `script \`${name}\` assente: atteso \`${expected}\`` });
|
|
34
|
+
else if (!actual.startsWith(expected))
|
|
35
|
+
gaps.push({ path: MANIFEST, message: `script \`${name}\` è \`${actual}\`: atteso \`${expected}\`` });
|
|
36
|
+
}
|
|
37
|
+
const ci = scripts.ci ?? '';
|
|
38
|
+
for (const step of CI_STEPS) {
|
|
39
|
+
if (!ci.includes(`pnpm run ${step}`))
|
|
40
|
+
gaps.push({ path: MANIFEST, message: `script \`ci\` non lancia \`${step}\`` });
|
|
41
|
+
}
|
|
42
|
+
return gaps;
|
|
43
|
+
}
|
|
44
|
+
const RETIRED_DEPENDENCIES = ['plop', 'ts-morph'];
|
|
45
|
+
export function dependencyGaps({ dependencies = {}, devDependencies = {} }) {
|
|
46
|
+
const declared = { ...dependencies, ...devDependencies };
|
|
47
|
+
const gaps = RETIRED_DEPENDENCIES.filter((name) => name in declared).map((name) => ({
|
|
48
|
+
path: MANIFEST,
|
|
49
|
+
message: `\`${name}\` non serve più al progetto: è una dipendenza di officina`,
|
|
50
|
+
}));
|
|
51
|
+
if (!('@elia97/officina' in devDependencies)) {
|
|
52
|
+
gaps.push({ path: MANIFEST, message: '`@elia97/officina` non è fra le devDependencies' });
|
|
53
|
+
}
|
|
54
|
+
return gaps;
|
|
55
|
+
}
|
|
56
|
+
const FROM_PLUGIN = 'arriva dal plugin `metodo`';
|
|
57
|
+
const FROM_PACKAGE = 'arriva da officina';
|
|
58
|
+
const OUT_OF_REPOSITORY = 'sta nella cartella del progetto, fuori dal repository';
|
|
59
|
+
// Una cartella si nomina con la barra finale: corrisponde a qualunque file sotto di lei.
|
|
60
|
+
const LEFTOVERS = [
|
|
61
|
+
{ path: '.claude/agents/', reason: FROM_PLUGIN },
|
|
62
|
+
{ path: '.claude/commands/', reason: FROM_PLUGIN },
|
|
63
|
+
{ path: '.claude/scripts/', reason: FROM_PLUGIN },
|
|
64
|
+
{ path: '.claude/hooks/', reason: FROM_PLUGIN },
|
|
65
|
+
{ path: 'HOW_TO_USE.md', reason: FROM_PLUGIN },
|
|
66
|
+
{ path: 'docs/milestone-templates/', reason: FROM_PLUGIN },
|
|
67
|
+
{ path: 'docs/proposal-templates/', reason: 'i modelli commerciali stanno nel sistema, non nel repository' },
|
|
68
|
+
{ path: 'plopfile.mjs', reason: FROM_PACKAGE },
|
|
69
|
+
{ path: 'scripts/gen/', reason: FROM_PACKAGE },
|
|
70
|
+
{ path: 'scripts/templates/', reason: FROM_PACKAGE },
|
|
71
|
+
{ path: 'scripts/check-comments.mjs', reason: FROM_PACKAGE },
|
|
72
|
+
{ path: 'scripts/check-language.mjs', reason: FROM_PACKAGE },
|
|
73
|
+
{ path: 'scripts/bundle-budget.mjs', reason: FROM_PACKAGE },
|
|
74
|
+
{ path: 'scripts/smoke-production.mjs', reason: FROM_PACKAGE },
|
|
75
|
+
{ path: 'scripts/lighthouse.mjs', reason: FROM_PACKAGE },
|
|
76
|
+
{ path: 'scripts/verify-analytics-coverage.mjs', reason: FROM_PACKAGE },
|
|
77
|
+
{ path: 'scripts/gen-icons.mjs', reason: FROM_PACKAGE },
|
|
78
|
+
{ path: 'scripts/gen-icons.ts', reason: FROM_PACKAGE },
|
|
79
|
+
{ path: 'scripts/lhci-local.sh', reason: FROM_PACKAGE },
|
|
80
|
+
{ path: 'scripts/lib/bundle-budget.ts', reason: FROM_PACKAGE },
|
|
81
|
+
{ path: 'scripts/lib/smoke-production.ts', reason: FROM_PACKAGE },
|
|
82
|
+
{ path: 'scripts/lib/routes.ts', reason: FROM_PACKAGE },
|
|
83
|
+
{ path: 'scripts/lib/check-comments.ts', reason: FROM_PACKAGE },
|
|
84
|
+
{ path: 'scripts/lib/check-language.ts', reason: FROM_PACKAGE },
|
|
85
|
+
{ path: 'scripts/lib/gtm-container.ts', reason: FROM_PACKAGE },
|
|
86
|
+
{ path: 'scripts/lib/analytics-coverage.ts', reason: FROM_PACKAGE },
|
|
87
|
+
{ path: 'scripts/lib/icons.ts', reason: FROM_PACKAGE },
|
|
88
|
+
{ path: '.github/workflows/vercel-cli.yml', reason: "il pin di Vercel sta nell'action di deploy di officina" },
|
|
89
|
+
{ path: 'docs/PROJECT.md', reason: OUT_OF_REPOSITORY },
|
|
90
|
+
{ path: 'docs/DECISIONS.md', reason: OUT_OF_REPOSITORY },
|
|
91
|
+
{ path: 'docs/RISKS.md', reason: OUT_OF_REPOSITORY },
|
|
92
|
+
{ path: 'docs/INPUTS.md', reason: OUT_OF_REPOSITORY },
|
|
93
|
+
{ path: 'docs/TASK-CONTEXT.md', reason: OUT_OF_REPOSITORY },
|
|
94
|
+
{ path: 'docs/ESTIMATE.md', reason: OUT_OF_REPOSITORY },
|
|
95
|
+
];
|
|
96
|
+
const holds = (paths, target) => target.endsWith('/') ? paths.some((path) => path.startsWith(target)) : paths.includes(target);
|
|
97
|
+
export function leftoverGaps({ paths, read }) {
|
|
98
|
+
const gaps = LEFTOVERS.filter(({ path }) => holds(paths, path)).map(({ path, reason }) => ({
|
|
99
|
+
path,
|
|
100
|
+
message: `residuo: ${reason}`,
|
|
101
|
+
}));
|
|
102
|
+
if (read('.claude/settings.json')?.includes('"hooks"')) {
|
|
103
|
+
gaps.push({ path: '.claude/settings.json', message: `blocco \`hooks\` residuo: il guard ${FROM_PLUGIN}` });
|
|
104
|
+
}
|
|
105
|
+
if (read('.mcp.json')?.includes('astro-docs')) {
|
|
106
|
+
gaps.push({ path: '.mcp.json', message: `server \`astro-docs\` residuo: ${FROM_PLUGIN}` });
|
|
107
|
+
}
|
|
108
|
+
return gaps;
|
|
109
|
+
}
|
|
110
|
+
export const BIOME_PRESET = '@elia97/officina/biome';
|
|
111
|
+
export const LEFTHOOK_PRESET = 'node_modules/@elia97/officina/presets/lefthook.yml';
|
|
112
|
+
export function presetGaps({ read }) {
|
|
113
|
+
const gaps = [];
|
|
114
|
+
if (!read('biome.json')?.includes(BIOME_PRESET)) {
|
|
115
|
+
gaps.push({ path: 'biome.json', message: `non estende \`${BIOME_PRESET}\`` });
|
|
116
|
+
}
|
|
117
|
+
if (!read('lefthook.yml')?.includes(LEFTHOOK_PRESET)) {
|
|
118
|
+
gaps.push({ path: 'lefthook.yml', message: `non estende \`${LEFTHOOK_PRESET}\`` });
|
|
119
|
+
}
|
|
120
|
+
return gaps;
|
|
121
|
+
}
|
|
122
|
+
const ACTIONS = [
|
|
123
|
+
{ workflow: '.github/workflows/ci.yml', action: 'ci' },
|
|
124
|
+
{ workflow: '.github/workflows/ci.yml', action: 'review' },
|
|
125
|
+
{ workflow: '.github/workflows/deploy.yml', action: 'deploy' },
|
|
126
|
+
{ workflow: '.github/workflows/lighthouse.yml', action: 'lighthouse' },
|
|
127
|
+
];
|
|
128
|
+
// Un tag di versione o uno SHA intero: `@main` farebbe girare in produzione passi mai collaudati.
|
|
129
|
+
const PINNED_REF = /^(v\d+\.\d+\.\d+|[0-9a-f]{40})$/;
|
|
130
|
+
function actionGap(source, action) {
|
|
131
|
+
const name = `Elia97/officina/actions/${action}`;
|
|
132
|
+
if (source === undefined)
|
|
133
|
+
return `manca: i suoi passi arrivano da \`${name}\``;
|
|
134
|
+
const ref = new RegExp(`${name}@(\\S+)`).exec(source)?.[1];
|
|
135
|
+
if (ref === undefined)
|
|
136
|
+
return `non usa \`${name}\``;
|
|
137
|
+
return PINNED_REF.test(ref)
|
|
138
|
+
? undefined
|
|
139
|
+
: `\`${name}@${ref}\`: il riferimento va fissato a un tag di versione o a uno SHA`;
|
|
140
|
+
}
|
|
141
|
+
export function workflowGaps({ read }) {
|
|
142
|
+
return ACTIONS.flatMap(({ workflow, action }) => {
|
|
143
|
+
const message = actionGap(read(workflow), action);
|
|
144
|
+
return message === undefined ? [] : [{ path: workflow, message }];
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const CONFIG = 'officina.config.ts';
|
|
148
|
+
function siteUrlGap(siteUrl) {
|
|
149
|
+
if (siteUrl === undefined)
|
|
150
|
+
return '`siteUrl` assente: `check smoke` non ha un host canonico';
|
|
151
|
+
if (!URL.canParse(siteUrl))
|
|
152
|
+
return `\`siteUrl\` non è un URL: \`${siteUrl}\``;
|
|
153
|
+
return siteUrl.endsWith('/') ? `\`siteUrl\` finisce con una barra: \`${siteUrl}\`` : undefined;
|
|
154
|
+
}
|
|
155
|
+
/** `loaded` è la configurazione letta, l'errore che il suo caricamento ha sollevato, o `undefined` se il file non c'è. */
|
|
156
|
+
export function configGaps(loaded) {
|
|
157
|
+
if (loaded === undefined)
|
|
158
|
+
return [{ path: CONFIG, message: 'manca: i valori del progetto per officina stanno qui' }];
|
|
159
|
+
if (loaded instanceof Error)
|
|
160
|
+
return [{ path: CONFIG, message: `non si carica: ${loaded.message}` }];
|
|
161
|
+
const messages = [
|
|
162
|
+
siteUrlGap(loaded.siteUrl),
|
|
163
|
+
loaded.icons === undefined ? '`icons.background` assente: `gen icons` non ha un colore di fondo' : undefined,
|
|
164
|
+
];
|
|
165
|
+
return messages.filter((message) => message !== undefined).map((message) => ({ path: CONFIG, message }));
|
|
166
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Trigger } from './gtm-container.ts';
|
|
2
|
+
export type LinkEvent = {
|
|
3
|
+
prefix: string;
|
|
4
|
+
event: string;
|
|
5
|
+
};
|
|
6
|
+
export type Coverage = {
|
|
7
|
+
event: string;
|
|
8
|
+
prefix: string;
|
|
9
|
+
covered: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function extractLinkEvents(source: string): LinkEvent[];
|
|
12
|
+
export declare function coverageOf(events: readonly LinkEvent[], triggers: readonly Trigger[]): Coverage[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const LINK_EVENT = /startsWith\('([^']+)'\)\)\s*return\s*'([^']+)'/g;
|
|
2
|
+
// Letto dal sorgente di src/lib/analytics/link-tracking.ts invece di essere riscritto qui: le
|
|
3
|
+
// due liste divergono il giorno in cui qualcuno aggiunge uno schema, e non fallirebbe niente.
|
|
4
|
+
export function extractLinkEvents(source) {
|
|
5
|
+
return [...source.matchAll(LINK_EVENT)].map(([, prefix, event]) => ({
|
|
6
|
+
/* v8 ignore start -- i due gruppi di LINK_EVENT sono obbligatori: il ripiego serve solo ai tipi */
|
|
7
|
+
prefix: prefix ?? '',
|
|
8
|
+
event: event ?? '',
|
|
9
|
+
/* v8 ignore stop */
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
export function coverageOf(events, triggers) {
|
|
13
|
+
const fired = new Set(triggers.map(({ eventName }) => eventName));
|
|
14
|
+
return events.map(({ event, prefix }) => ({ event, prefix, covered: fired.has(event) }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type Chunk = {
|
|
2
|
+
gzip: number;
|
|
3
|
+
static: Set<string>;
|
|
4
|
+
dynamic: Set<string>;
|
|
5
|
+
};
|
|
6
|
+
export type Budget = {
|
|
7
|
+
label: string;
|
|
8
|
+
matches: (route: string) => boolean;
|
|
9
|
+
maxGzip: number;
|
|
10
|
+
};
|
|
11
|
+
/** Le classi del progetto vengono prima, nell'ordine in cui `officina.config.ts` le dichiara. */
|
|
12
|
+
export declare function budgetFor(route: string, budgets?: readonly Budget[]): Budget;
|
|
13
|
+
/** Rollup racchiude gli specificatori statici con `"` e i dinamici in un template literal. */
|
|
14
|
+
export declare function parseEdges(source: string): Pick<Chunk, 'static' | 'dynamic'>;
|
|
15
|
+
export declare function htmlEntries(html: string): string[];
|
|
16
|
+
export declare function staticClosure(entries: Iterable<string>, chunks: Map<string, Chunk>): Set<string>;
|
|
17
|
+
export declare function deferredClosure(reached: Set<string>, chunks: Map<string, Chunk>): Set<string>;
|
|
18
|
+
export declare const CSS_BUDGET_GZIP: number;
|
|
19
|
+
export type Stylesheet = {
|
|
20
|
+
file: string;
|
|
21
|
+
gzip: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function heaviestStylesheet(sheets: readonly Stylesheet[]): Stylesheet | null;
|
|
24
|
+
export declare function cssBudgetFailure(sheets: readonly Stylesheet[], maxGzip?: number): string | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// 20 KB è ~1,5× la rotta più pesante misurata nello starter (/contatti, 13,4 KB gz).
|
|
2
|
+
const DEFAULT_BUDGET = { label: 'default', matches: () => true, maxGzip: 20 * 1024 };
|
|
3
|
+
/** Le classi del progetto vengono prima, nell'ordine in cui `officina.config.ts` le dichiara. */
|
|
4
|
+
export function budgetFor(route, budgets = []) {
|
|
5
|
+
/* v8 ignore next -- DEFAULT_BUDGET accetta ogni rotta, quindi find() non torna mai undefined */
|
|
6
|
+
return [...budgets, DEFAULT_BUDGET].find((budget) => budget.matches(route)) ?? DEFAULT_BUDGET;
|
|
7
|
+
}
|
|
8
|
+
const captured = (source, pattern, group) => new Set([...source.matchAll(pattern)].map((match) => match[group]).filter((name) => name !== undefined));
|
|
9
|
+
/** Rollup racchiude gli specificatori statici con `"` e i dinamici in un template literal. */
|
|
10
|
+
export function parseEdges(source) {
|
|
11
|
+
return {
|
|
12
|
+
static: captured(source, /(?:from|import)\s*(["'`])\.\/([^"'`]+\.js)\1/g, 2),
|
|
13
|
+
dynamic: captured(source, /import\(\s*(["'`])\.\/([^"'`]+\.js)\1\s*\)/g, 2),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function htmlEntries(html) {
|
|
17
|
+
return [...captured(html, /(?:src|href)="\/_astro\/([^"]+\.js)"/g, 1)];
|
|
18
|
+
}
|
|
19
|
+
export function staticClosure(entries, chunks) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const queue = [...entries];
|
|
22
|
+
while (queue.length > 0) {
|
|
23
|
+
const name = queue.pop();
|
|
24
|
+
if (name === undefined || seen.has(name))
|
|
25
|
+
continue;
|
|
26
|
+
const chunk = chunks.get(name);
|
|
27
|
+
if (chunk === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
seen.add(name);
|
|
30
|
+
queue.push(...chunk.static);
|
|
31
|
+
}
|
|
32
|
+
return seen;
|
|
33
|
+
}
|
|
34
|
+
export function deferredClosure(reached, chunks) {
|
|
35
|
+
const entries = [...reached].flatMap((name) => [...(chunks.get(name)?.dynamic ?? [])]);
|
|
36
|
+
return new Set([...staticClosure(entries, chunks)].filter((name) => !reached.has(name)));
|
|
37
|
+
}
|
|
38
|
+
export const CSS_BUDGET_GZIP = 12 * 1024;
|
|
39
|
+
export function heaviestStylesheet(sheets) {
|
|
40
|
+
return sheets.reduce((worst, sheet) => (worst && worst.gzip >= sheet.gzip ? worst : sheet), null);
|
|
41
|
+
}
|
|
42
|
+
// Astro emette un foglio di stile per gruppo di pagine e una rotta ne collega esattamente uno,
|
|
43
|
+
// quindi la loro somma è fatta di byte che nessun visitatore scarica mai insieme.
|
|
44
|
+
export function cssBudgetFailure(sheets, maxGzip = CSS_BUDGET_GZIP) {
|
|
45
|
+
const heaviest = heaviestStylesheet(sheets);
|
|
46
|
+
if (heaviest === null || heaviest.gzip <= maxGzip)
|
|
47
|
+
return null;
|
|
48
|
+
const over = heaviest.gzip - maxGzip;
|
|
49
|
+
return `CSS ${(heaviest.gzip / 1024).toFixed(1)} KB gz > ${(maxGzip / 1024).toFixed(1)} KB (+${(over / 1024).toFixed(1)} KB) in ${heaviest.file}`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Hit } from './cli.ts';
|
|
2
|
+
import { type Line } from './comment-syntax.ts';
|
|
3
|
+
export interface FileReport {
|
|
4
|
+
findings: Hit[];
|
|
5
|
+
comments: number;
|
|
6
|
+
total: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const SCANNED: RegExp;
|
|
9
|
+
export declare function report(file: string, lines: Line[]): FileReport;
|
|
10
|
+
export declare function isNoisy({ comments, total }: FileReport): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Hit } from './cli.ts';
|
|
2
|
+
export declare function isScanned(path: string): boolean;
|
|
3
|
+
export interface Tally {
|
|
4
|
+
italian: number;
|
|
5
|
+
english: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function tally(prose: string): Tally;
|
|
8
|
+
export type Verdict = 'english' | 'italian' | 'undecided';
|
|
9
|
+
export declare function classify({ italian, english }: Tally, minHits?: number): Verdict;
|
|
10
|
+
/** In un sorgente la prosa sta solo nei commenti: il codice è inglese per costruzione. */
|
|
11
|
+
export declare function findingsFor(path: string, source: string): Hit[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Finding, type Hit } from './cli.ts';
|
|
2
|
+
type EnvHit = Omit<Finding, 'path' | 'severity'>;
|
|
3
|
+
export declare function placeholderSources(): string[];
|
|
4
|
+
export declare function sourceFindings(source: string): Hit[];
|
|
5
|
+
export declare function envFindings(content: string): EnvHit[];
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Hit } from './cli.ts';
|
|
2
|
+
export interface Row {
|
|
3
|
+
line: number;
|
|
4
|
+
number: number;
|
|
5
|
+
name: string;
|
|
6
|
+
phase: number;
|
|
7
|
+
days: number;
|
|
8
|
+
}
|
|
9
|
+
export interface Subtotal {
|
|
10
|
+
line: number;
|
|
11
|
+
label: string;
|
|
12
|
+
phase: number | null;
|
|
13
|
+
days: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Section {
|
|
16
|
+
line: number;
|
|
17
|
+
number: number;
|
|
18
|
+
name: string;
|
|
19
|
+
phase: number;
|
|
20
|
+
days: number;
|
|
21
|
+
}
|
|
22
|
+
/** «1,5» è una giornata e mezza. Una cella vuota non è zero giornate: `Number('')` sarebbe 0. */
|
|
23
|
+
export declare function parseDays(raw: string): number;
|
|
24
|
+
/** Le righe numerate della tabella Status: una per milestone. */
|
|
25
|
+
export declare function statusRows(source: string): Row[];
|
|
26
|
+
/** Le righe di somma: un subtotale per fase, più il totale. */
|
|
27
|
+
export declare function subtotals(source: string): Subtotal[];
|
|
28
|
+
/** Le sezioni `## Milestone N`, con la fase e le giornate della loro testata. */
|
|
29
|
+
export declare function sections(source: string): Section[];
|
|
30
|
+
export declare function findingsFor(source: string): Hit[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Hit } from './cli.ts';
|
|
2
|
+
export declare const isScanned: (path: string) => boolean;
|
|
3
|
+
export declare function normalizeTitle(raw: string): string;
|
|
4
|
+
export declare function headings(source: string): string[];
|
|
5
|
+
export declare function resolveTarget(from: string, cited: string): string;
|
|
6
|
+
export declare function findingsFor(path: string, source: string): Hit[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface Hit {
|
|
2
|
+
line: number;
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
5
|
+
export type Finding = {
|
|
6
|
+
path: string;
|
|
7
|
+
line?: number;
|
|
8
|
+
message: string;
|
|
9
|
+
severity: 'error' | 'warning';
|
|
10
|
+
};
|
|
11
|
+
export declare function cliOptions(args?: string[]): {
|
|
12
|
+
base?: string;
|
|
13
|
+
head?: string;
|
|
14
|
+
strict: boolean;
|
|
15
|
+
format: string;
|
|
16
|
+
diff: boolean;
|
|
17
|
+
};
|
|
18
|
+
export type CliOptions = ReturnType<typeof cliOptions>;
|
|
19
|
+
export declare const isGenerated: (path: string) => boolean;
|
|
20
|
+
export declare function scopedPaths(options: CliOptions, isScanned: (path: string) => boolean): string[];
|
|
21
|
+
export declare function printFindings(findings: Finding[], format: string): void;
|
|
22
|
+
export declare function exitCode(findings: Finding[], strict: boolean): number;
|
|
23
|
+
export declare const readLines: (text: string) => {
|
|
24
|
+
n: number;
|
|
25
|
+
text: string;
|
|
26
|
+
}[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface Line {
|
|
2
|
+
n: number;
|
|
3
|
+
text: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CommentBlock {
|
|
6
|
+
lines: Line[];
|
|
7
|
+
trailing: boolean;
|
|
8
|
+
}
|
|
9
|
+
export type CommentStyle = 'hash' | 'css' | 'slash' | 'astro';
|
|
10
|
+
export declare function styleOf(file: string): CommentStyle;
|
|
11
|
+
export declare function commentBlocks(lines: Line[], style: CommentStyle): CommentBlock[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Budget } from './bundle-budget.ts';
|
|
2
|
+
import type { VerifiedRoute } from './routes.ts';
|
|
3
|
+
import type { SmokeCheck } from './smoke-production.ts';
|
|
4
|
+
export interface OfficinaConfig {
|
|
5
|
+
/** L'apice di produzione, senza barra finale: lo usa `check smoke` quando non riceve un URL. */
|
|
6
|
+
siteUrl?: string;
|
|
7
|
+
icons?: {
|
|
8
|
+
background: string;
|
|
9
|
+
};
|
|
10
|
+
bundle?: {
|
|
11
|
+
budgets?: readonly Budget[];
|
|
12
|
+
cssMaxGzip?: number;
|
|
13
|
+
};
|
|
14
|
+
smoke?: {
|
|
15
|
+
securityHeaders?: Record<string, string | null>;
|
|
16
|
+
nonHtmlRoutes?: readonly VerifiedRoute[];
|
|
17
|
+
checks?: readonly SmokeCheck[];
|
|
18
|
+
};
|
|
19
|
+
analytics?: {
|
|
20
|
+
linkTracking?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare const defineConfig: (config: OfficinaConfig) => OfficinaConfig;
|
|
24
|
+
export declare const findConfigFile: (root: string) => string | undefined;
|
|
25
|
+
export declare function loadConfig(root: string): Promise<OfficinaConfig>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
const CONFIG_FILES = ['officina.config.ts', 'officina.config.mjs', 'officina.config.js'];
|
|
13
|
+
export const defineConfig = (config) => config;
|
|
14
|
+
// Fuori da node_modules Node toglie i tipi da solo: il file del progetto può essere TypeScript.
|
|
15
|
+
export const findConfigFile = (root) => CONFIG_FILES.map((name) => join(root, name)).find((path) => existsSync(path));
|
|
16
|
+
export async function loadConfig(root) {
|
|
17
|
+
const file = findConfigFile(root);
|
|
18
|
+
if (file === undefined)
|
|
19
|
+
return {};
|
|
20
|
+
const module = await import(__rewriteRelativeImportExtension(pathToFileURL(file).href));
|
|
21
|
+
return module.default ?? {};
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type Requirement = {
|
|
2
|
+
kind: 'file';
|
|
3
|
+
path: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'directory';
|
|
7
|
+
path: string;
|
|
8
|
+
extension: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'anchor';
|
|
12
|
+
path: string;
|
|
13
|
+
text: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
export declare const CONTRACT: Requirement[];
|
|
17
|
+
export interface ContractGap {
|
|
18
|
+
path: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function contractGaps(root: string): ContractGap[];
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type Macro = {
|
|
2
|
+
function?: unknown;
|
|
3
|
+
vtp_name?: unknown;
|
|
4
|
+
};
|
|
5
|
+
export type Tag = {
|
|
6
|
+
function?: unknown;
|
|
7
|
+
vtp_eventName?: unknown;
|
|
8
|
+
vtp_tagId?: unknown;
|
|
9
|
+
};
|
|
10
|
+
export type Predicate = {
|
|
11
|
+
function?: unknown;
|
|
12
|
+
arg0?: unknown;
|
|
13
|
+
arg1?: unknown;
|
|
14
|
+
};
|
|
15
|
+
export type ContainerResource = {
|
|
16
|
+
macros: Macro[];
|
|
17
|
+
tags: Tag[];
|
|
18
|
+
predicates: Predicate[];
|
|
19
|
+
rules: unknown[][];
|
|
20
|
+
};
|
|
21
|
+
/** Un tag evento GA4 e le condizioni che lo fanno scattare. `unsupported` porta quello che
|
|
22
|
+
* il lettore non sa interpretare, così un trigger saltato a metà non risulta mai compreso. */
|
|
23
|
+
export type Trigger = {
|
|
24
|
+
eventName: string;
|
|
25
|
+
firesOn: string[];
|
|
26
|
+
selectors: string[];
|
|
27
|
+
urlContains: string[];
|
|
28
|
+
unsupported: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Il container arriva come un solo file JS che contiene un unico letterale `var data = {…}`,
|
|
31
|
+
* e a isolarlo è solo un conteggio di graffe che conosce le stringhe: i valori ne contengono. */
|
|
32
|
+
export declare function parseContainerData(source: string): ContainerResource;
|
|
33
|
+
/** L'id del container come lo serve la produzione, incorporato con `define:vars`. Pubblico per
|
|
34
|
+
* costruzione, ed è l'unica risposta a "quale container gira su questo host". */
|
|
35
|
+
export declare function extractGtmId(html: string): string;
|
|
36
|
+
/** Le proprietà GA4 che il container configura. Senza nemmeno una, ogni tag evento scatta nel
|
|
37
|
+
* vuoto: un elenco vuoto è un fallimento, non una nota. */
|
|
38
|
+
export declare function googleTagIds(container: ContainerResource): string[];
|
|
39
|
+
/** GTM tiene separati tag e condizioni: una regola elenca clausole, con `if` e `unless` che
|
|
40
|
+
* nominano predicati e `add` e `block` che nominano tag. Un trigger per regola che aggiunge il tag. */
|
|
41
|
+
export declare function extractTriggers(container: ContainerResource): Trigger[];
|