@elia97/officina 0.1.0 → 0.3.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 +57 -3
- package/dist/bin.d.ts +8 -0
- package/dist/bin.js +28 -6
- 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 +15 -0
- 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/gen/collection.d.mts +1 -0
- package/dist/gen/collection.mjs +76 -0
- package/dist/gen/component.d.mts +1 -0
- package/dist/gen/component.mjs +41 -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/identifier.mjs +52 -0
- package/dist/gen/inject-config.d.mts +12 -0
- package/dist/gen/inject-config.mjs +73 -0
- package/dist/gen/inject-section.d.mts +16 -0
- package/dist/gen/inject-section.mjs +140 -0
- package/dist/gen/inject-strings.d.mts +8 -0
- package/dist/gen/inject-strings.mjs +56 -0
- package/dist/gen/page.d.mts +10 -0
- package/dist/gen/page.mjs +68 -0
- package/dist/gen/plopfile.d.mts +2 -0
- package/dist/gen/plopfile.mjs +30 -0
- package/dist/gen/post-gen.d.mts +1 -0
- package/dist/gen/post-gen.mjs +16 -0
- package/dist/gen/run.d.ts +2 -0
- package/dist/gen/run.js +17 -0
- package/dist/gen/section-targets.d.mts +23 -0
- package/dist/gen/section-targets.mjs +46 -0
- package/dist/gen/section.d.mts +1 -0
- package/dist/gen/section.mjs +68 -0
- package/dist/gen/sectioned-collection.d.mts +26 -0
- package/dist/gen/sectioned-collection.mjs +81 -0
- package/dist/gen/templates/collection/data.yml.hbs +2 -0
- package/dist/gen/templates/collection/doc.md.hbs +8 -0
- package/dist/gen/templates/collection/schema.ts.hbs +11 -0
- package/dist/gen/templates/component/component.astro.hbs +21 -0
- package/dist/gen/templates/page/dynamic.astro.hbs +24 -0
- package/dist/gen/templates/page/static.astro.hbs +20 -0
- package/dist/gen/templates/section/component.astro.hbs +45 -0
- package/dist/gen/templates/section/content.yml.hbs +10 -0
- package/dist/gen/templates/section/schema.ts.hbs +20 -0
- package/dist/gen/templates/sections/data-layer.ts.hbs +13 -0
- package/dist/gen/templates/sections/index.ts.hbs +12 -0
- package/dist/gen/templates/sections/page.astro.hbs +30 -0
- package/dist/gen/ts-morph-utils.d.mts +1 -0
- package/dist/gen/ts-morph-utils.mjs +8 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -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 +24 -0
- package/dist/lib/config.js +21 -0
- package/dist/lib/contract.d.ts +22 -0
- package/dist/lib/contract.js +59 -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 +21 -3
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const pass = (check) => ({ check, status: 'pass' });
|
|
2
|
+
const fail = (check, detail) => ({ check, status: 'fail', detail });
|
|
3
|
+
const skip = (check, detail) => ({ check, status: 'skip', detail });
|
|
4
|
+
const messageOf = (error) => (error instanceof Error ? error.message : String(error));
|
|
5
|
+
// Dalla regola globale `/(.*)` di vercel.json. `null` = verifica solo la presenza; src/vercel-headers.test.ts fissa i valori.
|
|
6
|
+
export const SECURITY_HEADERS = {
|
|
7
|
+
'content-security-policy': null,
|
|
8
|
+
'strict-transport-security': null,
|
|
9
|
+
'x-content-type-options': 'nosniff',
|
|
10
|
+
'x-frame-options': 'DENY',
|
|
11
|
+
'referrer-policy': null,
|
|
12
|
+
'permissions-policy': null,
|
|
13
|
+
};
|
|
14
|
+
/** Deve corrispondere al `source` del rewrite in vercel.json. */
|
|
15
|
+
export const BOTID_CHALLENGE = '/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/a-4-a/c.js';
|
|
16
|
+
/** L'alias di produzione di Vercel impiega un attimo a puntare al deployment appena caricato. */
|
|
17
|
+
export async function waitForAlias({ get, baseUrl }, sleep, attempts = 5) {
|
|
18
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
19
|
+
try {
|
|
20
|
+
if ((await get(baseUrl)).ok)
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
if (attempt < attempts)
|
|
25
|
+
await sleep(attempt * 2000);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function checkPages({ get, baseUrl }, pages) {
|
|
29
|
+
const results = [];
|
|
30
|
+
for (const { path, type } of pages) {
|
|
31
|
+
const check = `GET ${path}`;
|
|
32
|
+
try {
|
|
33
|
+
const response = await get(`${baseUrl}${path}`);
|
|
34
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
35
|
+
if (response.status !== 200)
|
|
36
|
+
results.push(fail(check, `expected 200, got ${response.status}`));
|
|
37
|
+
else if (!contentType.includes(type))
|
|
38
|
+
results.push(fail(check, `expected a ${type} content-type, got "${contentType}"`));
|
|
39
|
+
else
|
|
40
|
+
results.push(pass(check));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
results.push(fail(check, messageOf(error)));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
export async function checkSecurityHeaders({ get, baseUrl, securityHeaders }) {
|
|
49
|
+
let response;
|
|
50
|
+
try {
|
|
51
|
+
response = await get(baseUrl);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return [fail('security headers', messageOf(error))];
|
|
55
|
+
}
|
|
56
|
+
const results = Object.entries(securityHeaders).map(([header, expected]) => {
|
|
57
|
+
const check = `header ${header}`;
|
|
58
|
+
const value = response.headers.get(header);
|
|
59
|
+
if (value === null)
|
|
60
|
+
return fail(check, 'missing');
|
|
61
|
+
if (expected !== null && value !== expected)
|
|
62
|
+
return fail(check, `expected "${expected}", got "${value}"`);
|
|
63
|
+
return pass(check);
|
|
64
|
+
});
|
|
65
|
+
// Il noindex `has: host = *.vercel.app` di vercel.json applicato al dominio vero farebbe sparire il sito da ogni indice.
|
|
66
|
+
const check = 'no x-robots-tag on the production host';
|
|
67
|
+
const robots = response.headers.get('x-robots-tag');
|
|
68
|
+
results.push(robots === null ? pass(check) : fail(check, `present on ${baseUrl}: "${robots}"`));
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
export async function checkBotIdChallenge({ get, baseUrl }) {
|
|
72
|
+
const check = 'BotID challenge proxied same-origin';
|
|
73
|
+
try {
|
|
74
|
+
const response = await get(`${baseUrl}${BOTID_CHALLENGE}`);
|
|
75
|
+
if (response.status !== 200)
|
|
76
|
+
return [fail(check, `expected 200 from the rewrite, got ${response.status}`)];
|
|
77
|
+
return [pass(check)];
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return [fail(check, messageOf(error))];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Il 308 da www all'apice, da vercel.json: dipende dal DNS e dal dominio del progetto Vercel, non dal deployment. */
|
|
84
|
+
export async function checkCanonicalHost({ get, baseUrl, siteUrl }) {
|
|
85
|
+
const check = 'www → apex 308';
|
|
86
|
+
if (baseUrl !== siteUrl)
|
|
87
|
+
return [skip(check, `base URL is not ${siteUrl}`)];
|
|
88
|
+
try {
|
|
89
|
+
const response = await get(`https://www.${new URL(siteUrl).host}/`);
|
|
90
|
+
const location = response.headers.get('location') ?? '';
|
|
91
|
+
if (response.status !== 308)
|
|
92
|
+
return [fail(check, `expected 308, got ${response.status}`)];
|
|
93
|
+
if (!location.startsWith(siteUrl))
|
|
94
|
+
return [fail(check, `location "${location}" does not point at ${siteUrl}`)];
|
|
95
|
+
return [pass(check)];
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return [fail(check, messageOf(error))];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export async function checkTrailingSlash({ get, baseUrl }, pages) {
|
|
102
|
+
const page = pages.find(({ path, type }) => type === 'text/html' && path !== '/');
|
|
103
|
+
const check = 'trailing slash → 308';
|
|
104
|
+
if (page === undefined)
|
|
105
|
+
return [skip(check, 'no HTML page other than / to probe')];
|
|
106
|
+
try {
|
|
107
|
+
const response = await get(`${baseUrl}${page.path}/`);
|
|
108
|
+
if (response.status !== 308)
|
|
109
|
+
return [fail(check, `expected 308 on ${page.path}/, got ${response.status}`)];
|
|
110
|
+
const location = response.headers.get('location') ?? '';
|
|
111
|
+
if (!location.endsWith(page.path))
|
|
112
|
+
return [fail(check, `location "${location}" does not point at ${page.path}`)];
|
|
113
|
+
return [pass(check)];
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return [fail(check, messageOf(error))];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export const DEFAULT_CHECKS = [
|
|
120
|
+
checkPages,
|
|
121
|
+
checkSecurityHeaders,
|
|
122
|
+
checkBotIdChallenge,
|
|
123
|
+
checkCanonicalHost,
|
|
124
|
+
checkTrailingSlash,
|
|
125
|
+
];
|
|
126
|
+
/** In sequenza, non in parallelo: l'ordine dei risultati è quello dei controlli. */
|
|
127
|
+
export async function runChecks(context, pages, checks = DEFAULT_CHECKS) {
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const check of checks)
|
|
130
|
+
results.push(...(await check(context, pages)));
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
PORT="${LHCI_PORT:-4399}"
|
|
5
|
+
OUT_DIR='dist/lh-prod'
|
|
6
|
+
REPORT_DIR="${LHCI_REPORT_DIR:-.lighthouseci/local}"
|
|
7
|
+
|
|
8
|
+
export TMPDIR="${TMPDIR:-/tmp}"
|
|
9
|
+
|
|
10
|
+
detect_chrome() {
|
|
11
|
+
local candidate
|
|
12
|
+
for candidate in \
|
|
13
|
+
"${CHROME_PATH:-}" \
|
|
14
|
+
$(ls -d "$HOME"/.cache/ms-playwright/chromium-*/chrome-linux/chrome 2>/dev/null | sort -V -r) \
|
|
15
|
+
$(ls -d "$HOME"/.cache/puppeteer/chrome/*/chrome-linux*/chrome 2>/dev/null | sort -V -r) \
|
|
16
|
+
/usr/bin/google-chrome /usr/bin/chromium /usr/bin/chromium-browser
|
|
17
|
+
do
|
|
18
|
+
[[ -n "${candidate}" && -x "${candidate}" && "${candidate}" != *.exe ]] || continue
|
|
19
|
+
if "${candidate}" --version >/dev/null 2>&1; then
|
|
20
|
+
echo "${candidate}"
|
|
21
|
+
return 0
|
|
22
|
+
fi
|
|
23
|
+
done
|
|
24
|
+
return 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if ! CHROME_PATH="$(detect_chrome)"; then
|
|
28
|
+
echo "✗ no usable Linux Chrome/Chromium found." >&2
|
|
29
|
+
echo " Install one with: pnpm dlx playwright@latest install chromium" >&2
|
|
30
|
+
echo " Or point at the binary: CHROME_PATH=/path/to/chrome pnpm run lhci:local" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
export CHROME_PATH
|
|
34
|
+
echo "✓ chrome: ${CHROME_PATH} ($("${CHROME_PATH}" --version 2>/dev/null))"
|
|
35
|
+
|
|
36
|
+
SERVER_PID=''
|
|
37
|
+
|
|
38
|
+
cleanup() {
|
|
39
|
+
if [[ -n "${SERVER_PID}" ]]; then
|
|
40
|
+
kill -- "-${SERVER_PID}" 2>/dev/null || kill "${SERVER_PID}" 2>/dev/null || true
|
|
41
|
+
pkill -f "serve@14 .*--listen ${PORT}" 2>/dev/null || true
|
|
42
|
+
fi
|
|
43
|
+
find . -maxdepth 1 \( -name '*wsl.localhost*' -o -name 'undefined*' \) -exec rm -rf {} + 2>/dev/null || true
|
|
44
|
+
}
|
|
45
|
+
trap cleanup EXIT
|
|
46
|
+
|
|
47
|
+
echo "→ production-equivalent build in ${OUT_DIR}"
|
|
48
|
+
VERCEL_ENV=production pnpm build --outDir "${OUT_DIR}"
|
|
49
|
+
|
|
50
|
+
if ! grep -qx 'Allow: /' "${OUT_DIR}/client/robots.txt"; then
|
|
51
|
+
echo "✗ ${OUT_DIR}/client/robots.txt has no 'Allow: /' — the build is not production-equivalent." >&2
|
|
52
|
+
head -3 "${OUT_DIR}/client/robots.txt" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
echo "✓ robots.txt: Allow: /"
|
|
56
|
+
|
|
57
|
+
echo "→ starting the static server on :${PORT}"
|
|
58
|
+
setsid pnpm dlx serve@14 "${OUT_DIR}/client" --listen "${PORT}" --no-clipboard >/dev/null 2>&1 &
|
|
59
|
+
SERVER_PID=$!
|
|
60
|
+
|
|
61
|
+
for _ in $(seq 1 60); do
|
|
62
|
+
if curl -fs -o /dev/null "http://localhost:${PORT}/"; then break; fi
|
|
63
|
+
sleep 1
|
|
64
|
+
done
|
|
65
|
+
if ! curl -fs -o /dev/null "http://localhost:${PORT}/"; then
|
|
66
|
+
echo "✗ the server did not answer on :${PORT} within 60s." >&2
|
|
67
|
+
echo " Retry on another port: LHCI_PORT=4400 pnpm run lhci:local" >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
echo "✓ server ready"
|
|
71
|
+
|
|
72
|
+
rm -rf "${REPORT_DIR}"
|
|
73
|
+
|
|
74
|
+
LH_PORT="${PORT}" \
|
|
75
|
+
LH_RUNS="${LHCI_RUNS:-1}" \
|
|
76
|
+
LH_OUT="${REPORT_DIR}" \
|
|
77
|
+
LH_EXTERNAL_SERVER=1 \
|
|
78
|
+
LH_CHROME_FLAGS='--no-sandbox --disable-dev-shm-usage' \
|
|
79
|
+
node "${OFFICINA_BIN}" check lighthouse
|
|
80
|
+
|
|
81
|
+
echo
|
|
82
|
+
echo "→ score per URL (median over ${LHCI_RUNS:-1} run(s), the way LHCI asserts)"
|
|
83
|
+
node -e '
|
|
84
|
+
const fs = require("fs")
|
|
85
|
+
const dir = process.argv[1]
|
|
86
|
+
const byUrl = new Map()
|
|
87
|
+
for (const f of fs.readdirSync(dir).filter((f) => f.endsWith(".report.json"))) {
|
|
88
|
+
const r = JSON.parse(fs.readFileSync(`${dir}/${f}`))
|
|
89
|
+
const url = String(r.finalDisplayedUrl)
|
|
90
|
+
if (!byUrl.has(url)) byUrl.set(url, [])
|
|
91
|
+
byUrl.get(url).push(r.categories)
|
|
92
|
+
}
|
|
93
|
+
const median = (xs) => {
|
|
94
|
+
const s = [...xs].sort((a, b) => a - b)
|
|
95
|
+
return s[Math.floor(s.length / 2)]
|
|
96
|
+
}
|
|
97
|
+
for (const [url, runs] of [...byUrl].sort()) {
|
|
98
|
+
const pick = (k) => median(runs.map((c) => c[k].score)).toFixed(2)
|
|
99
|
+
console.log(
|
|
100
|
+
url.padEnd(46),
|
|
101
|
+
"perf", pick("performance"),
|
|
102
|
+
"a11y", pick("accessibility"),
|
|
103
|
+
"bp", pick("best-practices"),
|
|
104
|
+
"seo", pick("seo"),
|
|
105
|
+
)
|
|
106
|
+
}' "${REPORT_DIR}"
|
package/package.json
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elia97/officina",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Gli strumenti dei progetti nati dai template di Elia: gate di
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/Elia97/officina.git"
|
|
10
10
|
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
11
17
|
"bin": {
|
|
12
18
|
"officina": "dist/bin.js"
|
|
13
19
|
},
|
|
@@ -29,8 +35,20 @@
|
|
|
29
35
|
"typescript": "^7.0.2",
|
|
30
36
|
"vitest": "^4.1.11"
|
|
31
37
|
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"plop": "^4.0.5",
|
|
40
|
+
"ts-morph": "^28.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"sharp": ">=0.34.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"sharp": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
32
50
|
"scripts": {
|
|
33
|
-
"build": "tsc -p tsconfig.build.json",
|
|
51
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp -r src/gen/templates dist/gen/templates && cp -r src/sh dist/sh",
|
|
34
52
|
"typecheck": "tsc --noEmit",
|
|
35
53
|
"test": "vitest run --coverage",
|
|
36
54
|
"format": "biome check --write .",
|