@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.
@@ -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
- expectations.patterns.push({ pattern: routePattern(route), label: route, file });
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 { pattern, label, file } of expected.patterns) {
78
- if (emitted.some((route) => pattern.test(route)))
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
@@ -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;
@@ -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\/([^/]+\.js)$/;
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(ASSET)?.[1];
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
- function ssrPage({ scripts = [], routeData }) {
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
- export function ssrRoutes(files) {
97
- for (const file of files) {
98
- const manifest = parseManifest(file.source);
106
+ function firstManifest(files) {
107
+ for (const host of files) {
108
+ const manifest = parseManifest(host.source);
99
109
  if (manifest !== null)
100
- return routesOf(file, manifest, files);
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.9.1",
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": {