@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/index.js
CHANGED
|
@@ -335,9 +335,9 @@ async function securityAudit(ctx) {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
// src/audits/lighthouse.ts
|
|
338
|
-
import { readFile as
|
|
338
|
+
import { readFile as readFile4, writeFile, mkdtemp, rm } from "fs/promises";
|
|
339
339
|
import { tmpdir } from "os";
|
|
340
|
-
import { join as
|
|
340
|
+
import { join as join4 } from "path";
|
|
341
341
|
|
|
342
342
|
// src/configs/lighthouse.ts
|
|
343
343
|
var lighthouseConfig = {
|
|
@@ -370,10 +370,37 @@ var lighthouseConfig = {
|
|
|
370
370
|
}
|
|
371
371
|
};
|
|
372
372
|
|
|
373
|
+
// src/audits/util/site-config.ts
|
|
374
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
375
|
+
import { join as join3 } from "path";
|
|
376
|
+
async function readSiteConfig(sitePath) {
|
|
377
|
+
let raw;
|
|
378
|
+
try {
|
|
379
|
+
raw = await readFile3(join3(sitePath, "package.json"), "utf-8");
|
|
380
|
+
} catch {
|
|
381
|
+
return {};
|
|
382
|
+
}
|
|
383
|
+
let pkg;
|
|
384
|
+
try {
|
|
385
|
+
pkg = JSON.parse(raw);
|
|
386
|
+
} catch {
|
|
387
|
+
return {};
|
|
388
|
+
}
|
|
389
|
+
if (!pkg || typeof pkg !== "object") return {};
|
|
390
|
+
const cfg = pkg.reddoor;
|
|
391
|
+
if (!cfg || typeof cfg !== "object") return {};
|
|
392
|
+
const out = {};
|
|
393
|
+
const url = cfg.lighthouseUrl;
|
|
394
|
+
if (typeof url === "string" && url.length > 0) {
|
|
395
|
+
out.lighthouseUrl = url;
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
|
|
373
400
|
// src/audits/lighthouse.ts
|
|
374
401
|
async function readJsonMaybe(path) {
|
|
375
402
|
try {
|
|
376
|
-
const raw = await
|
|
403
|
+
const raw = await readFile4(path, "utf-8");
|
|
377
404
|
return JSON.parse(raw);
|
|
378
405
|
} catch {
|
|
379
406
|
return null;
|
|
@@ -409,15 +436,28 @@ async function lighthouseAudit(ctx) {
|
|
|
409
436
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
410
437
|
const site = ctx.site;
|
|
411
438
|
const label = siteLabel(site);
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
439
|
+
const siteCfg = await readSiteConfig(site.path);
|
|
440
|
+
const resolvedConfig = siteCfg.lighthouseUrl ? {
|
|
441
|
+
...lighthouseConfig,
|
|
442
|
+
ci: {
|
|
443
|
+
...lighthouseConfig.ci,
|
|
444
|
+
collect: { ...lighthouseConfig.ci.collect, url: [siteCfg.lighthouseUrl] }
|
|
445
|
+
}
|
|
446
|
+
} : lighthouseConfig;
|
|
447
|
+
const configDir = await mkdtemp(join4(tmpdir(), "reddoor-lhci-"));
|
|
448
|
+
const configPath = join4(configDir, "lighthouserc.json");
|
|
449
|
+
await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
|
|
450
|
+
const resultsDir = join4(site.path, ".lighthouseci");
|
|
416
451
|
await rm(resultsDir, { recursive: true, force: true });
|
|
417
452
|
let raw;
|
|
418
453
|
try {
|
|
419
454
|
raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
|
|
420
|
-
cwd: site.path
|
|
455
|
+
cwd: site.path,
|
|
456
|
+
// lhci autorun boots the site's dev server, downloads Chrome on first
|
|
457
|
+
// use, and runs the audit — easily 2–3 min on a cold tree. The shared
|
|
458
|
+
// 30 s default in runAudits is fine for deps/lint/security but starves
|
|
459
|
+
// lhci.
|
|
460
|
+
timeoutMs: 5 * 6e4
|
|
421
461
|
});
|
|
422
462
|
} catch (err) {
|
|
423
463
|
await rm(configDir, { recursive: true, force: true });
|
|
@@ -433,7 +473,7 @@ async function lighthouseAudit(ctx) {
|
|
|
433
473
|
throw err;
|
|
434
474
|
}
|
|
435
475
|
await rm(configDir, { recursive: true, force: true });
|
|
436
|
-
const manifest = await readJsonMaybe(
|
|
476
|
+
const manifest = await readJsonMaybe(join4(resultsDir, "manifest.json"));
|
|
437
477
|
if (!manifest || manifest.length === 0) {
|
|
438
478
|
return {
|
|
439
479
|
audit: "lighthouse",
|
|
@@ -442,7 +482,7 @@ async function lighthouseAudit(ctx) {
|
|
|
442
482
|
summary: `lighthouse: no manifest written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
|
|
443
483
|
};
|
|
444
484
|
}
|
|
445
|
-
const assertionResults = await readJsonMaybe(
|
|
485
|
+
const assertionResults = await readJsonMaybe(join4(resultsDir, "assertion-results.json")) ?? [];
|
|
446
486
|
const failed = assertionResults.filter((a) => !a.passed);
|
|
447
487
|
const assertions = failed.map((a) => ({
|
|
448
488
|
category: categoryFromAssertion(a),
|
|
@@ -468,9 +508,9 @@ async function lighthouseAudit(ctx) {
|
|
|
468
508
|
}
|
|
469
509
|
|
|
470
510
|
// src/audits/a11y.ts
|
|
471
|
-
import { readFile as
|
|
511
|
+
import { readFile as readFile5, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
472
512
|
import { tmpdir as tmpdir2 } from "os";
|
|
473
|
-
import { join as
|
|
513
|
+
import { join as join5 } from "path";
|
|
474
514
|
|
|
475
515
|
// src/configs/playwright-a11y.ts
|
|
476
516
|
import { defineConfig, devices } from "@playwright/test";
|
|
@@ -508,7 +548,7 @@ var playwrightA11yConfig = defineConfig({
|
|
|
508
548
|
var RESULTS_REL = ".reddoor-a11y/results.json";
|
|
509
549
|
async function readJsonMaybe2(path) {
|
|
510
550
|
try {
|
|
511
|
-
const raw = await
|
|
551
|
+
const raw = await readFile5(path, "utf-8");
|
|
512
552
|
return JSON.parse(raw);
|
|
513
553
|
} catch {
|
|
514
554
|
return null;
|
|
@@ -564,11 +604,11 @@ async function a11yAudit(ctx) {
|
|
|
564
604
|
const spawn2 = ctx.spawn ?? defaultSpawn;
|
|
565
605
|
const site = ctx.site;
|
|
566
606
|
const label = siteLabel(site);
|
|
567
|
-
const specDir = await mkdtemp2(
|
|
568
|
-
const specPath =
|
|
607
|
+
const specDir = await mkdtemp2(join5(tmpdir2(), "reddoor-a11y-spec-"));
|
|
608
|
+
const specPath = join5(specDir, "a11y.spec.ts");
|
|
569
609
|
await writeFile2(specPath, buildSpec(), "utf-8");
|
|
570
|
-
const resultsPath =
|
|
571
|
-
await rm2(
|
|
610
|
+
const resultsPath = join5(site.path, RESULTS_REL);
|
|
611
|
+
await rm2(join5(site.path, ".reddoor-a11y"), { recursive: true, force: true });
|
|
572
612
|
let raw;
|
|
573
613
|
try {
|
|
574
614
|
raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=line", specPath], {
|
|
@@ -650,8 +690,8 @@ async function runAuditsAcross(sites, which) {
|
|
|
650
690
|
}
|
|
651
691
|
|
|
652
692
|
// src/recipes/sync-configs.ts
|
|
653
|
-
import { readFile as
|
|
654
|
-
import { join as
|
|
693
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
694
|
+
import { join as join6 } from "path";
|
|
655
695
|
|
|
656
696
|
// src/recipes/sync-configs/templates.ts
|
|
657
697
|
var eslint = {
|
|
@@ -904,7 +944,7 @@ async function withRecipe(body) {
|
|
|
904
944
|
var GITIGNORE_CONFIG = "gitignore";
|
|
905
945
|
async function readMaybe(path) {
|
|
906
946
|
try {
|
|
907
|
-
return await
|
|
947
|
+
return await readFile6(path, "utf-8");
|
|
908
948
|
} catch {
|
|
909
949
|
return null;
|
|
910
950
|
}
|
|
@@ -912,13 +952,13 @@ async function readMaybe(path) {
|
|
|
912
952
|
async function planTemplateDiffs(cwd, templates) {
|
|
913
953
|
const diffs = [];
|
|
914
954
|
for (const t of templates) {
|
|
915
|
-
const existing = await readMaybe(
|
|
955
|
+
const existing = await readMaybe(join6(cwd, t.path));
|
|
916
956
|
if (existing !== t.contents) diffs.push(t);
|
|
917
957
|
}
|
|
918
958
|
return diffs;
|
|
919
959
|
}
|
|
920
960
|
async function planGitignore(cwd) {
|
|
921
|
-
const existing = await readMaybe(
|
|
961
|
+
const existing = await readMaybe(join6(cwd, ".gitignore"));
|
|
922
962
|
const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
|
|
923
963
|
const tracked = await listTrackedFiles(cwd);
|
|
924
964
|
const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);
|
|
@@ -926,7 +966,7 @@ async function planGitignore(cwd) {
|
|
|
926
966
|
return { kind: "apply", content: merge.content, toUntrack, added: merge.added };
|
|
927
967
|
}
|
|
928
968
|
async function applyGitignore(cwd, plan) {
|
|
929
|
-
await writeFile3(
|
|
969
|
+
await writeFile3(join6(cwd, ".gitignore"), plan.content, "utf-8");
|
|
930
970
|
if (plan.toUntrack.length > 0) {
|
|
931
971
|
await removeFromIndex(cwd, plan.toUntrack);
|
|
932
972
|
}
|
|
@@ -949,7 +989,7 @@ async function syncConfigs(site, opts = {}) {
|
|
|
949
989
|
},
|
|
950
990
|
apply: async ({ templateDiffs, gitignorePlan }, { commit: commit2 }) => {
|
|
951
991
|
for (const t of templateDiffs) {
|
|
952
|
-
await writeFile3(
|
|
992
|
+
await writeFile3(join6(site.path, t.path), t.contents, "utf-8");
|
|
953
993
|
await commit2(`chore: sync ${t.config} config from @reddoorla/maintenance`);
|
|
954
994
|
}
|
|
955
995
|
if (gitignorePlan.kind === "apply") {
|
|
@@ -963,7 +1003,7 @@ async function syncConfigs(site, opts = {}) {
|
|
|
963
1003
|
|
|
964
1004
|
// src/recipes/bump-deps.ts
|
|
965
1005
|
import { stat } from "fs/promises";
|
|
966
|
-
import { join as
|
|
1006
|
+
import { join as join7 } from "path";
|
|
967
1007
|
async function exists(path) {
|
|
968
1008
|
try {
|
|
969
1009
|
await stat(path);
|
|
@@ -992,10 +1032,10 @@ async function bumpDeps(site, opts = {}) {
|
|
|
992
1032
|
// land on top of whatever else was in the tree.
|
|
993
1033
|
checkTreeFirst: true,
|
|
994
1034
|
plan: async () => {
|
|
995
|
-
const hasPnpmLock = await exists(
|
|
1035
|
+
const hasPnpmLock = await exists(join7(site.path, "pnpm-lock.yaml"));
|
|
996
1036
|
if (!hasPnpmLock) {
|
|
997
|
-
const hasNpmLock = await exists(
|
|
998
|
-
const hasYarnLock = await exists(
|
|
1037
|
+
const hasNpmLock = await exists(join7(site.path, "package-lock.json"));
|
|
1038
|
+
const hasYarnLock = await exists(join7(site.path, "yarn.lock"));
|
|
999
1039
|
if (hasNpmLock || hasYarnLock) {
|
|
1000
1040
|
const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
1001
1041
|
return {
|
|
@@ -1030,12 +1070,12 @@ async function bumpDeps(site, opts = {}) {
|
|
|
1030
1070
|
}
|
|
1031
1071
|
|
|
1032
1072
|
// src/recipes/svelte-5/index.ts
|
|
1033
|
-
import { join as
|
|
1073
|
+
import { join as join13 } from "path";
|
|
1034
1074
|
|
|
1035
1075
|
// src/util/pkg.ts
|
|
1036
|
-
import { readFile as
|
|
1076
|
+
import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
|
|
1037
1077
|
async function readPackageJson(path) {
|
|
1038
|
-
const raw = await
|
|
1078
|
+
const raw = await readFile7(path, "utf-8");
|
|
1039
1079
|
return JSON.parse(raw);
|
|
1040
1080
|
}
|
|
1041
1081
|
function detectIndentFromContent(raw) {
|
|
@@ -1045,7 +1085,7 @@ function detectIndentFromContent(raw) {
|
|
|
1045
1085
|
async function writePackageJson(path, pkg) {
|
|
1046
1086
|
let indent = " ";
|
|
1047
1087
|
try {
|
|
1048
|
-
const existing = await
|
|
1088
|
+
const existing = await readFile7(path, "utf-8");
|
|
1049
1089
|
indent = detectIndentFromContent(existing);
|
|
1050
1090
|
} catch {
|
|
1051
1091
|
}
|
|
@@ -1079,7 +1119,7 @@ function bumpDep(pkg, name, version, opts = {}) {
|
|
|
1079
1119
|
}
|
|
1080
1120
|
|
|
1081
1121
|
// src/recipes/svelte-5/step-bump-versions.ts
|
|
1082
|
-
import { join as
|
|
1122
|
+
import { join as join8 } from "path";
|
|
1083
1123
|
var SVELTE_5_VERSIONS = {
|
|
1084
1124
|
svelte: "^5.55.5",
|
|
1085
1125
|
"@sveltejs/kit": "^2.59.0",
|
|
@@ -1092,7 +1132,7 @@ var SVELTE_5_VERSIONS = {
|
|
|
1092
1132
|
"typescript-svelte-plugin": "^0.3.52"
|
|
1093
1133
|
};
|
|
1094
1134
|
async function bumpToSvelte5Versions(cwd) {
|
|
1095
|
-
const pkgPath =
|
|
1135
|
+
const pkgPath = join8(cwd, "package.json");
|
|
1096
1136
|
const pkg = await readPackageJson(pkgPath);
|
|
1097
1137
|
let next = pkg;
|
|
1098
1138
|
for (const [name, version] of Object.entries(SVELTE_5_VERSIONS)) {
|
|
@@ -1104,8 +1144,8 @@ async function bumpToSvelte5Versions(cwd) {
|
|
|
1104
1144
|
}
|
|
1105
1145
|
|
|
1106
1146
|
// src/recipes/svelte-5/step-svelte-config.ts
|
|
1107
|
-
import { readFile as
|
|
1108
|
-
import { join as
|
|
1147
|
+
import { readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
|
|
1148
|
+
import { join as join9 } from "path";
|
|
1109
1149
|
var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
|
|
1110
1150
|
var IMPORT_FROM_VITE_PLUGIN = new RegExp(
|
|
1111
1151
|
String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
|
|
@@ -1146,10 +1186,10 @@ function dropPreprocessKey(source) {
|
|
|
1146
1186
|
return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
|
|
1147
1187
|
}
|
|
1148
1188
|
async function migrateSvelteConfig(cwd) {
|
|
1149
|
-
const path =
|
|
1189
|
+
const path = join9(cwd, "svelte.config.js");
|
|
1150
1190
|
let src;
|
|
1151
1191
|
try {
|
|
1152
|
-
src = await
|
|
1192
|
+
src = await readFile8(path, "utf-8");
|
|
1153
1193
|
} catch {
|
|
1154
1194
|
return false;
|
|
1155
1195
|
}
|
|
@@ -1183,9 +1223,9 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
|
|
|
1183
1223
|
}
|
|
1184
1224
|
|
|
1185
1225
|
// src/recipes/svelte-5/step-tailwind-upgrade.ts
|
|
1186
|
-
import { join as
|
|
1226
|
+
import { join as join10 } from "path";
|
|
1187
1227
|
async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
1188
|
-
const pkg = await readPackageJson(
|
|
1228
|
+
const pkg = await readPackageJson(join10(cwd, "package.json"));
|
|
1189
1229
|
const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
|
|
1190
1230
|
if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
|
|
1191
1231
|
if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
|
|
@@ -1206,8 +1246,8 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
|
1206
1246
|
}
|
|
1207
1247
|
|
|
1208
1248
|
// src/recipes/svelte-5/step-gotchas.ts
|
|
1209
|
-
import { readFile as
|
|
1210
|
-
import { join as
|
|
1249
|
+
import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
|
|
1250
|
+
import { join as join11 } from "path";
|
|
1211
1251
|
import { glob as glob2 } from "tinyglobby";
|
|
1212
1252
|
|
|
1213
1253
|
// src/recipes/svelte-5/codemods/on-event-to-handler.ts
|
|
@@ -1539,8 +1579,8 @@ async function planGotchaCodemods(cwd) {
|
|
|
1539
1579
|
const changes = [];
|
|
1540
1580
|
const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
|
|
1541
1581
|
for (const rel of relPaths) {
|
|
1542
|
-
const path =
|
|
1543
|
-
const before = await
|
|
1582
|
+
const path = join11(cwd, rel);
|
|
1583
|
+
const before = await readFile9(path, "utf-8");
|
|
1544
1584
|
const after = CODEMODS.reduce((s, fn) => fn(s), before);
|
|
1545
1585
|
if (after !== before) changes.push({ rel, after });
|
|
1546
1586
|
}
|
|
@@ -1549,7 +1589,7 @@ async function planGotchaCodemods(cwd) {
|
|
|
1549
1589
|
async function applyGotchaCodemods(cwd) {
|
|
1550
1590
|
const changes = await planGotchaCodemods(cwd);
|
|
1551
1591
|
for (const c of changes) {
|
|
1552
|
-
await writeFile6(
|
|
1592
|
+
await writeFile6(join11(cwd, c.rel), c.after, "utf-8");
|
|
1553
1593
|
}
|
|
1554
1594
|
return { filesChanged: changes.length };
|
|
1555
1595
|
}
|
|
@@ -1573,7 +1613,7 @@ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
|
|
|
1573
1613
|
|
|
1574
1614
|
// src/recipes/svelte-5/step-summary.ts
|
|
1575
1615
|
import { writeFile as writeFile7 } from "fs/promises";
|
|
1576
|
-
import { join as
|
|
1616
|
+
import { join as join12 } from "path";
|
|
1577
1617
|
async function writeMigrationSummary(input) {
|
|
1578
1618
|
const lines = [
|
|
1579
1619
|
`# Svelte 4 \u2192 5 migration summary`,
|
|
@@ -1590,7 +1630,7 @@ async function writeMigrationSummary(input) {
|
|
|
1590
1630
|
`- Verify Playwright a11y tests still pass.`
|
|
1591
1631
|
];
|
|
1592
1632
|
const content = lines.join("\n") + "\n";
|
|
1593
|
-
const path =
|
|
1633
|
+
const path = join12(input.cwd, "MIGRATION_SVELTE_5.md");
|
|
1594
1634
|
await writeFile7(path, content, "utf-8");
|
|
1595
1635
|
return path;
|
|
1596
1636
|
}
|
|
@@ -1598,7 +1638,7 @@ async function writeMigrationSummary(input) {
|
|
|
1598
1638
|
// src/recipes/svelte-5/index.ts
|
|
1599
1639
|
async function alreadyOnSvelte5(cwd) {
|
|
1600
1640
|
try {
|
|
1601
|
-
const pkg = await readPackageJson(
|
|
1641
|
+
const pkg = await readPackageJson(join13(cwd, "package.json"));
|
|
1602
1642
|
const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
|
|
1603
1643
|
return !!v && /^\^?5\./.test(v);
|
|
1604
1644
|
} catch {
|
|
@@ -1653,7 +1693,7 @@ async function upgradeSvelte4to5(site, opts = {}) {
|
|
|
1653
1693
|
|
|
1654
1694
|
// src/recipes/svelte-codemods.ts
|
|
1655
1695
|
import { writeFile as writeFile8 } from "fs/promises";
|
|
1656
|
-
import { join as
|
|
1696
|
+
import { join as join14 } from "path";
|
|
1657
1697
|
async function svelteCodemods(site) {
|
|
1658
1698
|
return withRecipe({
|
|
1659
1699
|
name: "svelte-codemods",
|
|
@@ -1667,7 +1707,7 @@ async function svelteCodemods(site) {
|
|
|
1667
1707
|
},
|
|
1668
1708
|
apply: async (changes, { commit: commit2, cwd }) => {
|
|
1669
1709
|
for (const c of changes) {
|
|
1670
|
-
await writeFile8(
|
|
1710
|
+
await writeFile8(join14(cwd, c.rel), c.after, "utf-8");
|
|
1671
1711
|
}
|
|
1672
1712
|
await commit2(`refactor(svelte5): apply codemods (${changes.length} files)`);
|
|
1673
1713
|
return { kind: "ok" };
|
|
@@ -1677,7 +1717,7 @@ async function svelteCodemods(site) {
|
|
|
1677
1717
|
|
|
1678
1718
|
// src/recipes/convert-to-pnpm.ts
|
|
1679
1719
|
import { rm as rm3, stat as stat2 } from "fs/promises";
|
|
1680
|
-
import { join as
|
|
1720
|
+
import { join as join15 } from "path";
|
|
1681
1721
|
|
|
1682
1722
|
// src/recipes/convert-to-pnpm/script-rewrites.ts
|
|
1683
1723
|
function rewriteScriptForPnpm(script) {
|
|
@@ -1710,9 +1750,9 @@ async function exists2(path) {
|
|
|
1710
1750
|
async function convertToPnpm(site, opts = {}) {
|
|
1711
1751
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
1712
1752
|
const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
|
|
1713
|
-
const pnpmLockPath =
|
|
1714
|
-
const npmLockPath =
|
|
1715
|
-
const yarnLockPath =
|
|
1753
|
+
const pnpmLockPath = join15(site.path, "pnpm-lock.yaml");
|
|
1754
|
+
const npmLockPath = join15(site.path, "package-lock.json");
|
|
1755
|
+
const yarnLockPath = join15(site.path, "yarn.lock");
|
|
1716
1756
|
return withRecipe({
|
|
1717
1757
|
name: "convert-to-pnpm",
|
|
1718
1758
|
site,
|
|
@@ -1735,7 +1775,7 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
1735
1775
|
if (hasYarnLock) await rm3(yarnLockPath, { force: true });
|
|
1736
1776
|
const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
1737
1777
|
await commit2(`chore(pnpm): remove ${sourceLock}`);
|
|
1738
|
-
const pkgPath =
|
|
1778
|
+
const pkgPath = join15(cwd, "package.json");
|
|
1739
1779
|
const pkg = await readPackageJson(pkgPath);
|
|
1740
1780
|
const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
|
|
1741
1781
|
if (pkg.scripts && typeof pkg.scripts === "object") {
|
|
@@ -1748,7 +1788,7 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
1748
1788
|
}
|
|
1749
1789
|
await writePackageJson(pkgPath, next);
|
|
1750
1790
|
await commit2("chore(pnpm): pin packageManager + rewrite npm scripts");
|
|
1751
|
-
await rm3(
|
|
1791
|
+
await rm3(join15(cwd, "node_modules"), { recursive: true, force: true });
|
|
1752
1792
|
const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
|
|
1753
1793
|
if (installResult.code !== 0) {
|
|
1754
1794
|
return { kind: "failed", notes: `pnpm install failed (exit ${installResult.code})` };
|
|
@@ -1761,16 +1801,16 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
1761
1801
|
|
|
1762
1802
|
// src/recipes/onboard.ts
|
|
1763
1803
|
import { stat as stat3 } from "fs/promises";
|
|
1764
|
-
import { join as
|
|
1804
|
+
import { join as join17 } from "path";
|
|
1765
1805
|
|
|
1766
1806
|
// src/util/self-version.ts
|
|
1767
1807
|
import { readFileSync } from "fs";
|
|
1768
1808
|
import { fileURLToPath } from "url";
|
|
1769
|
-
import { dirname, join as
|
|
1809
|
+
import { dirname, join as join16 } from "path";
|
|
1770
1810
|
function selfPackageVersion(callerImportMetaUrl) {
|
|
1771
1811
|
try {
|
|
1772
|
-
const
|
|
1773
|
-
const raw = readFileSync(
|
|
1812
|
+
const here2 = dirname(fileURLToPath(callerImportMetaUrl));
|
|
1813
|
+
const raw = readFileSync(join16(here2, "..", "..", "package.json"), "utf-8");
|
|
1774
1814
|
const pkg = JSON.parse(raw);
|
|
1775
1815
|
return pkg.version ?? "0.0.0";
|
|
1776
1816
|
} catch {
|
|
@@ -1820,13 +1860,13 @@ async function onboard(site, opts = {}) {
|
|
|
1820
1860
|
name: "onboard",
|
|
1821
1861
|
site,
|
|
1822
1862
|
plan: async () => {
|
|
1823
|
-
if (!await exists3(
|
|
1863
|
+
if (!await exists3(join17(site.path, "pnpm-lock.yaml"))) {
|
|
1824
1864
|
return {
|
|
1825
1865
|
kind: "failed",
|
|
1826
1866
|
notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
|
|
1827
1867
|
};
|
|
1828
1868
|
}
|
|
1829
|
-
const pkgPath =
|
|
1869
|
+
const pkgPath = join17(site.path, "package.json");
|
|
1830
1870
|
const pkg = await readPackageJson(pkgPath);
|
|
1831
1871
|
const toAdd = [];
|
|
1832
1872
|
if (!isDeclared(pkg, PACKAGE_NAME)) {
|
|
@@ -1846,7 +1886,7 @@ async function onboard(site, opts = {}) {
|
|
|
1846
1886
|
return { kind: "apply", plan: { pkg, toAdd } };
|
|
1847
1887
|
},
|
|
1848
1888
|
apply: async ({ pkg, toAdd }, { commit: commit2, cwd }) => {
|
|
1849
|
-
const pkgPath =
|
|
1889
|
+
const pkgPath = join17(cwd, "package.json");
|
|
1850
1890
|
let next = pkg;
|
|
1851
1891
|
for (const dep of toAdd) {
|
|
1852
1892
|
next = bumpDep(next, dep.name, dep.version);
|
|
@@ -1889,7 +1929,7 @@ function localPath(path, opts = {}) {
|
|
|
1889
1929
|
}
|
|
1890
1930
|
|
|
1891
1931
|
// src/inventory/json.ts
|
|
1892
|
-
import { readFile as
|
|
1932
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1893
1933
|
import { isAbsolute } from "path";
|
|
1894
1934
|
function validate(raw) {
|
|
1895
1935
|
if (!Array.isArray(raw)) {
|
|
@@ -1919,21 +1959,111 @@ function validate(raw) {
|
|
|
1919
1959
|
}
|
|
1920
1960
|
function fromJsonFile(path) {
|
|
1921
1961
|
return async () => {
|
|
1922
|
-
const raw = JSON.parse(await
|
|
1962
|
+
const raw = JSON.parse(await readFile10(path, "utf-8"));
|
|
1923
1963
|
return validate(raw);
|
|
1924
1964
|
};
|
|
1925
1965
|
}
|
|
1926
1966
|
|
|
1967
|
+
// src/reports/airtable/websites.ts
|
|
1968
|
+
var WEBSITES_TABLE = "Websites";
|
|
1969
|
+
function siteSlug(name) {
|
|
1970
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1971
|
+
}
|
|
1972
|
+
function mapRow(rec) {
|
|
1973
|
+
const f = rec.fields;
|
|
1974
|
+
const attachments = f["Header image"] ?? [];
|
|
1975
|
+
const header = attachments[0] ?? null;
|
|
1976
|
+
return {
|
|
1977
|
+
id: rec.id,
|
|
1978
|
+
name: String(f["Name"] ?? ""),
|
|
1979
|
+
url: String(f["url"] ?? ""),
|
|
1980
|
+
status: f["Status"] ?? null,
|
|
1981
|
+
pointOfContact: f["point of contact"] ?? null,
|
|
1982
|
+
maintenanceFreq: f["maintenence freq"] ?? "None",
|
|
1983
|
+
testingFreq: f["testing freq"] ?? "None",
|
|
1984
|
+
maintenanceDay: f["maintenance day"] ?? null,
|
|
1985
|
+
testingDay: f["testing day"] ?? null,
|
|
1986
|
+
ga4PropertyId: f["GA4 property ID"] ?? null,
|
|
1987
|
+
reportRecipientsTo: f["Report recipients (To)"] ?? null,
|
|
1988
|
+
reportRecipientsCc: f["Report recipients (CC)"] ?? null,
|
|
1989
|
+
headerImage: header,
|
|
1990
|
+
pScore: f["pScore"] ?? null,
|
|
1991
|
+
rScore: f["rScore"] ?? null,
|
|
1992
|
+
bpScore: f["bpScore"] ?? null,
|
|
1993
|
+
seoScore: f["seoScore"] ?? null,
|
|
1994
|
+
lastLighthouseAuditAt: f["Last lighthouse audit at"] ?? null
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
async function listWebsites(base) {
|
|
1998
|
+
const out = [];
|
|
1999
|
+
await base(WEBSITES_TABLE).select({ pageSize: 100 }).eachPage((records, fetchNextPage) => {
|
|
2000
|
+
for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));
|
|
2001
|
+
fetchNextPage();
|
|
2002
|
+
});
|
|
2003
|
+
return out;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// src/inventory/airtable.ts
|
|
2007
|
+
function fromAirtableBase(base, opts = {}) {
|
|
2008
|
+
return async () => {
|
|
2009
|
+
const workdir = opts.workdir ?? process.env.REDDOOR_FLEET_WORKDIR;
|
|
2010
|
+
if (!workdir) {
|
|
2011
|
+
throw new Error(
|
|
2012
|
+
"fromAirtableBase requires `workdir` option or REDDOOR_FLEET_WORKDIR env (sites need a local path)"
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
const websites = await listWebsites(base);
|
|
2016
|
+
return websites.filter((w) => w.maintenanceFreq !== "None" || w.testingFreq !== "None").map((w) => {
|
|
2017
|
+
const slug = siteSlug(w.name);
|
|
2018
|
+
const site = {
|
|
2019
|
+
path: `${workdir}/${slug}`,
|
|
2020
|
+
name: slug,
|
|
2021
|
+
meta: { airtableRowId: w.id, displayName: w.name }
|
|
2022
|
+
};
|
|
2023
|
+
if (w.url) site.repoUrl = w.url;
|
|
2024
|
+
return site;
|
|
2025
|
+
});
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1927
2029
|
// src/reports/draft.ts
|
|
1928
2030
|
import { mkdir, writeFile as writeFile9 } from "fs/promises";
|
|
1929
|
-
import { dirname as
|
|
2031
|
+
import { dirname as dirname3 } from "path";
|
|
1930
2032
|
|
|
1931
2033
|
// src/reports/render.ts
|
|
1932
2034
|
import mjml2html from "mjml";
|
|
1933
2035
|
|
|
2036
|
+
// src/reports/maintenance-email/assets/index.ts
|
|
2037
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2038
|
+
import { dirname as dirname2, join as join18 } from "path";
|
|
2039
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2040
|
+
var here = dirname2(fileURLToPath2(import.meta.url));
|
|
2041
|
+
var CHECK_CID = "rd-check-png";
|
|
2042
|
+
var BLURRED_CID = "rd-blurred-tests-jpg";
|
|
2043
|
+
async function loadBundledImages() {
|
|
2044
|
+
const [check, blurred] = await Promise.all([
|
|
2045
|
+
readFile11(join18(here, "check.png")),
|
|
2046
|
+
readFile11(join18(here, "blurredTests.jpg"))
|
|
2047
|
+
]);
|
|
2048
|
+
return {
|
|
2049
|
+
check: {
|
|
2050
|
+
bytes: new Uint8Array(check),
|
|
2051
|
+
contentType: "image/png",
|
|
2052
|
+
cid: CHECK_CID,
|
|
2053
|
+
filename: "check.png"
|
|
2054
|
+
},
|
|
2055
|
+
blurred: {
|
|
2056
|
+
bytes: new Uint8Array(blurred),
|
|
2057
|
+
contentType: "image/jpeg",
|
|
2058
|
+
cid: BLURRED_CID,
|
|
2059
|
+
filename: "blurredTests.jpg"
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1934
2064
|
// src/reports/maintenance-email/template.ts
|
|
1935
|
-
var CHECK_PNG =
|
|
1936
|
-
var BLURRED_TESTS =
|
|
2065
|
+
var CHECK_PNG = `cid:${CHECK_CID}`;
|
|
2066
|
+
var BLURRED_TESTS = `cid:${BLURRED_CID}`;
|
|
1937
2067
|
function fmtDate(d) {
|
|
1938
2068
|
if (!d) return "";
|
|
1939
2069
|
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
@@ -2104,43 +2234,6 @@ async function renderReportHtml(data) {
|
|
|
2104
2234
|
return { html: out.html, warnings: out.errors ?? [] };
|
|
2105
2235
|
}
|
|
2106
2236
|
|
|
2107
|
-
// src/reports/airtable/websites.ts
|
|
2108
|
-
var WEBSITES_TABLE = "Websites";
|
|
2109
|
-
function siteSlug(name) {
|
|
2110
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
2111
|
-
}
|
|
2112
|
-
function mapRow(rec) {
|
|
2113
|
-
const f = rec.fields;
|
|
2114
|
-
const attachments = f["Header image"] ?? [];
|
|
2115
|
-
const header = attachments[0] ?? null;
|
|
2116
|
-
return {
|
|
2117
|
-
id: rec.id,
|
|
2118
|
-
name: String(f["Name"] ?? ""),
|
|
2119
|
-
url: String(f["url"] ?? ""),
|
|
2120
|
-
pointOfContact: f["point of contact"] ?? null,
|
|
2121
|
-
maintenanceFreq: f["maintenence freq"] ?? "None",
|
|
2122
|
-
testingFreq: f["testing freq"] ?? "None",
|
|
2123
|
-
maintenanceDay: f["maintenance day"] ?? null,
|
|
2124
|
-
testingDay: f["testing day"] ?? null,
|
|
2125
|
-
ga4PropertyId: f["GA4 property ID"] ?? null,
|
|
2126
|
-
reportRecipientsTo: f["Report recipients (To)"] ?? null,
|
|
2127
|
-
reportRecipientsCc: f["Report recipients (CC)"] ?? null,
|
|
2128
|
-
headerImage: header,
|
|
2129
|
-
pScore: f["pScore"] ?? null,
|
|
2130
|
-
rScore: f["rScore"] ?? null,
|
|
2131
|
-
bpScore: f["bpScore"] ?? null,
|
|
2132
|
-
seoScore: f["seoScore"] ?? null
|
|
2133
|
-
};
|
|
2134
|
-
}
|
|
2135
|
-
async function listWebsites(base) {
|
|
2136
|
-
const out = [];
|
|
2137
|
-
await base(WEBSITES_TABLE).select({ pageSize: 100 }).eachPage((records, fetchNextPage) => {
|
|
2138
|
-
for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));
|
|
2139
|
-
fetchNextPage();
|
|
2140
|
-
});
|
|
2141
|
-
return out;
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
2237
|
// src/reports/airtable/reports.ts
|
|
2145
2238
|
var REPORTS_TABLE = "Reports";
|
|
2146
2239
|
function mapRow2(rec) {
|
|
@@ -2242,6 +2335,40 @@ async function stampSent(base, recordId, sentAt, messageId) {
|
|
|
2242
2335
|
]);
|
|
2243
2336
|
}
|
|
2244
2337
|
|
|
2338
|
+
// src/reports/airtable/attachments.ts
|
|
2339
|
+
async function fetchAttachmentBytes(url) {
|
|
2340
|
+
const res = await fetch(url);
|
|
2341
|
+
if (!res.ok) {
|
|
2342
|
+
throw new Error(
|
|
2343
|
+
`Failed to fetch Airtable attachment ${res.status} ${res.statusText} (url=${url})`
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
2347
|
+
const ab = await res.arrayBuffer();
|
|
2348
|
+
return { bytes: new Uint8Array(ab), contentType };
|
|
2349
|
+
}
|
|
2350
|
+
async function uploadAttachment(recordId, fieldName, body, filename, contentType) {
|
|
2351
|
+
const apiKey = process.env.AIRTABLE_PAT;
|
|
2352
|
+
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
2353
|
+
if (!apiKey || !baseId) {
|
|
2354
|
+
throw new Error("AIRTABLE_PAT and AIRTABLE_BASE_ID must be set");
|
|
2355
|
+
}
|
|
2356
|
+
const base64 = typeof body === "string" ? Buffer.from(body, "utf-8").toString("base64") : Buffer.from(body).toString("base64");
|
|
2357
|
+
const payload = { contentType, file: base64, filename };
|
|
2358
|
+
const url = `https://content.airtable.com/v0/${baseId}/${recordId}/${encodeURIComponent(fieldName)}/uploadAttachment`;
|
|
2359
|
+
const res = await fetch(url, {
|
|
2360
|
+
method: "POST",
|
|
2361
|
+
headers: {
|
|
2362
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2363
|
+
"Content-Type": "application/json"
|
|
2364
|
+
},
|
|
2365
|
+
body: JSON.stringify(payload)
|
|
2366
|
+
});
|
|
2367
|
+
if (!res.ok) {
|
|
2368
|
+
throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2245
2372
|
// src/reports/draft.ts
|
|
2246
2373
|
function scoresFromWebsite(siteRow) {
|
|
2247
2374
|
const { pScore, rScore, bpScore, seoScore } = siteRow;
|
|
@@ -2280,7 +2407,7 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
2280
2407
|
});
|
|
2281
2408
|
if (options.previewOnly) {
|
|
2282
2409
|
const path = options.previewPath ?? `reports/${slug}/draft.html`;
|
|
2283
|
-
await mkdir(
|
|
2410
|
+
await mkdir(dirname3(path), { recursive: true });
|
|
2284
2411
|
await writeFile9(path, html, "utf-8");
|
|
2285
2412
|
return { reportRow: null, htmlPath: path, html };
|
|
2286
2413
|
}
|
|
@@ -2296,7 +2423,8 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
2296
2423
|
lighthouse: scores,
|
|
2297
2424
|
lastTestedDate
|
|
2298
2425
|
});
|
|
2299
|
-
|
|
2426
|
+
const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
2427
|
+
await uploadAttachment(created.id, "Rendered HTML", html, htmlFilename, "text/html");
|
|
2300
2428
|
await setDraftReady(base, created.id, true);
|
|
2301
2429
|
return { reportRow: created, htmlPath: null, html };
|
|
2302
2430
|
}
|
|
@@ -2306,28 +2434,6 @@ async function derivePeriodStart(base, siteRow, reportType, today) {
|
|
|
2306
2434
|
const latest = sameType[sameType.length - 1];
|
|
2307
2435
|
return latest ? new Date(latest) : daysAgo(today, 30);
|
|
2308
2436
|
}
|
|
2309
|
-
async function uploadHtmlAttachment(recordId, html, slug, periodEnd) {
|
|
2310
|
-
const apiKey = process.env.AIRTABLE_PAT;
|
|
2311
|
-
const baseId = process.env.AIRTABLE_BASE_ID;
|
|
2312
|
-
const filename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
2313
|
-
const body = {
|
|
2314
|
-
contentType: "text/html",
|
|
2315
|
-
file: Buffer.from(html, "utf-8").toString("base64"),
|
|
2316
|
-
filename
|
|
2317
|
-
};
|
|
2318
|
-
const url = `https://content.airtable.com/v0/${baseId}/${recordId}/Rendered%20HTML/uploadAttachment`;
|
|
2319
|
-
const res = await fetch(url, {
|
|
2320
|
-
method: "POST",
|
|
2321
|
-
headers: {
|
|
2322
|
-
Authorization: `Bearer ${apiKey}`,
|
|
2323
|
-
"Content-Type": "application/json"
|
|
2324
|
-
},
|
|
2325
|
-
body: JSON.stringify(body)
|
|
2326
|
-
});
|
|
2327
|
-
if (!res.ok) {
|
|
2328
|
-
throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
2437
|
|
|
2332
2438
|
// src/reports/airtable/client.ts
|
|
2333
2439
|
import Airtable from "airtable";
|
|
@@ -2342,19 +2448,6 @@ function openBase(cfg) {
|
|
|
2342
2448
|
return new Airtable({ apiKey: cfg.apiKey }).base(cfg.baseId);
|
|
2343
2449
|
}
|
|
2344
2450
|
|
|
2345
|
-
// src/reports/airtable/attachments.ts
|
|
2346
|
-
async function fetchAttachmentBytes(url) {
|
|
2347
|
-
const res = await fetch(url);
|
|
2348
|
-
if (!res.ok) {
|
|
2349
|
-
throw new Error(
|
|
2350
|
-
`Failed to fetch Airtable attachment ${res.status} ${res.statusText} (url=${url})`
|
|
2351
|
-
);
|
|
2352
|
-
}
|
|
2353
|
-
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
2354
|
-
const ab = await res.arrayBuffer();
|
|
2355
|
-
return { bytes: new Uint8Array(ab), contentType };
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
2451
|
// src/reports/send/resend.ts
|
|
2359
2452
|
import { Resend } from "resend";
|
|
2360
2453
|
function defaultResendClient() {
|
|
@@ -2419,6 +2512,7 @@ async function sendOne(client, base, site, report) {
|
|
|
2419
2512
|
throw new Error(`Report ${report.reportId} has no Lighthouse scores`);
|
|
2420
2513
|
}
|
|
2421
2514
|
const { bytes, contentType } = await fetchAttachmentBytes(site.headerImage.url);
|
|
2515
|
+
const bundled = await loadBundledImages();
|
|
2422
2516
|
const slug = siteSlug(site.name);
|
|
2423
2517
|
const cidName = `${slug}-header`;
|
|
2424
2518
|
const { html } = await renderReportHtml({
|
|
@@ -2471,6 +2565,21 @@ async function sendOne(client, base, site, report) {
|
|
|
2471
2565
|
content: Buffer.from(bytes).toString("base64"),
|
|
2472
2566
|
contentType,
|
|
2473
2567
|
inlineContentId: cidName
|
|
2568
|
+
},
|
|
2569
|
+
// Bundled images referenced via cid:rd-check-png / cid:rd-blurred-tests-jpg
|
|
2570
|
+
// in the template. Attached inline so the email is self-contained — no
|
|
2571
|
+
// external CDN dependency, no image-blocked broken icons in webmail.
|
|
2572
|
+
{
|
|
2573
|
+
filename: bundled.check.filename,
|
|
2574
|
+
content: Buffer.from(bundled.check.bytes).toString("base64"),
|
|
2575
|
+
contentType: bundled.check.contentType,
|
|
2576
|
+
inlineContentId: bundled.check.cid
|
|
2577
|
+
},
|
|
2578
|
+
{
|
|
2579
|
+
filename: bundled.blurred.filename,
|
|
2580
|
+
content: Buffer.from(bundled.blurred.bytes).toString("base64"),
|
|
2581
|
+
contentType: bundled.blurred.contentType,
|
|
2582
|
+
inlineContentId: bundled.blurred.cid
|
|
2474
2583
|
}
|
|
2475
2584
|
],
|
|
2476
2585
|
// Stable across retries of the same row — if Airtable stamping fails after a
|
|
@@ -2508,6 +2617,12 @@ function isProbablyEmail(s) {
|
|
|
2508
2617
|
}
|
|
2509
2618
|
|
|
2510
2619
|
// src/reports/due.ts
|
|
2620
|
+
var ELIGIBLE_STATUSES = /* @__PURE__ */ new Set([
|
|
2621
|
+
"in development",
|
|
2622
|
+
"launch period",
|
|
2623
|
+
"maintenance",
|
|
2624
|
+
"hosting"
|
|
2625
|
+
]);
|
|
2511
2626
|
var MONTHS = {
|
|
2512
2627
|
Monthly: 1,
|
|
2513
2628
|
Quarterly: 3,
|
|
@@ -2537,6 +2652,7 @@ function findDueReports(websites, reports, today) {
|
|
|
2537
2652
|
const out = [];
|
|
2538
2653
|
const todayStart = startOfDay(today);
|
|
2539
2654
|
for (const site of websites) {
|
|
2655
|
+
if (site.status !== null && !ELIGIBLE_STATUSES.has(site.status)) continue;
|
|
2540
2656
|
for (const type of ["Maintenance", "Testing"]) {
|
|
2541
2657
|
const freq = type === "Maintenance" ? site.maintenanceFreq : site.testingFreq;
|
|
2542
2658
|
if (freq === "None") continue;
|
|
@@ -2564,6 +2680,7 @@ export {
|
|
|
2564
2680
|
depsAudit,
|
|
2565
2681
|
draftReportForSite,
|
|
2566
2682
|
findDueReports,
|
|
2683
|
+
fromAirtableBase,
|
|
2567
2684
|
fromJsonFile,
|
|
2568
2685
|
isRecipeName,
|
|
2569
2686
|
lighthouseAudit,
|