@elia97/officina 0.9.1 → 0.11.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 +23 -9
- package/dist/bin.js +2 -1
- package/dist/checks/bundle.js +42 -26
- package/dist/checks/doctor.js +20 -7
- package/dist/checks/lighthouse.js +4 -1
- package/dist/checks/links.d.ts +2 -0
- package/dist/checks/links.js +69 -0
- package/dist/checks/placeholders.js +2 -2
- package/dist/checks/smoke.js +3 -2
- package/dist/gen/hook-points.mjs +0 -3
- package/dist/lib/alignment.d.ts +1 -0
- package/dist/lib/alignment.js +19 -3
- package/dist/lib/bundle-budget.d.ts +1 -2
- package/dist/lib/bundle-budget.js +8 -13
- package/dist/lib/bundle-css.d.ts +16 -0
- package/dist/lib/bundle-css.js +33 -0
- package/dist/lib/bundle-ssr.d.ts +3 -0
- package/dist/lib/bundle-ssr.js +4 -3
- package/dist/lib/check-placeholders.js +4 -3
- package/dist/lib/check-routes.js +87 -18
- package/dist/lib/config-gaps.d.ts +5 -5
- package/dist/lib/config-gaps.js +14 -8
- package/dist/lib/config.d.ts +5 -2
- package/dist/lib/config.js +3 -3
- package/dist/lib/contract.d.ts +1 -0
- package/dist/lib/dependabot.js +19 -2
- package/dist/lib/links.d.ts +30 -0
- package/dist/lib/links.js +101 -0
- package/dist/lib/markdown.d.ts +7 -0
- package/dist/lib/markdown.js +41 -0
- package/dist/lib/past-tense.js +1 -1
- package/dist/lib/routes.d.ts +19 -7
- package/dist/lib/routes.js +34 -10
- package/dist/lib/validate.js +0 -2
- package/dist/lib/vercel-build.d.ts +2 -0
- package/dist/lib/vercel-build.js +41 -8
- package/dist/smoke.js +0 -2
- package/package.json +3 -2
package/dist/lib/routes.js
CHANGED
|
@@ -28,7 +28,7 @@ export function routeOf(htmlPath, dist) {
|
|
|
28
28
|
return route.replace(/\.html$/, '') || '/';
|
|
29
29
|
}
|
|
30
30
|
const SSR_OPT_OUT = /^\s*export\s+const\s+prerender\s*=\s*false\b/m;
|
|
31
|
-
function pageRouteOf(file, pagesDir) {
|
|
31
|
+
export function pageRouteOf(file, pagesDir) {
|
|
32
32
|
return (toPosix(file, pagesDir)
|
|
33
33
|
.replace(/\.astro$/, '')
|
|
34
34
|
.replace(/\/index$/, '') || '/');
|
|
@@ -41,14 +41,14 @@ function segmentPattern(segment) {
|
|
|
41
41
|
const TRAILING_REST_SEGMENT = /\/\[\.\.\.[^/]*\]$/;
|
|
42
42
|
// Anche un segmento rest finale fa match col vuoto: `paginate()` emette la prima pagina come
|
|
43
43
|
// percorso nudo (`/news`, mai `/news/1`), quindi esigere un segmento boccia un archivio di una pagina.
|
|
44
|
-
function routePattern(route) {
|
|
44
|
+
export function routePattern(route) {
|
|
45
45
|
const body = (path) => path.split('/').map(segmentPattern).join('/');
|
|
46
46
|
if (!TRAILING_REST_SEGMENT.test(route))
|
|
47
47
|
return new RegExp(`^${body(route)}$`);
|
|
48
48
|
return new RegExp(`^${body(route.replace(TRAILING_REST_SEGMENT, ''))}(?:/.*)?$`);
|
|
49
49
|
}
|
|
50
|
-
export function expectedRoutes(pages, pagesDir) {
|
|
51
|
-
const expectations = { exact: [], patterns: [], ssr: [] };
|
|
50
|
+
export function expectedRoutes(pages, pagesDir, disabled = []) {
|
|
51
|
+
const expectations = { exact: [], patterns: [], disabled: [], ssr: [] };
|
|
52
52
|
for (const { file, source } of pages) {
|
|
53
53
|
if (SSR_OPT_OUT.test(source)) {
|
|
54
54
|
expectations.ssr.push(file);
|
|
@@ -59,28 +59,52 @@ export function expectedRoutes(pages, pagesDir) {
|
|
|
59
59
|
expectations.exact.push({ route, file });
|
|
60
60
|
continue;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
const target = disabled.includes(route) ? expectations.disabled : expectations.patterns;
|
|
63
|
+
target.push({ pattern: routePattern(route), label: route, file });
|
|
63
64
|
}
|
|
64
65
|
return expectations;
|
|
65
66
|
}
|
|
67
|
+
// Il manifest di Astro nomina una rotta resa a richiesta col suo pattern (`/blog/[slug]`), non coi percorsi che serve.
|
|
68
|
+
const emits = ({ pattern, label }, { html, ssr }) => html.some((route) => pattern.test(route)) || ssr.includes(label);
|
|
66
69
|
/** [HARD] Guardia fail-open: ogni asserzione per rotta itera sulle pagine emesse, quindi una dist vuota non asserisce niente. */
|
|
67
70
|
export function missingRouteFailures(expected, emitted, dist) {
|
|
68
|
-
if (emitted.length === 0) {
|
|
71
|
+
if (emitted.html.length + emitted.ssr.length === 0) {
|
|
69
72
|
return [`${dist} non contiene nessun .html: nessuna rotta misurata, quindi i budget per rotta non affermano niente`];
|
|
70
73
|
}
|
|
71
74
|
const failures = [];
|
|
72
|
-
const routes = new Set(emitted);
|
|
75
|
+
const routes = new Set([...emitted.html, ...emitted.ssr]);
|
|
73
76
|
for (const { route, file } of expected.exact) {
|
|
74
77
|
if (!routes.has(route))
|
|
75
78
|
failures.push(`rotta mancante ${route}: ${file} è prerenderizzata ma non ha emesso HTML`);
|
|
76
79
|
}
|
|
77
|
-
for (const
|
|
78
|
-
if (
|
|
80
|
+
for (const page of expected.patterns) {
|
|
81
|
+
if (emits(page, emitted))
|
|
79
82
|
continue;
|
|
80
|
-
failures.push(`rotta mancante ${label}: ${file} è prerenderizzata ma getStaticPaths non ha emesso niente`);
|
|
83
|
+
failures.push(`rotta mancante ${page.label}: ${page.file} è prerenderizzata ma getStaticPaths non ha emesso niente`);
|
|
81
84
|
}
|
|
82
85
|
return failures;
|
|
83
86
|
}
|
|
87
|
+
const SHOWN = 3;
|
|
88
|
+
export const listed = (routes) => routes.length <= SHOWN ? routes.join(', ') : `${routes.slice(0, SHOWN).join(', ')} e altre ${routes.length - SHOWN}`;
|
|
89
|
+
export function disabledRouteFailures(expected, { html, ssr }) {
|
|
90
|
+
const exact = new Set(expected.exact.map(({ route }) => route));
|
|
91
|
+
const unexplained = html.filter((route) => !exact.has(route) && !expected.patterns.some(({ pattern }) => pattern.test(route)));
|
|
92
|
+
return expected.disabled.flatMap(({ pattern, label }) => {
|
|
93
|
+
if (ssr.includes(label)) {
|
|
94
|
+
return [
|
|
95
|
+
`rotta spenta ${label}: è in \`routes.disabled\`, ma la build la rende a richiesta, e si spegne solo una pagina prerenderizzata`,
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
const routes = unexplained.filter((route) => pattern.test(route)).sort();
|
|
99
|
+
if (routes.length === 0)
|
|
100
|
+
return [];
|
|
101
|
+
return [`rotta spenta ${label}: è in \`routes.disabled\`, ma la build ha emesso ${listed(routes)}`];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function unknownDisabled(expected, disabled = []) {
|
|
105
|
+
const known = new Set(expected.disabled.map(({ label }) => label));
|
|
106
|
+
return disabled.filter((label) => !known.has(label));
|
|
107
|
+
}
|
|
84
108
|
/** I pattern dinamici che `officina.config.ts` non ha ancora mandato nessuno a visitare. */
|
|
85
109
|
export function missingRepresentatives(expected, representatives = {}) {
|
|
86
110
|
return expected.patterns
|
package/dist/lib/validate.js
CHANGED
|
@@ -25,8 +25,6 @@ export const aRecordOf = (item) => (value, path) => isRecord(value)
|
|
|
25
25
|
? Object.entries(value).flatMap(([key, entry]) => item(entry, `${path}.${key}`))
|
|
26
26
|
: [problem(path, 'un oggetto', value)];
|
|
27
27
|
export const oneOf = (expected, allowed) => (value, path) => allowed.includes(value) ? [] : [problem(path, expected, value)];
|
|
28
|
-
// Una voce non dichiarata è un errore: `sitUrl` al posto di `siteUrl` non deve passare in
|
|
29
|
-
// silenzio, che è il modo esatto in cui una configurazione smette di dire qualcosa.
|
|
30
28
|
export const aShape = (fields) => (value, path) => {
|
|
31
29
|
if (!isRecord(value))
|
|
32
30
|
return [problem(path, 'un oggetto', value)];
|
|
@@ -5,7 +5,9 @@ export type ServerFile = {
|
|
|
5
5
|
export type SsrRoute = {
|
|
6
6
|
route: string;
|
|
7
7
|
entries: string[];
|
|
8
|
+
stylesheets: string[] | null;
|
|
8
9
|
unmapped: boolean;
|
|
9
10
|
unknownIslands: string[];
|
|
10
11
|
};
|
|
11
12
|
export declare function ssrRoutes(files: readonly ServerFile[]): SsrRoute[] | null;
|
|
13
|
+
export declare function onDemandPatterns(files: readonly ServerFile[]): RegExp[] | null;
|
package/dist/lib/vercel-build.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
|
+
import { routePattern } from './routes.js';
|
|
2
3
|
const MANIFEST_CALL = 'deserializeManifest({';
|
|
3
|
-
const ASSET = /^_astro\/([
|
|
4
|
+
const ASSET = /^(?:.*\/)?_astro\/([^/?#]+\.js)(?:[?#].*)?$/;
|
|
5
|
+
const STYLESHEET = /^(?:.*\/)?_astro\/([^?#]+\.css)(?:[?#].*)?$/;
|
|
4
6
|
const RENDERER_RUNTIME = /^@astrojs\/[\w-]+\/client\.js$/;
|
|
5
7
|
const COMPONENT_SCRIPT = '?astro&type=script';
|
|
6
8
|
const PAGE_ROUTE = /\["(src\/pages\/[^"]+)", (_page\d+)\]/g;
|
|
@@ -65,7 +67,7 @@ function serverClosure(start, files, manifestFile) {
|
|
|
65
67
|
}
|
|
66
68
|
return [...reached.values()];
|
|
67
69
|
}
|
|
68
|
-
const assetOf = (value) => value?.match(
|
|
70
|
+
const assetOf = (value, pattern = ASSET) => value?.match(pattern)?.[1];
|
|
69
71
|
function clientModules(manifest) {
|
|
70
72
|
const inlined = new Set((manifest.inlinedScripts ?? []).map(([specifier]) => specifier));
|
|
71
73
|
const modules = new Map();
|
|
@@ -87,20 +89,50 @@ function routeEntries(page, texts, modules) {
|
|
|
87
89
|
const unknownIslands = (texts, modules) => [...new Set(texts.flatMap((text) => [...text.matchAll(ISLAND_PATH)].map(([, path]) => path)))]
|
|
88
90
|
.filter((path) => !modules.has(path))
|
|
89
91
|
.sort();
|
|
90
|
-
|
|
92
|
+
const routeStylesheets = (styles) => !Array.isArray(styles)
|
|
93
|
+
? null
|
|
94
|
+
: [
|
|
95
|
+
...new Set(styles
|
|
96
|
+
.filter((style) => style.type === 'external')
|
|
97
|
+
.map((style) => assetOf(style.src, STYLESHEET))
|
|
98
|
+
.filter((name) => name !== undefined)),
|
|
99
|
+
].sort();
|
|
100
|
+
function ssrPage({ scripts = [], styles, routeData }) {
|
|
91
101
|
if (routeData?.type !== 'page' || routeData.origin !== 'project' || routeData.prerender !== false)
|
|
92
102
|
return [];
|
|
93
103
|
const { route, component } = routeData;
|
|
94
|
-
return typeof route === 'string' && typeof component === 'string' ? [{ scripts, route, component }] : [];
|
|
104
|
+
return typeof route === 'string' && typeof component === 'string' ? [{ scripts, styles, route, component }] : [];
|
|
95
105
|
}
|
|
96
|
-
|
|
97
|
-
for (const
|
|
98
|
-
const manifest = parseManifest(
|
|
106
|
+
function firstManifest(files) {
|
|
107
|
+
for (const host of files) {
|
|
108
|
+
const manifest = parseManifest(host.source);
|
|
99
109
|
if (manifest !== null)
|
|
100
|
-
return
|
|
110
|
+
return { host, manifest };
|
|
101
111
|
}
|
|
102
112
|
return null;
|
|
103
113
|
}
|
|
114
|
+
export function ssrRoutes(files) {
|
|
115
|
+
const found = firstManifest(files);
|
|
116
|
+
return found === null ? null : routesOf(found.host, found.manifest, files);
|
|
117
|
+
}
|
|
118
|
+
// `routePattern` non legge i segmenti misti (`[slug].md`, `post-[id]`), che il pattern del manifest di Astro sì.
|
|
119
|
+
function astroPattern(route, pattern) {
|
|
120
|
+
if (typeof pattern === 'string') {
|
|
121
|
+
try {
|
|
122
|
+
return new RegExp(pattern);
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
}
|
|
126
|
+
return routePattern(route);
|
|
127
|
+
}
|
|
128
|
+
export function onDemandPatterns(files) {
|
|
129
|
+
const found = firstManifest(files);
|
|
130
|
+
if (found === null)
|
|
131
|
+
return null;
|
|
132
|
+
return (found.manifest.routes ?? []).flatMap(({ routeData }) => routeData?.prerender === false && typeof routeData.route === 'string'
|
|
133
|
+
? [astroPattern(routeData.route, routeData.pattern)]
|
|
134
|
+
: []);
|
|
135
|
+
}
|
|
104
136
|
function routesOf(host, manifest, files) {
|
|
105
137
|
const sources = new Map(files.map((file) => [file.path, file.source]));
|
|
106
138
|
const chunks = pageChunks(host);
|
|
@@ -111,6 +143,7 @@ function routesOf(host, manifest, files) {
|
|
|
111
143
|
return {
|
|
112
144
|
route: page.route,
|
|
113
145
|
entries: routeEntries(page, texts, modules),
|
|
146
|
+
stylesheets: routeStylesheets(page.styles),
|
|
114
147
|
unmapped: chunk === undefined,
|
|
115
148
|
unknownIslands: unknownIslands(texts, modules),
|
|
116
149
|
};
|
package/dist/smoke.js
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
// I pezzi con cui un progetto compone i propri controlli di smoke: `smoke.checks` sostituisce la
|
|
2
|
-
// lista del pacchetto, quindi chi ne toglie uno ricompone la lista da qui.
|
|
3
1
|
export { NON_HTML_ROUTES } from './lib/routes.js';
|
|
4
2
|
export { checkBotIdChallenge, checkCanonicalHost, checkPages, checkSecurityHeaders, checkTrailingSlash, DEFAULT_CHECKS, SECURITY_HEADERS, } from './lib/smoke-production.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elia97/officina",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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",
|
|
@@ -48,9 +48,10 @@
|
|
|
48
48
|
"typecheck": "tsc --noEmit",
|
|
49
49
|
"test": "vitest run --coverage",
|
|
50
50
|
"format": "biome check --write .",
|
|
51
|
+
"check:comments": "node src/bin.ts check comments --strict",
|
|
51
52
|
"check:deadcode": "fallow dead-code",
|
|
52
53
|
"check:health": "fallow health --complexity --coverage coverage/coverage-final.json",
|
|
53
|
-
"ci": "biome ci --error-on-warnings . && pnpm run typecheck && pnpm run check:deadcode && pnpm run test && pnpm run check:health",
|
|
54
|
+
"ci": "biome ci --error-on-warnings . && pnpm run typecheck && pnpm run check:deadcode && pnpm run check:comments && pnpm run test && pnpm run check:health",
|
|
54
55
|
"test:pack": "bash scripts/pack-test.sh"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|