@reddoorla/maintenance 0.7.0 → 0.9.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 +8 -2
- package/dist/cli/bin.js +388 -117
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +6 -0
- package/dist/cli/commands/audit.js +284 -26
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/index.d.ts +35 -9
- package/dist/index.js +259 -142
- package/dist/index.js.map +1 -1
- package/dist/reports/maintenance-email/assets/blurredTests.jpg +0 -0
- package/dist/reports/maintenance-email/assets/check.png +0 -0
- package/package.json +1 -1
package/dist/cli/bin.js
CHANGED
|
@@ -10,6 +10,11 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/reports/airtable/client.ts
|
|
13
|
+
var client_exports = {};
|
|
14
|
+
__export(client_exports, {
|
|
15
|
+
openBase: () => openBase,
|
|
16
|
+
readAirtableConfig: () => readAirtableConfig
|
|
17
|
+
});
|
|
13
18
|
import Airtable from "airtable";
|
|
14
19
|
function readAirtableConfig() {
|
|
15
20
|
const apiKey = process.env.AIRTABLE_PAT;
|
|
@@ -28,6 +33,14 @@ var init_client = __esm({
|
|
|
28
33
|
});
|
|
29
34
|
|
|
30
35
|
// src/reports/airtable/websites.ts
|
|
36
|
+
var websites_exports = {};
|
|
37
|
+
__export(websites_exports, {
|
|
38
|
+
WEBSITES_TABLE: () => WEBSITES_TABLE,
|
|
39
|
+
getWebsiteBySlug: () => getWebsiteBySlug,
|
|
40
|
+
listWebsites: () => listWebsites,
|
|
41
|
+
siteSlug: () => siteSlug,
|
|
42
|
+
updateScores: () => updateScores
|
|
43
|
+
});
|
|
31
44
|
function siteSlug(name) {
|
|
32
45
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
33
46
|
}
|
|
@@ -39,6 +52,7 @@ function mapRow(rec) {
|
|
|
39
52
|
id: rec.id,
|
|
40
53
|
name: String(f["Name"] ?? ""),
|
|
41
54
|
url: String(f["url"] ?? ""),
|
|
55
|
+
status: f["Status"] ?? null,
|
|
42
56
|
pointOfContact: f["point of contact"] ?? null,
|
|
43
57
|
maintenanceFreq: f["maintenence freq"] ?? "None",
|
|
44
58
|
testingFreq: f["testing freq"] ?? "None",
|
|
@@ -51,7 +65,8 @@ function mapRow(rec) {
|
|
|
51
65
|
pScore: f["pScore"] ?? null,
|
|
52
66
|
rScore: f["rScore"] ?? null,
|
|
53
67
|
bpScore: f["bpScore"] ?? null,
|
|
54
|
-
seoScore: f["seoScore"] ?? null
|
|
68
|
+
seoScore: f["seoScore"] ?? null,
|
|
69
|
+
lastLighthouseAuditAt: f["Last lighthouse audit at"] ?? null
|
|
55
70
|
};
|
|
56
71
|
}
|
|
57
72
|
async function listWebsites(base) {
|
|
@@ -62,6 +77,20 @@ async function listWebsites(base) {
|
|
|
62
77
|
});
|
|
63
78
|
return out;
|
|
64
79
|
}
|
|
80
|
+
async function getWebsiteBySlug(base, slug) {
|
|
81
|
+
const all = await listWebsites(base);
|
|
82
|
+
return all.find((w) => siteSlug(w.name) === slug) ?? null;
|
|
83
|
+
}
|
|
84
|
+
async function updateScores(base, recordId, scores) {
|
|
85
|
+
const fields = {
|
|
86
|
+
pScore: scores.performance,
|
|
87
|
+
rScore: scores.accessibility,
|
|
88
|
+
bpScore: scores.bestPractices,
|
|
89
|
+
seoScore: scores.seo,
|
|
90
|
+
"Last lighthouse audit at": (/* @__PURE__ */ new Date()).toISOString()
|
|
91
|
+
};
|
|
92
|
+
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
93
|
+
}
|
|
65
94
|
var WEBSITES_TABLE;
|
|
66
95
|
var init_websites = __esm({
|
|
67
96
|
"src/reports/airtable/websites.ts"() {
|
|
@@ -70,6 +99,82 @@ var init_websites = __esm({
|
|
|
70
99
|
}
|
|
71
100
|
});
|
|
72
101
|
|
|
102
|
+
// src/inventory/airtable.ts
|
|
103
|
+
var airtable_exports = {};
|
|
104
|
+
__export(airtable_exports, {
|
|
105
|
+
fromAirtableBase: () => fromAirtableBase
|
|
106
|
+
});
|
|
107
|
+
function fromAirtableBase(base, opts = {}) {
|
|
108
|
+
return async () => {
|
|
109
|
+
const workdir = opts.workdir ?? process.env.REDDOOR_FLEET_WORKDIR;
|
|
110
|
+
if (!workdir) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
"fromAirtableBase requires `workdir` option or REDDOOR_FLEET_WORKDIR env (sites need a local path)"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const websites = await listWebsites(base);
|
|
116
|
+
return websites.filter((w) => w.maintenanceFreq !== "None" || w.testingFreq !== "None").map((w) => {
|
|
117
|
+
const slug = siteSlug(w.name);
|
|
118
|
+
const site = {
|
|
119
|
+
path: `${workdir}/${slug}`,
|
|
120
|
+
name: slug,
|
|
121
|
+
meta: { airtableRowId: w.id, displayName: w.name }
|
|
122
|
+
};
|
|
123
|
+
if (w.url) site.repoUrl = w.url;
|
|
124
|
+
return site;
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
var init_airtable = __esm({
|
|
129
|
+
"src/inventory/airtable.ts"() {
|
|
130
|
+
"use strict";
|
|
131
|
+
init_websites();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// src/audits/lighthouse-airtable.ts
|
|
136
|
+
var lighthouse_airtable_exports = {};
|
|
137
|
+
__export(lighthouse_airtable_exports, {
|
|
138
|
+
lighthouseScoresFromResult: () => lighthouseScoresFromResult,
|
|
139
|
+
resolveSlugFromCwd: () => resolveSlugFromCwd
|
|
140
|
+
});
|
|
141
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
142
|
+
import { join as join7 } from "path";
|
|
143
|
+
function lighthouseScoresFromResult(result) {
|
|
144
|
+
if (result.audit !== "lighthouse") {
|
|
145
|
+
throw new Error(`Expected a 'lighthouse' AuditResult, got '${result.audit}'`);
|
|
146
|
+
}
|
|
147
|
+
const details = result.details ?? {};
|
|
148
|
+
const summary = details.summary ?? {};
|
|
149
|
+
const toPct = (n) => typeof n === "number" && !Number.isNaN(n) ? Math.round(n * 100) : 0;
|
|
150
|
+
return {
|
|
151
|
+
performance: toPct(summary["performance"]),
|
|
152
|
+
accessibility: toPct(summary["accessibility"]),
|
|
153
|
+
bestPractices: toPct(summary["best-practices"]),
|
|
154
|
+
seo: toPct(summary["seo"])
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function resolveSlugFromCwd(cwd) {
|
|
158
|
+
try {
|
|
159
|
+
const pkgPath = join7(cwd, "package.json");
|
|
160
|
+
const raw = await readFile7(pkgPath, "utf-8");
|
|
161
|
+
const pkg = JSON.parse(raw);
|
|
162
|
+
if (!pkg.name) throw new Error("package.json has no 'name' field");
|
|
163
|
+
return siteSlug(pkg.name);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Could not derive site slug from ${cwd}/package.json: ${e.message}. Pass --write-airtable=<slug> explicitly.`,
|
|
167
|
+
{ cause: e }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
var init_lighthouse_airtable = __esm({
|
|
172
|
+
"src/audits/lighthouse-airtable.ts"() {
|
|
173
|
+
"use strict";
|
|
174
|
+
init_websites();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
73
178
|
// src/reports/airtable/reports.ts
|
|
74
179
|
function mapRow2(rec) {
|
|
75
180
|
const f = rec.fields;
|
|
@@ -177,6 +282,40 @@ var init_reports = __esm({
|
|
|
177
282
|
}
|
|
178
283
|
});
|
|
179
284
|
|
|
285
|
+
// src/reports/maintenance-email/assets/index.ts
|
|
286
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
287
|
+
import { dirname as dirname2, join as join21 } from "path";
|
|
288
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
289
|
+
async function loadBundledImages() {
|
|
290
|
+
const [check, blurred] = await Promise.all([
|
|
291
|
+
readFile13(join21(here, "check.png")),
|
|
292
|
+
readFile13(join21(here, "blurredTests.jpg"))
|
|
293
|
+
]);
|
|
294
|
+
return {
|
|
295
|
+
check: {
|
|
296
|
+
bytes: new Uint8Array(check),
|
|
297
|
+
contentType: "image/png",
|
|
298
|
+
cid: CHECK_CID,
|
|
299
|
+
filename: "check.png"
|
|
300
|
+
},
|
|
301
|
+
blurred: {
|
|
302
|
+
bytes: new Uint8Array(blurred),
|
|
303
|
+
contentType: "image/jpeg",
|
|
304
|
+
cid: BLURRED_CID,
|
|
305
|
+
filename: "blurredTests.jpg"
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
var here, CHECK_CID, BLURRED_CID;
|
|
310
|
+
var init_assets = __esm({
|
|
311
|
+
"src/reports/maintenance-email/assets/index.ts"() {
|
|
312
|
+
"use strict";
|
|
313
|
+
here = dirname2(fileURLToPath2(import.meta.url));
|
|
314
|
+
CHECK_CID = "rd-check-png";
|
|
315
|
+
BLURRED_CID = "rd-blurred-tests-jpg";
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
180
319
|
// src/reports/maintenance-email/template.ts
|
|
181
320
|
function fmtDate(d) {
|
|
182
321
|
if (!d) return "";
|
|
@@ -344,8 +483,9 @@ var CHECK_PNG, BLURRED_TESTS;
|
|
|
344
483
|
var init_template = __esm({
|
|
345
484
|
"src/reports/maintenance-email/template.ts"() {
|
|
346
485
|
"use strict";
|
|
347
|
-
|
|
348
|
-
|
|
486
|
+
init_assets();
|
|
487
|
+
CHECK_PNG = `cid:${CHECK_CID}`;
|
|
488
|
+
BLURRED_TESTS = `cid:${BLURRED_CID}`;
|
|
349
489
|
}
|
|
350
490
|
});
|
|
351
491
|
|
|
@@ -375,6 +515,27 @@ async function fetchAttachmentBytes(url) {
|
|
|
375
515
|
const ab = await res.arrayBuffer();
|
|
376
516
|
return { bytes: new Uint8Array(ab), contentType };
|
|
377
517
|
}
|
|
518
|
+
async function uploadAttachment(recordId, fieldName, body, filename, contentType) {
|
|
519
|
+
const apiKey = process.env.AIRTABLE_PAT;
|
|
520
|
+
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
521
|
+
if (!apiKey || !baseId) {
|
|
522
|
+
throw new Error("AIRTABLE_PAT and AIRTABLE_BASE_ID must be set");
|
|
523
|
+
}
|
|
524
|
+
const base64 = typeof body === "string" ? Buffer.from(body, "utf-8").toString("base64") : Buffer.from(body).toString("base64");
|
|
525
|
+
const payload = { contentType, file: base64, filename };
|
|
526
|
+
const url = `https://content.airtable.com/v0/${baseId}/${recordId}/${encodeURIComponent(fieldName)}/uploadAttachment`;
|
|
527
|
+
const res = await fetch(url, {
|
|
528
|
+
method: "POST",
|
|
529
|
+
headers: {
|
|
530
|
+
Authorization: `Bearer ${apiKey}`,
|
|
531
|
+
"Content-Type": "application/json"
|
|
532
|
+
},
|
|
533
|
+
body: JSON.stringify(payload)
|
|
534
|
+
});
|
|
535
|
+
if (!res.ok) {
|
|
536
|
+
throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
378
539
|
var init_attachments = __esm({
|
|
379
540
|
"src/reports/airtable/attachments.ts"() {
|
|
380
541
|
"use strict";
|
|
@@ -454,6 +615,7 @@ async function sendOne(client, base, site, report) {
|
|
|
454
615
|
throw new Error(`Report ${report.reportId} has no Lighthouse scores`);
|
|
455
616
|
}
|
|
456
617
|
const { bytes, contentType } = await fetchAttachmentBytes(site.headerImage.url);
|
|
618
|
+
const bundled = await loadBundledImages();
|
|
457
619
|
const slug = siteSlug(site.name);
|
|
458
620
|
const cidName = `${slug}-header`;
|
|
459
621
|
const { html } = await renderReportHtml({
|
|
@@ -506,6 +668,21 @@ async function sendOne(client, base, site, report) {
|
|
|
506
668
|
content: Buffer.from(bytes).toString("base64"),
|
|
507
669
|
contentType,
|
|
508
670
|
inlineContentId: cidName
|
|
671
|
+
},
|
|
672
|
+
// Bundled images referenced via cid:rd-check-png / cid:rd-blurred-tests-jpg
|
|
673
|
+
// in the template. Attached inline so the email is self-contained — no
|
|
674
|
+
// external CDN dependency, no image-blocked broken icons in webmail.
|
|
675
|
+
{
|
|
676
|
+
filename: bundled.check.filename,
|
|
677
|
+
content: Buffer.from(bundled.check.bytes).toString("base64"),
|
|
678
|
+
contentType: bundled.check.contentType,
|
|
679
|
+
inlineContentId: bundled.check.cid
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
filename: bundled.blurred.filename,
|
|
683
|
+
content: Buffer.from(bundled.blurred.bytes).toString("base64"),
|
|
684
|
+
contentType: bundled.blurred.contentType,
|
|
685
|
+
inlineContentId: bundled.blurred.cid
|
|
509
686
|
}
|
|
510
687
|
],
|
|
511
688
|
// Stable across retries of the same row — if Airtable stamping fails after a
|
|
@@ -550,6 +727,7 @@ var init_orchestrate = __esm({
|
|
|
550
727
|
init_websites();
|
|
551
728
|
init_attachments();
|
|
552
729
|
init_render();
|
|
730
|
+
init_assets();
|
|
553
731
|
init_resend();
|
|
554
732
|
FROM_ADDRESS = "Reddoor Reports <reports@reddoorla.com>";
|
|
555
733
|
REPLY_TO = "info@reddoorla.com";
|
|
@@ -557,8 +735,8 @@ var init_orchestrate = __esm({
|
|
|
557
735
|
});
|
|
558
736
|
|
|
559
737
|
// src/cli/bin.ts
|
|
560
|
-
import { dirname as
|
|
561
|
-
import { fileURLToPath as
|
|
738
|
+
import { dirname as dirname4 } from "path";
|
|
739
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
562
740
|
import { cac } from "cac";
|
|
563
741
|
|
|
564
742
|
// src/cli/commands/audit.ts
|
|
@@ -901,9 +1079,9 @@ async function securityAudit(ctx) {
|
|
|
901
1079
|
}
|
|
902
1080
|
|
|
903
1081
|
// src/audits/lighthouse.ts
|
|
904
|
-
import { readFile as
|
|
1082
|
+
import { readFile as readFile4, writeFile, mkdtemp, rm } from "fs/promises";
|
|
905
1083
|
import { tmpdir } from "os";
|
|
906
|
-
import { join as
|
|
1084
|
+
import { join as join4 } from "path";
|
|
907
1085
|
|
|
908
1086
|
// src/configs/lighthouse.ts
|
|
909
1087
|
var lighthouseConfig = {
|
|
@@ -936,10 +1114,37 @@ var lighthouseConfig = {
|
|
|
936
1114
|
}
|
|
937
1115
|
};
|
|
938
1116
|
|
|
1117
|
+
// src/audits/util/site-config.ts
|
|
1118
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1119
|
+
import { join as join3 } from "path";
|
|
1120
|
+
async function readSiteConfig(sitePath) {
|
|
1121
|
+
let raw;
|
|
1122
|
+
try {
|
|
1123
|
+
raw = await readFile3(join3(sitePath, "package.json"), "utf-8");
|
|
1124
|
+
} catch {
|
|
1125
|
+
return {};
|
|
1126
|
+
}
|
|
1127
|
+
let pkg;
|
|
1128
|
+
try {
|
|
1129
|
+
pkg = JSON.parse(raw);
|
|
1130
|
+
} catch {
|
|
1131
|
+
return {};
|
|
1132
|
+
}
|
|
1133
|
+
if (!pkg || typeof pkg !== "object") return {};
|
|
1134
|
+
const cfg = pkg.reddoor;
|
|
1135
|
+
if (!cfg || typeof cfg !== "object") return {};
|
|
1136
|
+
const out = {};
|
|
1137
|
+
const url = cfg.lighthouseUrl;
|
|
1138
|
+
if (typeof url === "string" && url.length > 0) {
|
|
1139
|
+
out.lighthouseUrl = url;
|
|
1140
|
+
}
|
|
1141
|
+
return out;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
939
1144
|
// src/audits/lighthouse.ts
|
|
940
1145
|
async function readJsonMaybe(path) {
|
|
941
1146
|
try {
|
|
942
|
-
const raw = await
|
|
1147
|
+
const raw = await readFile4(path, "utf-8");
|
|
943
1148
|
return JSON.parse(raw);
|
|
944
1149
|
} catch {
|
|
945
1150
|
return null;
|
|
@@ -975,15 +1180,28 @@ async function lighthouseAudit(ctx) {
|
|
|
975
1180
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
976
1181
|
const site = ctx.site;
|
|
977
1182
|
const label = siteLabel(site);
|
|
978
|
-
const
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
1183
|
+
const siteCfg = await readSiteConfig(site.path);
|
|
1184
|
+
const resolvedConfig = siteCfg.lighthouseUrl ? {
|
|
1185
|
+
...lighthouseConfig,
|
|
1186
|
+
ci: {
|
|
1187
|
+
...lighthouseConfig.ci,
|
|
1188
|
+
collect: { ...lighthouseConfig.ci.collect, url: [siteCfg.lighthouseUrl] }
|
|
1189
|
+
}
|
|
1190
|
+
} : lighthouseConfig;
|
|
1191
|
+
const configDir = await mkdtemp(join4(tmpdir(), "reddoor-lhci-"));
|
|
1192
|
+
const configPath = join4(configDir, "lighthouserc.json");
|
|
1193
|
+
await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
|
|
1194
|
+
const resultsDir = join4(site.path, ".lighthouseci");
|
|
982
1195
|
await rm(resultsDir, { recursive: true, force: true });
|
|
983
1196
|
let raw;
|
|
984
1197
|
try {
|
|
985
1198
|
raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
|
|
986
|
-
cwd: site.path
|
|
1199
|
+
cwd: site.path,
|
|
1200
|
+
// lhci autorun boots the site's dev server, downloads Chrome on first
|
|
1201
|
+
// use, and runs the audit — easily 2–3 min on a cold tree. The shared
|
|
1202
|
+
// 30 s default in runAudits is fine for deps/lint/security but starves
|
|
1203
|
+
// lhci.
|
|
1204
|
+
timeoutMs: 5 * 6e4
|
|
987
1205
|
});
|
|
988
1206
|
} catch (err) {
|
|
989
1207
|
await rm(configDir, { recursive: true, force: true });
|
|
@@ -999,7 +1217,7 @@ async function lighthouseAudit(ctx) {
|
|
|
999
1217
|
throw err;
|
|
1000
1218
|
}
|
|
1001
1219
|
await rm(configDir, { recursive: true, force: true });
|
|
1002
|
-
const manifest = await readJsonMaybe(
|
|
1220
|
+
const manifest = await readJsonMaybe(join4(resultsDir, "manifest.json"));
|
|
1003
1221
|
if (!manifest || manifest.length === 0) {
|
|
1004
1222
|
return {
|
|
1005
1223
|
audit: "lighthouse",
|
|
@@ -1008,7 +1226,7 @@ async function lighthouseAudit(ctx) {
|
|
|
1008
1226
|
summary: `lighthouse: no manifest written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
|
|
1009
1227
|
};
|
|
1010
1228
|
}
|
|
1011
|
-
const assertionResults = await readJsonMaybe(
|
|
1229
|
+
const assertionResults = await readJsonMaybe(join4(resultsDir, "assertion-results.json")) ?? [];
|
|
1012
1230
|
const failed = assertionResults.filter((a) => !a.passed);
|
|
1013
1231
|
const assertions = failed.map((a) => ({
|
|
1014
1232
|
category: categoryFromAssertion(a),
|
|
@@ -1034,9 +1252,9 @@ async function lighthouseAudit(ctx) {
|
|
|
1034
1252
|
}
|
|
1035
1253
|
|
|
1036
1254
|
// src/audits/a11y.ts
|
|
1037
|
-
import { readFile as
|
|
1255
|
+
import { readFile as readFile5, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
1038
1256
|
import { tmpdir as tmpdir2 } from "os";
|
|
1039
|
-
import { join as
|
|
1257
|
+
import { join as join5 } from "path";
|
|
1040
1258
|
|
|
1041
1259
|
// src/configs/playwright-a11y.ts
|
|
1042
1260
|
import { defineConfig, devices } from "@playwright/test";
|
|
@@ -1074,7 +1292,7 @@ var playwrightA11yConfig = defineConfig({
|
|
|
1074
1292
|
var RESULTS_REL = ".reddoor-a11y/results.json";
|
|
1075
1293
|
async function readJsonMaybe2(path) {
|
|
1076
1294
|
try {
|
|
1077
|
-
const raw = await
|
|
1295
|
+
const raw = await readFile5(path, "utf-8");
|
|
1078
1296
|
return JSON.parse(raw);
|
|
1079
1297
|
} catch {
|
|
1080
1298
|
return null;
|
|
@@ -1130,11 +1348,11 @@ async function a11yAudit(ctx) {
|
|
|
1130
1348
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
1131
1349
|
const site = ctx.site;
|
|
1132
1350
|
const label = siteLabel(site);
|
|
1133
|
-
const specDir = await mkdtemp2(
|
|
1134
|
-
const specPath =
|
|
1351
|
+
const specDir = await mkdtemp2(join5(tmpdir2(), "reddoor-a11y-spec-"));
|
|
1352
|
+
const specPath = join5(specDir, "a11y.spec.ts");
|
|
1135
1353
|
await writeFile2(specPath, buildSpec(), "utf-8");
|
|
1136
|
-
const resultsPath =
|
|
1137
|
-
await rm2(
|
|
1354
|
+
const resultsPath = join5(site.path, RESULTS_REL);
|
|
1355
|
+
await rm2(join5(site.path, ".reddoor-a11y"), { recursive: true, force: true });
|
|
1138
1356
|
let raw;
|
|
1139
1357
|
try {
|
|
1140
1358
|
raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=line", specPath], {
|
|
@@ -1210,6 +1428,10 @@ async function runAudits(site, which) {
|
|
|
1210
1428
|
)
|
|
1211
1429
|
);
|
|
1212
1430
|
}
|
|
1431
|
+
async function runAuditsAcross(sites, which) {
|
|
1432
|
+
const all = await Promise.all(sites.map((s) => runAudits(s, which)));
|
|
1433
|
+
return all.flat();
|
|
1434
|
+
}
|
|
1213
1435
|
|
|
1214
1436
|
// src/cli/fleet/resolve-sites.ts
|
|
1215
1437
|
import { pathToFileURL } from "url";
|
|
@@ -1223,7 +1445,7 @@ function localPath(path, opts = {}) {
|
|
|
1223
1445
|
}
|
|
1224
1446
|
|
|
1225
1447
|
// src/inventory/json.ts
|
|
1226
|
-
import { readFile as
|
|
1448
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1227
1449
|
import { isAbsolute } from "path";
|
|
1228
1450
|
function validate(raw) {
|
|
1229
1451
|
if (!Array.isArray(raw)) {
|
|
@@ -1253,7 +1475,7 @@ function validate(raw) {
|
|
|
1253
1475
|
}
|
|
1254
1476
|
function fromJsonFile(path) {
|
|
1255
1477
|
return async () => {
|
|
1256
|
-
const raw = JSON.parse(await
|
|
1478
|
+
const raw = JSON.parse(await readFile6(path, "utf-8"));
|
|
1257
1479
|
return validate(raw);
|
|
1258
1480
|
};
|
|
1259
1481
|
}
|
|
@@ -1265,6 +1487,13 @@ async function resolveSites(input) {
|
|
|
1265
1487
|
exitCode: 2
|
|
1266
1488
|
});
|
|
1267
1489
|
}
|
|
1490
|
+
if (input.fleet === "airtable") {
|
|
1491
|
+
const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1492
|
+
const { fromAirtableBase: fromAirtableBase2 } = await Promise.resolve().then(() => (init_airtable(), airtable_exports));
|
|
1493
|
+
const base = openBase2(readAirtableConfig2());
|
|
1494
|
+
const provider = fromAirtableBase2(base, input.workdir ? { workdir: input.workdir } : {});
|
|
1495
|
+
return provider();
|
|
1496
|
+
}
|
|
1268
1497
|
if (input.fleet) {
|
|
1269
1498
|
const fleetPath = resolve(input.cwd, input.fleet);
|
|
1270
1499
|
const ext = extname(fleetPath).toLowerCase();
|
|
@@ -1292,7 +1521,7 @@ async function resolveSites(input) {
|
|
|
1292
1521
|
|
|
1293
1522
|
// src/cli/fleet/clone-if-needed.ts
|
|
1294
1523
|
import { stat, readdir, mkdir } from "fs/promises";
|
|
1295
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1524
|
+
import { isAbsolute as isAbsolute2, join as join6 } from "path";
|
|
1296
1525
|
function deriveNameFromRepoUrl(repoUrl) {
|
|
1297
1526
|
const slash = repoUrl.split("/").pop() ?? repoUrl;
|
|
1298
1527
|
return slash.replace(/\.git$/, "");
|
|
@@ -1333,7 +1562,7 @@ async function cloneIfNeeded(site, opts) {
|
|
|
1333
1562
|
const name = site.name ?? deriveNameFromRepoUrl(site.repoUrl);
|
|
1334
1563
|
assertSafeName(name);
|
|
1335
1564
|
assertSafeRepoUrl(site.repoUrl);
|
|
1336
|
-
const target =
|
|
1565
|
+
const target = join6(opts.workdir, name);
|
|
1337
1566
|
await mkdir(opts.workdir, { recursive: true });
|
|
1338
1567
|
if (await isNonEmptyDir(target)) {
|
|
1339
1568
|
return { ...site, name, path: target };
|
|
@@ -1373,28 +1602,59 @@ async function runAuditCommand(site, opts) {
|
|
|
1373
1602
|
let sites = await resolveSites({
|
|
1374
1603
|
...site !== void 0 ? { site } : {},
|
|
1375
1604
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
1605
|
+
...opts.workdir !== void 0 ? { workdir: opts.workdir } : {},
|
|
1376
1606
|
cwd
|
|
1377
1607
|
});
|
|
1378
1608
|
if (opts.fleet) {
|
|
1379
1609
|
const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
|
|
1380
1610
|
sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
|
|
1381
1611
|
}
|
|
1382
|
-
const results =
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1612
|
+
const results = await runAuditsAcross(sites, which);
|
|
1613
|
+
let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
|
|
1614
|
+
if (opts.writeAirtable !== void 0) {
|
|
1615
|
+
const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
1616
|
+
const { listWebsites: listWebsites2, updateScores: updateScores2, siteSlug: siteSlug2 } = await Promise.resolve().then(() => (init_websites(), websites_exports));
|
|
1617
|
+
const { lighthouseScoresFromResult: lighthouseScoresFromResult2, resolveSlugFromCwd: resolveSlugFromCwd2 } = await Promise.resolve().then(() => (init_lighthouse_airtable(), lighthouse_airtable_exports));
|
|
1618
|
+
const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd2(cwd);
|
|
1619
|
+
const lhResult = results.find((r) => r.audit === "lighthouse");
|
|
1620
|
+
if (!lhResult) {
|
|
1621
|
+
throw Object.assign(
|
|
1622
|
+
new Error(
|
|
1623
|
+
"--write-airtable requires a lighthouse result; did you pass --only without lighthouse?"
|
|
1624
|
+
),
|
|
1625
|
+
{ exitCode: 2 }
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
if (lhResult.status === "fail") {
|
|
1629
|
+
throw Object.assign(
|
|
1630
|
+
new Error(
|
|
1631
|
+
`Lighthouse audit failed; refusing to write scores to Airtable. Summary: ${lhResult.summary}`
|
|
1632
|
+
),
|
|
1633
|
+
{ exitCode: 1 }
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
const base = openBase2(readAirtableConfig2());
|
|
1637
|
+
const websites = await listWebsites2(base);
|
|
1638
|
+
const target = websites.find((w) => siteSlug2(w.name) === slug);
|
|
1639
|
+
if (!target) {
|
|
1640
|
+
throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
|
|
1641
|
+
}
|
|
1642
|
+
const scores = lighthouseScoresFromResult2(lhResult);
|
|
1643
|
+
await updateScores2(base, target.id, scores);
|
|
1644
|
+
output += `
|
|
1645
|
+
|
|
1646
|
+
\u2192 wrote scores to Websites[${target.name}]: P=${scores.performance} A=${scores.accessibility} BP=${scores.bestPractices} SEO=${scores.seo}`;
|
|
1386
1647
|
}
|
|
1387
|
-
const output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
|
|
1388
1648
|
return { output, code: exitCode(results) };
|
|
1389
1649
|
}
|
|
1390
1650
|
|
|
1391
1651
|
// src/cli/commands/sync-configs.ts
|
|
1392
|
-
import { readFile as
|
|
1393
|
-
import { join as
|
|
1652
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1653
|
+
import { join as join9, resolve as resolve3 } from "path";
|
|
1394
1654
|
|
|
1395
1655
|
// src/recipes/sync-configs.ts
|
|
1396
|
-
import { readFile as
|
|
1397
|
-
import { join as
|
|
1656
|
+
import { readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
|
|
1657
|
+
import { join as join8 } from "path";
|
|
1398
1658
|
|
|
1399
1659
|
// src/recipes/sync-configs/templates.ts
|
|
1400
1660
|
var eslint = {
|
|
@@ -1658,7 +1918,7 @@ function isConfigName(value) {
|
|
|
1658
1918
|
}
|
|
1659
1919
|
async function readMaybe(path) {
|
|
1660
1920
|
try {
|
|
1661
|
-
return await
|
|
1921
|
+
return await readFile8(path, "utf-8");
|
|
1662
1922
|
} catch {
|
|
1663
1923
|
return null;
|
|
1664
1924
|
}
|
|
@@ -1666,13 +1926,13 @@ async function readMaybe(path) {
|
|
|
1666
1926
|
async function planTemplateDiffs(cwd, templates) {
|
|
1667
1927
|
const diffs = [];
|
|
1668
1928
|
for (const t of templates) {
|
|
1669
|
-
const existing = await readMaybe(
|
|
1929
|
+
const existing = await readMaybe(join8(cwd, t.path));
|
|
1670
1930
|
if (existing !== t.contents) diffs.push(t);
|
|
1671
1931
|
}
|
|
1672
1932
|
return diffs;
|
|
1673
1933
|
}
|
|
1674
1934
|
async function planGitignore(cwd) {
|
|
1675
|
-
const existing = await readMaybe(
|
|
1935
|
+
const existing = await readMaybe(join8(cwd, ".gitignore"));
|
|
1676
1936
|
const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
|
|
1677
1937
|
const tracked = await listTrackedFiles(cwd);
|
|
1678
1938
|
const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);
|
|
@@ -1680,7 +1940,7 @@ async function planGitignore(cwd) {
|
|
|
1680
1940
|
return { kind: "apply", content: merge.content, toUntrack, added: merge.added };
|
|
1681
1941
|
}
|
|
1682
1942
|
async function applyGitignore(cwd, plan) {
|
|
1683
|
-
await writeFile3(
|
|
1943
|
+
await writeFile3(join8(cwd, ".gitignore"), plan.content, "utf-8");
|
|
1684
1944
|
if (plan.toUntrack.length > 0) {
|
|
1685
1945
|
await removeFromIndex(cwd, plan.toUntrack);
|
|
1686
1946
|
}
|
|
@@ -1703,7 +1963,7 @@ async function syncConfigs(site, opts = {}) {
|
|
|
1703
1963
|
},
|
|
1704
1964
|
apply: async ({ templateDiffs, gitignorePlan }, { commit: commit2 }) => {
|
|
1705
1965
|
for (const t of templateDiffs) {
|
|
1706
|
-
await writeFile3(
|
|
1966
|
+
await writeFile3(join8(site.path, t.path), t.contents, "utf-8");
|
|
1707
1967
|
await commit2(`chore: sync ${t.config} config from @reddoorla/maintenance`);
|
|
1708
1968
|
}
|
|
1709
1969
|
if (gitignorePlan.kind === "apply") {
|
|
@@ -1732,7 +1992,7 @@ function parseOnly2(value) {
|
|
|
1732
1992
|
async function dryPlanGitignore(cwd) {
|
|
1733
1993
|
let existing;
|
|
1734
1994
|
try {
|
|
1735
|
-
existing = await
|
|
1995
|
+
existing = await readFile9(join9(cwd, ".gitignore"), "utf-8");
|
|
1736
1996
|
} catch {
|
|
1737
1997
|
return "would create .gitignore";
|
|
1738
1998
|
}
|
|
@@ -1747,7 +2007,7 @@ async function dryPlan(cwd, which) {
|
|
|
1747
2007
|
for (const t of templateTargets) {
|
|
1748
2008
|
let existing = "";
|
|
1749
2009
|
try {
|
|
1750
|
-
existing = await
|
|
2010
|
+
existing = await readFile9(join9(cwd, t.path), "utf-8");
|
|
1751
2011
|
} catch {
|
|
1752
2012
|
}
|
|
1753
2013
|
if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
|
|
@@ -1795,7 +2055,7 @@ import { resolve as resolve4 } from "path";
|
|
|
1795
2055
|
|
|
1796
2056
|
// src/recipes/bump-deps.ts
|
|
1797
2057
|
import { stat as stat2 } from "fs/promises";
|
|
1798
|
-
import { join as
|
|
2058
|
+
import { join as join10 } from "path";
|
|
1799
2059
|
async function exists(path) {
|
|
1800
2060
|
try {
|
|
1801
2061
|
await stat2(path);
|
|
@@ -1824,10 +2084,10 @@ async function bumpDeps(site, opts = {}) {
|
|
|
1824
2084
|
// land on top of whatever else was in the tree.
|
|
1825
2085
|
checkTreeFirst: true,
|
|
1826
2086
|
plan: async () => {
|
|
1827
|
-
const hasPnpmLock = await exists(
|
|
2087
|
+
const hasPnpmLock = await exists(join10(site.path, "pnpm-lock.yaml"));
|
|
1828
2088
|
if (!hasPnpmLock) {
|
|
1829
|
-
const hasNpmLock = await exists(
|
|
1830
|
-
const hasYarnLock = await exists(
|
|
2089
|
+
const hasNpmLock = await exists(join10(site.path, "package-lock.json"));
|
|
2090
|
+
const hasYarnLock = await exists(join10(site.path, "yarn.lock"));
|
|
1831
2091
|
if (hasNpmLock || hasYarnLock) {
|
|
1832
2092
|
const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
1833
2093
|
return {
|
|
@@ -1897,12 +2157,12 @@ async function runBumpDepsCommand(site, opts) {
|
|
|
1897
2157
|
import { resolve as resolve5 } from "path";
|
|
1898
2158
|
|
|
1899
2159
|
// src/recipes/svelte-5/index.ts
|
|
1900
|
-
import { join as
|
|
2160
|
+
import { join as join16 } from "path";
|
|
1901
2161
|
|
|
1902
2162
|
// src/util/pkg.ts
|
|
1903
|
-
import { readFile as
|
|
2163
|
+
import { readFile as readFile10, writeFile as writeFile4 } from "fs/promises";
|
|
1904
2164
|
async function readPackageJson(path) {
|
|
1905
|
-
const raw = await
|
|
2165
|
+
const raw = await readFile10(path, "utf-8");
|
|
1906
2166
|
return JSON.parse(raw);
|
|
1907
2167
|
}
|
|
1908
2168
|
function detectIndentFromContent(raw) {
|
|
@@ -1912,7 +2172,7 @@ function detectIndentFromContent(raw) {
|
|
|
1912
2172
|
async function writePackageJson(path, pkg) {
|
|
1913
2173
|
let indent = " ";
|
|
1914
2174
|
try {
|
|
1915
|
-
const existing = await
|
|
2175
|
+
const existing = await readFile10(path, "utf-8");
|
|
1916
2176
|
indent = detectIndentFromContent(existing);
|
|
1917
2177
|
} catch {
|
|
1918
2178
|
}
|
|
@@ -1946,7 +2206,7 @@ function bumpDep(pkg, name, version2, opts = {}) {
|
|
|
1946
2206
|
}
|
|
1947
2207
|
|
|
1948
2208
|
// src/recipes/svelte-5/step-bump-versions.ts
|
|
1949
|
-
import { join as
|
|
2209
|
+
import { join as join11 } from "path";
|
|
1950
2210
|
var SVELTE_5_VERSIONS = {
|
|
1951
2211
|
svelte: "^5.55.5",
|
|
1952
2212
|
"@sveltejs/kit": "^2.59.0",
|
|
@@ -1959,7 +2219,7 @@ var SVELTE_5_VERSIONS = {
|
|
|
1959
2219
|
"typescript-svelte-plugin": "^0.3.52"
|
|
1960
2220
|
};
|
|
1961
2221
|
async function bumpToSvelte5Versions(cwd) {
|
|
1962
|
-
const pkgPath =
|
|
2222
|
+
const pkgPath = join11(cwd, "package.json");
|
|
1963
2223
|
const pkg = await readPackageJson(pkgPath);
|
|
1964
2224
|
let next = pkg;
|
|
1965
2225
|
for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
|
|
@@ -1971,8 +2231,8 @@ async function bumpToSvelte5Versions(cwd) {
|
|
|
1971
2231
|
}
|
|
1972
2232
|
|
|
1973
2233
|
// src/recipes/svelte-5/step-svelte-config.ts
|
|
1974
|
-
import { readFile as
|
|
1975
|
-
import { join as
|
|
2234
|
+
import { readFile as readFile11, writeFile as writeFile5 } from "fs/promises";
|
|
2235
|
+
import { join as join12 } from "path";
|
|
1976
2236
|
var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
|
|
1977
2237
|
var IMPORT_FROM_VITE_PLUGIN = new RegExp(
|
|
1978
2238
|
String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
|
|
@@ -2013,10 +2273,10 @@ function dropPreprocessKey(source) {
|
|
|
2013
2273
|
return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
|
|
2014
2274
|
}
|
|
2015
2275
|
async function migrateSvelteConfig(cwd) {
|
|
2016
|
-
const path =
|
|
2276
|
+
const path = join12(cwd, "svelte.config.js");
|
|
2017
2277
|
let src;
|
|
2018
2278
|
try {
|
|
2019
|
-
src = await
|
|
2279
|
+
src = await readFile11(path, "utf-8");
|
|
2020
2280
|
} catch {
|
|
2021
2281
|
return false;
|
|
2022
2282
|
}
|
|
@@ -2050,9 +2310,9 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
|
|
|
2050
2310
|
}
|
|
2051
2311
|
|
|
2052
2312
|
// src/recipes/svelte-5/step-tailwind-upgrade.ts
|
|
2053
|
-
import { join as
|
|
2313
|
+
import { join as join13 } from "path";
|
|
2054
2314
|
async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
2055
|
-
const pkg = await readPackageJson(
|
|
2315
|
+
const pkg = await readPackageJson(join13(cwd, "package.json"));
|
|
2056
2316
|
const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
|
|
2057
2317
|
if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
|
|
2058
2318
|
if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
|
|
@@ -2073,8 +2333,8 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
|
2073
2333
|
}
|
|
2074
2334
|
|
|
2075
2335
|
// src/recipes/svelte-5/step-gotchas.ts
|
|
2076
|
-
import { readFile as
|
|
2077
|
-
import { join as
|
|
2336
|
+
import { readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
|
|
2337
|
+
import { join as join14 } from "path";
|
|
2078
2338
|
import { glob as glob2 } from "tinyglobby";
|
|
2079
2339
|
|
|
2080
2340
|
// src/recipes/svelte-5/codemods/on-event-to-handler.ts
|
|
@@ -2406,8 +2666,8 @@ async function planGotchaCodemods(cwd) {
|
|
|
2406
2666
|
const changes = [];
|
|
2407
2667
|
const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
|
|
2408
2668
|
for (const rel of relPaths) {
|
|
2409
|
-
const path =
|
|
2410
|
-
const before = await
|
|
2669
|
+
const path = join14(cwd, rel);
|
|
2670
|
+
const before = await readFile12(path, "utf-8");
|
|
2411
2671
|
const after = CODEMODS.reduce((s, fn) => fn(s), before);
|
|
2412
2672
|
if (after !== before) changes.push({ rel, after });
|
|
2413
2673
|
}
|
|
@@ -2416,7 +2676,7 @@ async function planGotchaCodemods(cwd) {
|
|
|
2416
2676
|
async function applyGotchaCodemods(cwd) {
|
|
2417
2677
|
const changes = await planGotchaCodemods(cwd);
|
|
2418
2678
|
for (const c of changes) {
|
|
2419
|
-
await writeFile6(
|
|
2679
|
+
await writeFile6(join14(cwd, c.rel), c.after, "utf-8");
|
|
2420
2680
|
}
|
|
2421
2681
|
return { filesChanged: changes.length };
|
|
2422
2682
|
}
|
|
@@ -2440,7 +2700,7 @@ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
|
|
|
2440
2700
|
|
|
2441
2701
|
// src/recipes/svelte-5/step-summary.ts
|
|
2442
2702
|
import { writeFile as writeFile7 } from "fs/promises";
|
|
2443
|
-
import { join as
|
|
2703
|
+
import { join as join15 } from "path";
|
|
2444
2704
|
async function writeMigrationSummary(input) {
|
|
2445
2705
|
const lines = [
|
|
2446
2706
|
`# Svelte 4 \u2192 5 migration summary`,
|
|
@@ -2457,7 +2717,7 @@ async function writeMigrationSummary(input) {
|
|
|
2457
2717
|
`- Verify Playwright a11y tests still pass.`
|
|
2458
2718
|
];
|
|
2459
2719
|
const content = lines.join("\n") + "\n";
|
|
2460
|
-
const path =
|
|
2720
|
+
const path = join15(input.cwd, "MIGRATION_SVELTE_5.md");
|
|
2461
2721
|
await writeFile7(path, content, "utf-8");
|
|
2462
2722
|
return path;
|
|
2463
2723
|
}
|
|
@@ -2465,7 +2725,7 @@ async function writeMigrationSummary(input) {
|
|
|
2465
2725
|
// src/recipes/svelte-5/index.ts
|
|
2466
2726
|
async function alreadyOnSvelte5(cwd) {
|
|
2467
2727
|
try {
|
|
2468
|
-
const pkg = await readPackageJson(
|
|
2728
|
+
const pkg = await readPackageJson(join16(cwd, "package.json"));
|
|
2469
2729
|
const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
|
|
2470
2730
|
return !!v && /^\^?5\./.test(v);
|
|
2471
2731
|
} catch {
|
|
@@ -2560,7 +2820,7 @@ import { resolve as resolve6 } from "path";
|
|
|
2560
2820
|
|
|
2561
2821
|
// src/recipes/convert-to-pnpm.ts
|
|
2562
2822
|
import { rm as rm3, stat as stat3 } from "fs/promises";
|
|
2563
|
-
import { join as
|
|
2823
|
+
import { join as join17 } from "path";
|
|
2564
2824
|
|
|
2565
2825
|
// src/recipes/convert-to-pnpm/script-rewrites.ts
|
|
2566
2826
|
function rewriteScriptForPnpm(script) {
|
|
@@ -2593,9 +2853,9 @@ async function exists2(path) {
|
|
|
2593
2853
|
async function convertToPnpm(site, opts = {}) {
|
|
2594
2854
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
2595
2855
|
const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
|
|
2596
|
-
const pnpmLockPath =
|
|
2597
|
-
const npmLockPath =
|
|
2598
|
-
const yarnLockPath =
|
|
2856
|
+
const pnpmLockPath = join17(site.path, "pnpm-lock.yaml");
|
|
2857
|
+
const npmLockPath = join17(site.path, "package-lock.json");
|
|
2858
|
+
const yarnLockPath = join17(site.path, "yarn.lock");
|
|
2599
2859
|
return withRecipe({
|
|
2600
2860
|
name: "convert-to-pnpm",
|
|
2601
2861
|
site,
|
|
@@ -2618,7 +2878,7 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
2618
2878
|
if (hasYarnLock) await rm3(yarnLockPath, { force: true });
|
|
2619
2879
|
const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
2620
2880
|
await commit2(`chore(pnpm): remove ${sourceLock}`);
|
|
2621
|
-
const pkgPath =
|
|
2881
|
+
const pkgPath = join17(cwd, "package.json");
|
|
2622
2882
|
const pkg = await readPackageJson(pkgPath);
|
|
2623
2883
|
const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
|
|
2624
2884
|
if (pkg.scripts && typeof pkg.scripts === "object") {
|
|
@@ -2631,7 +2891,7 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
2631
2891
|
}
|
|
2632
2892
|
await writePackageJson(pkgPath, next);
|
|
2633
2893
|
await commit2("chore(pnpm): pin packageManager + rewrite npm scripts");
|
|
2634
|
-
await rm3(
|
|
2894
|
+
await rm3(join17(cwd, "node_modules"), { recursive: true, force: true });
|
|
2635
2895
|
const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
|
|
2636
2896
|
if (installResult.code !== 0) {
|
|
2637
2897
|
return { kind: "failed", notes: `pnpm install failed (exit ${installResult.code})` };
|
|
@@ -2672,16 +2932,16 @@ import { resolve as resolve7 } from "path";
|
|
|
2672
2932
|
|
|
2673
2933
|
// src/recipes/onboard.ts
|
|
2674
2934
|
import { stat as stat4 } from "fs/promises";
|
|
2675
|
-
import { join as
|
|
2935
|
+
import { join as join19 } from "path";
|
|
2676
2936
|
|
|
2677
2937
|
// src/util/self-version.ts
|
|
2678
2938
|
import { readFileSync } from "fs";
|
|
2679
2939
|
import { fileURLToPath } from "url";
|
|
2680
|
-
import { dirname, join as
|
|
2940
|
+
import { dirname, join as join18 } from "path";
|
|
2681
2941
|
function selfPackageVersion(callerImportMetaUrl) {
|
|
2682
2942
|
try {
|
|
2683
|
-
const
|
|
2684
|
-
const raw = readFileSync(
|
|
2943
|
+
const here3 = dirname(fileURLToPath(callerImportMetaUrl));
|
|
2944
|
+
const raw = readFileSync(join18(here3, "..", "..", "package.json"), "utf-8");
|
|
2685
2945
|
const pkg = JSON.parse(raw);
|
|
2686
2946
|
return pkg.version ?? "0.0.0";
|
|
2687
2947
|
} catch {
|
|
@@ -2731,13 +2991,13 @@ async function onboard(site, opts = {}) {
|
|
|
2731
2991
|
name: "onboard",
|
|
2732
2992
|
site,
|
|
2733
2993
|
plan: async () => {
|
|
2734
|
-
if (!await exists3(
|
|
2994
|
+
if (!await exists3(join19(site.path, "pnpm-lock.yaml"))) {
|
|
2735
2995
|
return {
|
|
2736
2996
|
kind: "failed",
|
|
2737
2997
|
notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
|
|
2738
2998
|
};
|
|
2739
2999
|
}
|
|
2740
|
-
const pkgPath =
|
|
3000
|
+
const pkgPath = join19(site.path, "package.json");
|
|
2741
3001
|
const pkg = await readPackageJson(pkgPath);
|
|
2742
3002
|
const toAdd = [];
|
|
2743
3003
|
if (!isDeclared(pkg, PACKAGE_NAME)) {
|
|
@@ -2757,7 +3017,7 @@ async function onboard(site, opts = {}) {
|
|
|
2757
3017
|
return { kind: "apply", plan: { pkg, toAdd } };
|
|
2758
3018
|
},
|
|
2759
3019
|
apply: async ({ pkg, toAdd }, { commit: commit2, cwd }) => {
|
|
2760
|
-
const pkgPath =
|
|
3020
|
+
const pkgPath = join19(cwd, "package.json");
|
|
2761
3021
|
let next = pkg;
|
|
2762
3022
|
for (const dep of toAdd) {
|
|
2763
3023
|
next = bumpDep(next, dep.name, dep.version);
|
|
@@ -2826,7 +3086,7 @@ import { resolve as resolve8 } from "path";
|
|
|
2826
3086
|
|
|
2827
3087
|
// src/recipes/svelte-codemods.ts
|
|
2828
3088
|
import { writeFile as writeFile8 } from "fs/promises";
|
|
2829
|
-
import { join as
|
|
3089
|
+
import { join as join20 } from "path";
|
|
2830
3090
|
async function svelteCodemods(site) {
|
|
2831
3091
|
return withRecipe({
|
|
2832
3092
|
name: "svelte-codemods",
|
|
@@ -2840,7 +3100,7 @@ async function svelteCodemods(site) {
|
|
|
2840
3100
|
},
|
|
2841
3101
|
apply: async (changes, { commit: commit2, cwd }) => {
|
|
2842
3102
|
for (const c of changes) {
|
|
2843
|
-
await writeFile8(
|
|
3103
|
+
await writeFile8(join20(cwd, c.rel), c.after, "utf-8");
|
|
2844
3104
|
}
|
|
2845
3105
|
await commit2(`refactor(svelte5): apply codemods (${changes.length} files)`);
|
|
2846
3106
|
return { kind: "ok" };
|
|
@@ -2879,6 +3139,12 @@ init_websites();
|
|
|
2879
3139
|
init_reports();
|
|
2880
3140
|
|
|
2881
3141
|
// src/reports/due.ts
|
|
3142
|
+
var ELIGIBLE_STATUSES = /* @__PURE__ */ new Set([
|
|
3143
|
+
"in development",
|
|
3144
|
+
"launch period",
|
|
3145
|
+
"maintenance",
|
|
3146
|
+
"hosting"
|
|
3147
|
+
]);
|
|
2882
3148
|
var MONTHS = {
|
|
2883
3149
|
Monthly: 1,
|
|
2884
3150
|
Quarterly: 3,
|
|
@@ -2908,6 +3174,7 @@ function findDueReports(websites, reports, today) {
|
|
|
2908
3174
|
const out = [];
|
|
2909
3175
|
const todayStart = startOfDay(today);
|
|
2910
3176
|
for (const site of websites) {
|
|
3177
|
+
if (site.status !== null && !ELIGIBLE_STATUSES.has(site.status)) continue;
|
|
2911
3178
|
for (const type of ["Maintenance", "Testing"]) {
|
|
2912
3179
|
const freq = type === "Maintenance" ? site.maintenanceFreq : site.testingFreq;
|
|
2913
3180
|
if (freq === "None") continue;
|
|
@@ -2931,8 +3198,9 @@ function findDueReports(websites, reports, today) {
|
|
|
2931
3198
|
init_render();
|
|
2932
3199
|
init_websites();
|
|
2933
3200
|
init_reports();
|
|
3201
|
+
init_attachments();
|
|
2934
3202
|
import { mkdir as mkdir2, writeFile as writeFile9 } from "fs/promises";
|
|
2935
|
-
import { dirname as
|
|
3203
|
+
import { dirname as dirname3 } from "path";
|
|
2936
3204
|
function scoresFromWebsite(siteRow) {
|
|
2937
3205
|
const { pScore, rScore, bpScore, seoScore } = siteRow;
|
|
2938
3206
|
if (pScore === null || rScore === null || bpScore === null || seoScore === null) {
|
|
@@ -2970,7 +3238,7 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
2970
3238
|
});
|
|
2971
3239
|
if (options.previewOnly) {
|
|
2972
3240
|
const path = options.previewPath ?? `reports/${slug}/draft.html`;
|
|
2973
|
-
await mkdir2(
|
|
3241
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
2974
3242
|
await writeFile9(path, html, "utf-8");
|
|
2975
3243
|
return { reportRow: null, htmlPath: path, html };
|
|
2976
3244
|
}
|
|
@@ -2986,7 +3254,8 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
2986
3254
|
lighthouse: scores,
|
|
2987
3255
|
lastTestedDate
|
|
2988
3256
|
});
|
|
2989
|
-
|
|
3257
|
+
const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
3258
|
+
await uploadAttachment(created.id, "Rendered HTML", html, htmlFilename, "text/html");
|
|
2990
3259
|
await setDraftReady(base, created.id, true);
|
|
2991
3260
|
return { reportRow: created, htmlPath: null, html };
|
|
2992
3261
|
}
|
|
@@ -2996,28 +3265,6 @@ async function derivePeriodStart(base, siteRow, reportType, today) {
|
|
|
2996
3265
|
const latest = sameType[sameType.length - 1];
|
|
2997
3266
|
return latest ? new Date(latest) : daysAgo(today, 30);
|
|
2998
3267
|
}
|
|
2999
|
-
async function uploadHtmlAttachment(recordId, html, slug, periodEnd) {
|
|
3000
|
-
const apiKey = process.env.AIRTABLE_PAT;
|
|
3001
|
-
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
3002
|
-
const filename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
3003
|
-
const body = {
|
|
3004
|
-
contentType: "text/html",
|
|
3005
|
-
file: Buffer.from(html, "utf-8").toString("base64"),
|
|
3006
|
-
filename
|
|
3007
|
-
};
|
|
3008
|
-
const url = `https://content.airtable.com/v0/${baseId}/${recordId}/Rendered%20HTML/uploadAttachment`;
|
|
3009
|
-
const res = await fetch(url, {
|
|
3010
|
-
method: "POST",
|
|
3011
|
-
headers: {
|
|
3012
|
-
Authorization: `Bearer ${apiKey}`,
|
|
3013
|
-
"Content-Type": "application/json"
|
|
3014
|
-
},
|
|
3015
|
-
body: JSON.stringify(body)
|
|
3016
|
-
});
|
|
3017
|
-
if (!res.ok) {
|
|
3018
|
-
throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
3268
|
|
|
3022
3269
|
// src/cli/commands/report.ts
|
|
3023
3270
|
async function runReportCommand(slug, opts) {
|
|
@@ -3077,10 +3324,10 @@ async function runSingleSiteDraft(slug, opts) {
|
|
|
3077
3324
|
|
|
3078
3325
|
// src/cli/version.ts
|
|
3079
3326
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3080
|
-
import { join as
|
|
3327
|
+
import { join as join22 } from "path";
|
|
3081
3328
|
function resolvePackageVersion(fromDir) {
|
|
3082
3329
|
try {
|
|
3083
|
-
const raw = readFileSync2(
|
|
3330
|
+
const raw = readFileSync2(join22(fromDir, "..", "..", "package.json"), "utf-8");
|
|
3084
3331
|
const pkg = JSON.parse(raw);
|
|
3085
3332
|
return pkg.version ?? "unknown";
|
|
3086
3333
|
} catch {
|
|
@@ -3089,8 +3336,8 @@ function resolvePackageVersion(fromDir) {
|
|
|
3089
3336
|
}
|
|
3090
3337
|
|
|
3091
3338
|
// src/cli/bin.ts
|
|
3092
|
-
var
|
|
3093
|
-
var version = resolvePackageVersion(
|
|
3339
|
+
var here2 = dirname4(fileURLToPath3(import.meta.url));
|
|
3340
|
+
var version = resolvePackageVersion(here2);
|
|
3094
3341
|
var AUDIT_DESCRIPTIONS = {
|
|
3095
3342
|
deps: "Diff site package.json against the bundled baseline version map.",
|
|
3096
3343
|
lighthouse: "Run @lhci/cli autorun using the canonical lighthouserc.",
|
|
@@ -3130,31 +3377,55 @@ cli.command("list-recipes", "Print the available recipes.").action(() => {
|
|
|
3130
3377
|
console.log(`${name.padEnd(16)} ${desc}`);
|
|
3131
3378
|
}
|
|
3132
3379
|
});
|
|
3133
|
-
cli.command("audit [site]", "Run audits against a site (default: cwd).").option("--only <names>", "Comma-separated audit names (e.g. deps,lighthouse)").option("--json", "Machine-readable JSON output").option(
|
|
3380
|
+
cli.command("audit [site]", "Run audits against a site (default: cwd).").option("--only <names>", "Comma-separated audit names (e.g. deps,lighthouse)").option("--json", "Machine-readable JSON output").option(
|
|
3381
|
+
"--fleet <inventory>",
|
|
3382
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3383
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").option(
|
|
3384
|
+
"--write-airtable [slug]",
|
|
3385
|
+
"After lighthouse runs, write pScore/rScore/bpScore/seoScore + timestamp to the matching Websites row. Slug defaults to cwd's package.json#name."
|
|
3386
|
+
).action(
|
|
3134
3387
|
async (site, opts) => runOrExit(() => runAuditCommand(site, opts), opts)
|
|
3135
3388
|
);
|
|
3136
|
-
cli.command("sync-configs [site]", "Sync canonical configs into a site.").option("--only <names>", "Comma-separated config names (e.g. eslint,prettier)").option("--dry", "Print diff without writing").option(
|
|
3389
|
+
cli.command("sync-configs [site]", "Sync canonical configs into a site.").option("--only <names>", "Comma-separated config names (e.g. eslint,prettier)").option("--dry", "Print diff without writing").option(
|
|
3390
|
+
"--fleet <inventory>",
|
|
3391
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3392
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3137
3393
|
async (site, opts) => runOrExit(() => runSyncConfigsCommand(site, opts), opts)
|
|
3138
3394
|
);
|
|
3139
|
-
cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>", "patch | minor | major", { default: "minor" }).option(
|
|
3395
|
+
cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>", "patch | minor | major", { default: "minor" }).option(
|
|
3396
|
+
"--fleet <inventory>",
|
|
3397
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3398
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3140
3399
|
async (site, opts) => runOrExit(() => runBumpDepsCommand(site, opts), opts)
|
|
3141
3400
|
);
|
|
3142
|
-
cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to-5).").example("reddoor-maint upgrade svelte-4-to-5 ./my-site").option(
|
|
3401
|
+
cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to-5).").example("reddoor-maint upgrade svelte-4-to-5 ./my-site").option(
|
|
3402
|
+
"--fleet <inventory>",
|
|
3403
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3404
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3143
3405
|
async (upgrade, site, opts) => runOrExit(() => runUpgradeCommand(upgrade, site, opts), opts)
|
|
3144
3406
|
);
|
|
3145
3407
|
cli.command(
|
|
3146
3408
|
"convert-to-pnpm [site]",
|
|
3147
3409
|
"Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts)."
|
|
3148
|
-
).option(
|
|
3410
|
+
).option(
|
|
3411
|
+
"--fleet <inventory>",
|
|
3412
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3413
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3149
3414
|
async (site, opts) => runOrExit(() => runConvertToPnpmCommand(site, opts), opts)
|
|
3150
3415
|
);
|
|
3151
|
-
cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an already-migrated site.").option(
|
|
3416
|
+
cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an already-migrated site.").option(
|
|
3417
|
+
"--fleet <inventory>",
|
|
3418
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3419
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3152
3420
|
async (site, opts) => runOrExit(() => runSvelteCodemodsCommand(site, opts), opts)
|
|
3153
3421
|
);
|
|
3154
3422
|
cli.command(
|
|
3155
3423
|
"onboard [site]",
|
|
3156
3424
|
"Install @reddoorla/maintenance + audit deps on a site (run after convert-to-pnpm)."
|
|
3157
|
-
).option("--audits <names>", "Comma-separated audit subset: lighthouse,a11y (default: both)").option(
|
|
3425
|
+
).option("--audits <names>", "Comma-separated audit subset: lighthouse,a11y (default: both)").option(
|
|
3426
|
+
"--fleet <inventory>",
|
|
3427
|
+
'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
|
|
3428
|
+
).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
|
|
3158
3429
|
async (site, opts) => runOrExit(() => runOnboardCommand(site, opts), opts)
|
|
3159
3430
|
);
|
|
3160
3431
|
cli.command("report [site]", "Draft or send maintenance/testing reports.").option("--due", "Scan all Websites and draft overdue reports.").option(
|