@reddoorla/maintenance 0.65.0 → 0.65.3

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.
Files changed (99) hide show
  1. package/README.md +17 -10
  2. package/dist/airtable-JUPGAI4R.js +9 -0
  3. package/dist/{announce-GFLKAPTT.js → announce-WXWOHR2I.js} +24 -9
  4. package/dist/announce-WXWOHR2I.js.map +1 -0
  5. package/dist/{bump-deps-3QQHYDKJ.js → bump-deps-2MAMI7DB.js} +6 -4
  6. package/dist/bump-deps-2MAMI7DB.js.map +1 -0
  7. package/dist/{chunk-MQM5YP4J.js → chunk-ARHDSMFK.js} +2 -2
  8. package/dist/{chunk-FUD4I2ZW.js → chunk-BTEBRTW4.js} +2 -2
  9. package/dist/{chunk-7RGQ4KLE.js → chunk-CCXVX5JN.js} +5 -1
  10. package/dist/chunk-CCXVX5JN.js.map +1 -0
  11. package/dist/{chunk-4A5PYCGB.js → chunk-IMMB64VH.js} +2 -2
  12. package/dist/{chunk-UATHDNWP.js → chunk-IRRZG66U.js} +13 -6
  13. package/dist/chunk-IRRZG66U.js.map +1 -0
  14. package/dist/{chunk-OQPMZ6BM.js → chunk-J7SZQUCW.js} +10 -2
  15. package/dist/{chunk-OQPMZ6BM.js.map → chunk-J7SZQUCW.js.map} +1 -1
  16. package/dist/{chunk-M5LX2VSR.js → chunk-JWA4FO2J.js} +3 -3
  17. package/dist/{chunk-FX3QBSUF.js → chunk-LCKKIJ22.js} +3 -3
  18. package/dist/{chunk-LM42CATB.js → chunk-MEP6PR32.js} +5 -4
  19. package/dist/chunk-MEP6PR32.js.map +1 -0
  20. package/dist/{chunk-CSTLFSGM.js → chunk-N5MZ6WRX.js} +2 -2
  21. package/dist/{chunk-OYN6H4CX.js → chunk-PMIKXAPF.js} +4 -4
  22. package/dist/{chunk-U3MF6RCS.js → chunk-SYTPVYIK.js} +13 -14
  23. package/dist/chunk-SYTPVYIK.js.map +1 -0
  24. package/dist/chunk-VPJ7N6P7.js +27 -0
  25. package/dist/chunk-VPJ7N6P7.js.map +1 -0
  26. package/dist/{chunk-FH7WQHD7.js → chunk-VUKSRWRD.js} +2 -2
  27. package/dist/{chunk-TC7Z5LG7.js → chunk-WCN54744.js} +30 -16
  28. package/dist/{chunk-TC7Z5LG7.js.map → chunk-WCN54744.js.map} +1 -1
  29. package/dist/{chunk-QSL3EJGI.js → chunk-WXNSLK4Q.js} +3 -3
  30. package/dist/{chunk-4GACMVNE.js → chunk-YEGHUODU.js} +17 -1
  31. package/dist/chunk-YEGHUODU.js.map +1 -0
  32. package/dist/{chunk-O65I7G7G.js → chunk-ZO7WNY57.js} +2 -2
  33. package/dist/cli/bin.js +14 -14
  34. package/dist/cli/commands/audit.js +7 -7
  35. package/dist/{convert-to-pnpm-ZO673UFB.js → convert-to-pnpm-3OEHYF76.js} +6 -4
  36. package/dist/convert-to-pnpm-3OEHYF76.js.map +1 -0
  37. package/dist/{digest-R3ROSGLP.js → digest-BTHXYYQZ.js} +3 -3
  38. package/dist/{github-signals-N5JGWHW2.js → github-signals-H7VU4EOH.js} +5 -5
  39. package/dist/index.d.ts +29 -7
  40. package/dist/index.js +16 -16
  41. package/dist/index.js.map +1 -1
  42. package/dist/{init-FOHNISF2.js → init-VA3H4OHM.js} +26 -9
  43. package/dist/init-VA3H4OHM.js.map +1 -0
  44. package/dist/{launch-TM3IHSSU.js → launch-OMHMW5OK.js} +9 -9
  45. package/dist/{lighthouse-airtable-5FHELRCA.js → lighthouse-airtable-4KWC3P72.js} +3 -3
  46. package/dist/{onboard-K2EL4MSG.js → onboard-2PLVVJWN.js} +10 -6
  47. package/dist/onboard-2PLVVJWN.js.map +1 -0
  48. package/dist/{orchestrate-OX3Z4DUI.js → orchestrate-6PKHJXLF.js} +4 -4
  49. package/dist/{renovate-dispatch-H25CQ36P.js → renovate-dispatch-K6VO5KWM.js} +3 -3
  50. package/dist/{report-UEYKO6WH.js → report-X3XPBRBO.js} +6 -6
  51. package/dist/reports/maintenance-email/assets/blurredTests.jpg +0 -0
  52. package/dist/{self-updating-YXOGQWH5.js → self-updating-VXVJSV5T.js} +8 -6
  53. package/dist/self-updating-VXVJSV5T.js.map +1 -0
  54. package/dist/{selftest-VE7CSGWE.js → selftest-FV4MO7TX.js} +6 -6
  55. package/dist/{svelte-codemods-745QM25J.js → svelte-codemods-5RXRRTMK.js} +8 -6
  56. package/dist/svelte-codemods-5RXRRTMK.js.map +1 -0
  57. package/dist/{sync-configs-NYC3Z625.js → sync-configs-TPJMRNRU.js} +10 -4
  58. package/dist/sync-configs-TPJMRNRU.js.map +1 -0
  59. package/dist/{upgrade-UBPPJ6HN.js → upgrade-7TN2XPFK.js} +8 -10
  60. package/dist/upgrade-7TN2XPFK.js.map +1 -0
  61. package/dist/{websites-CSVLSEVL.js → websites-EIPNY4DY.js} +2 -2
  62. package/dist/{write-audits-to-airtable-JCV7FYAW.js → write-audits-to-airtable-WVWFESZW.js} +4 -4
  63. package/package.json +1 -1
  64. package/dist/airtable-GGZJUGQB.js +0 -9
  65. package/dist/announce-GFLKAPTT.js.map +0 -1
  66. package/dist/bump-deps-3QQHYDKJ.js.map +0 -1
  67. package/dist/chunk-4GACMVNE.js.map +0 -1
  68. package/dist/chunk-7RGQ4KLE.js.map +0 -1
  69. package/dist/chunk-LM42CATB.js.map +0 -1
  70. package/dist/chunk-U3MF6RCS.js.map +0 -1
  71. package/dist/chunk-UATHDNWP.js.map +0 -1
  72. package/dist/convert-to-pnpm-ZO673UFB.js.map +0 -1
  73. package/dist/init-FOHNISF2.js.map +0 -1
  74. package/dist/onboard-K2EL4MSG.js.map +0 -1
  75. package/dist/self-updating-YXOGQWH5.js.map +0 -1
  76. package/dist/svelte-codemods-745QM25J.js.map +0 -1
  77. package/dist/sync-configs-NYC3Z625.js.map +0 -1
  78. package/dist/upgrade-UBPPJ6HN.js.map +0 -1
  79. /package/dist/{airtable-GGZJUGQB.js.map → airtable-JUPGAI4R.js.map} +0 -0
  80. /package/dist/{chunk-MQM5YP4J.js.map → chunk-ARHDSMFK.js.map} +0 -0
  81. /package/dist/{chunk-FUD4I2ZW.js.map → chunk-BTEBRTW4.js.map} +0 -0
  82. /package/dist/{chunk-4A5PYCGB.js.map → chunk-IMMB64VH.js.map} +0 -0
  83. /package/dist/{chunk-M5LX2VSR.js.map → chunk-JWA4FO2J.js.map} +0 -0
  84. /package/dist/{chunk-FX3QBSUF.js.map → chunk-LCKKIJ22.js.map} +0 -0
  85. /package/dist/{chunk-CSTLFSGM.js.map → chunk-N5MZ6WRX.js.map} +0 -0
  86. /package/dist/{chunk-OYN6H4CX.js.map → chunk-PMIKXAPF.js.map} +0 -0
  87. /package/dist/{chunk-FH7WQHD7.js.map → chunk-VUKSRWRD.js.map} +0 -0
  88. /package/dist/{chunk-QSL3EJGI.js.map → chunk-WXNSLK4Q.js.map} +0 -0
  89. /package/dist/{chunk-O65I7G7G.js.map → chunk-ZO7WNY57.js.map} +0 -0
  90. /package/dist/{digest-R3ROSGLP.js.map → digest-BTHXYYQZ.js.map} +0 -0
  91. /package/dist/{github-signals-N5JGWHW2.js.map → github-signals-H7VU4EOH.js.map} +0 -0
  92. /package/dist/{launch-TM3IHSSU.js.map → launch-OMHMW5OK.js.map} +0 -0
  93. /package/dist/{lighthouse-airtable-5FHELRCA.js.map → lighthouse-airtable-4KWC3P72.js.map} +0 -0
  94. /package/dist/{orchestrate-OX3Z4DUI.js.map → orchestrate-6PKHJXLF.js.map} +0 -0
  95. /package/dist/{renovate-dispatch-H25CQ36P.js.map → renovate-dispatch-K6VO5KWM.js.map} +0 -0
  96. /package/dist/{report-UEYKO6WH.js.map → report-X3XPBRBO.js.map} +0 -0
  97. /package/dist/{selftest-VE7CSGWE.js.map → selftest-FV4MO7TX.js.map} +0 -0
  98. /package/dist/{websites-CSVLSEVL.js.map → websites-EIPNY4DY.js.map} +0 -0
  99. /package/dist/{write-audits-to-airtable-JCV7FYAW.js.map → write-audits-to-airtable-WVWFESZW.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/recipes/svelte-5/step-gotchas.ts","../src/recipes/svelte-5/codemods/on-event-to-handler.ts","../src/recipes/svelte-5/codemods/dollar-props.ts","../src/util/svelte-source.ts","../src/recipes/svelte-5/codemods/dollar-restprops.ts","../src/recipes/svelte-5/codemods/state-effect-sync.ts","../src/recipes/svelte-5/codemods/dollar-props-class.ts","../src/recipes/svelte-5/codemods/legacy-reactive.ts"],"sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { glob } from \"tinyglobby\";\nimport { onEventToHandler } from \"./codemods/on-event-to-handler.js\";\nimport { exportLetToProps } from \"./codemods/dollar-props.js\";\nimport { removeDollarRestProps } from \"./codemods/dollar-restprops.js\";\nimport { stateEffectSyncToDerived } from \"./codemods/state-effect-sync.js\";\nimport { dollarPropsClass } from \"./codemods/dollar-props-class.js\";\nimport { legacyReactiveToRunes } from \"./codemods/legacy-reactive.js\";\n\nconst SVELTE_GLOBS = [\"src/**/*.svelte\"];\nconst IGNORE = [\"node_modules/**\", \".svelte-kit/**\", \"build/**\"];\n\ntype Codemod = (src: string) => string;\n\n// Order matters: exportLetToProps creates the $props() destructuring that\n// dollarPropsClass extends with a `class:` named prop.\nconst CODEMODS: Codemod[] = [\n onEventToHandler,\n exportLetToProps,\n removeDollarRestProps,\n stateEffectSyncToDerived,\n dollarPropsClass,\n legacyReactiveToRunes,\n];\n\nexport type CodemodChange = { rel: string; after: string };\n\nexport async function planGotchaCodemods(cwd: string): Promise<CodemodChange[]> {\n const changes: CodemodChange[] = [];\n const relPaths = await glob(SVELTE_GLOBS, { cwd, ignore: IGNORE, absolute: false });\n for (const rel of relPaths) {\n const path = join(cwd, rel);\n const before = await readFile(path, \"utf-8\");\n const after = CODEMODS.reduce((s, fn) => fn(s), before);\n if (after !== before) changes.push({ rel, after });\n }\n return changes;\n}\n\nexport async function applyGotchaCodemods(cwd: string): Promise<{ filesChanged: number }> {\n const changes = await planGotchaCodemods(cwd);\n for (const c of changes) {\n await writeFile(join(cwd, c.rel), c.after, \"utf-8\");\n }\n return { filesChanged: changes.length };\n}\n","const SCRIPT_BLOCK = /<script\\b[^>]*>[\\s\\S]*?<\\/script>/g;\nconst SIMPLE_ON_EVENT = /\\bon:([a-z]+)(?=\\s*=)/g;\nconst MODIFIER_EVENT = /\\bon:[a-z]+\\|[a-zA-Z]+(?=\\s*=)/g;\n\n/** Svelte 5 removed event modifier syntax (`on:click|preventDefault={fn}`).\n * The rewrite is non-trivial — the modifier behavior must be inlined into\n * the handler body — so this codemod doesn't attempt it automatically.\n * Instead it inserts a `@migration-task` marker immediately above each\n * offending element so the user gets a visible audit trail rather than\n * a silent build error from the Svelte 5 compiler. */\nfunction flagEventModifiers(source: string): string {\n const insertions: Array<{ tagStart: number; indent: string; modifier: string }> = [];\n let m: RegExpExecArray | null;\n MODIFIER_EVENT.lastIndex = 0;\n while ((m = MODIFIER_EVENT.exec(source)) !== null) {\n const tagStart = source.lastIndexOf(\"<\", m.index);\n if (tagStart === -1) continue;\n\n // Idempotency: if the line immediately above the tag already carries an\n // @migration-task marker for this site, don't double-insert on re-run.\n const prevLineEnd = tagStart - 1;\n if (prevLineEnd >= 0) {\n const prevLineStart = source.lastIndexOf(\"\\n\", prevLineEnd - 1) + 1;\n const prevLine = source.slice(prevLineStart, prevLineEnd + 1);\n if (/<!--\\s*@migration-task/.test(prevLine)) continue;\n }\n\n const lineStart = source.lastIndexOf(\"\\n\", tagStart - 1) + 1;\n const indent = source.slice(lineStart, tagStart);\n const safeIndent = /^[ \\t]*$/.test(indent) ? indent : \"\";\n insertions.push({ tagStart, indent: safeIndent, modifier: m[0] });\n }\n\n // Apply back-to-front so earlier insertion offsets stay valid.\n let out = source;\n for (let i = insertions.length - 1; i >= 0; i--) {\n const { tagStart, indent, modifier } = insertions[i]!;\n const comment = `<!-- @migration-task: Svelte 5 removed event modifier syntax (\\`${modifier}\\`). Rewrite inline, e.g. onclick={(e) => { e.preventDefault(); ... }}. -->\\n${indent}`;\n out = out.slice(0, tagStart) + comment + out.slice(tagStart);\n }\n return out;\n}\n\nexport function onEventToHandler(source: string): string {\n const masked: string[] = [];\n const placeholder = (i: number): string => ` SCRIPT_${i} `;\n const intermediate = source.replace(SCRIPT_BLOCK, (match) => {\n masked.push(match);\n return placeholder(masked.length - 1);\n });\n\n let processed = intermediate.replace(SIMPLE_ON_EVENT, (_full, name: string) => `on${name}`);\n processed = flagEventModifiers(processed);\n\n let out = processed;\n masked.forEach((blk, i) => {\n out = out.replace(placeholder(i), blk);\n });\n\n return out;\n}\n","const SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/;\nconst EXPORT_LET = /^\\s*export\\s+let\\s+(\\w+)\\s*(?::\\s*([^=;\\n]+))?\\s*(?:=\\s*([^;\\n]+))?;?\\s*$/gm;\n\ntype Prop = { name: string; type?: string | undefined; defaultExpr?: string | undefined };\n\nfunction transformScript(scriptBody: string, isTs: boolean): { body: string; changed: boolean } {\n const props: Prop[] = [];\n const cleaned = scriptBody.replace(\n EXPORT_LET,\n (_full, name: string, type?: string, defaultExpr?: string) => {\n props.push({\n name,\n type: type?.trim(),\n defaultExpr: defaultExpr?.trim(),\n });\n return \"\";\n },\n );\n if (props.length === 0) return { body: scriptBody, changed: false };\n\n const destructured = props\n .map((p) => (p.defaultExpr ? `${p.name} = ${p.defaultExpr}` : p.name))\n .join(\", \");\n\n let decl: string;\n if (isTs) {\n const typeSig = props\n .map((p) => {\n const optional = p.defaultExpr ? \"?\" : \"\";\n return `${p.name}${optional}: ${p.type ?? \"unknown\"}`;\n })\n .join(\"; \");\n decl = ` let { ${destructured} }: { ${typeSig} } = $props();`;\n } else {\n decl = ` let { ${destructured} } = $props();`;\n }\n\n const next = cleaned.replace(/^(\\s*)/, (m) => `${m}${decl}\\n`);\n return { body: next, changed: true };\n}\n\nexport function exportLetToProps(source: string): string {\n const match = source.match(SCRIPT_BLOCK);\n if (!match) return source;\n const attrs = match[1] ?? \"\";\n const inner = match[2] ?? \"\";\n const isTs = /\\blang=[\"']ts[\"']/.test(attrs);\n const { body, changed } = transformScript(inner, isTs);\n if (!changed) return source;\n return source.replace(SCRIPT_BLOCK, (full) => full.replace(inner, body));\n}\n","/** Find the index of the closing quote for a string literal that opens at\n * `openIdx`. Handles backslash escapes. Returns -1 if the string is\n * unterminated.\n *\n * Treats backtick template literals the same as `'…'` / `\"…\"` — the\n * closing backtick terminates. Callers needing precise `${…}` interpolation\n * handling will need a real parser; this helper is intentionally simple\n * and good enough for the codemod-grade string masking we do today. */\nexport function findStringEnd(source: string, openIdx: number): number {\n const quote = source[openIdx];\n let i = openIdx + 1;\n while (i < source.length) {\n const ch = source[i];\n if (ch === \"\\\\\") {\n i += 2;\n continue;\n }\n if (ch === quote) return i;\n i++;\n }\n return -1;\n}\n","/** Locate `interface $$Props {` declarations and remove them, including\n * the matching closing `}` even if the body has nested braces or spans\n * multiple lines. Regex alone can't do balanced-brace matching, so we\n * walk the string manually. */\nfunction removeInterfaceBlock(source: string): string {\n const re = /^\\s*interface\\s+\\$\\$Props\\s*\\{/m;\n let out = source;\n while (true) {\n const match = re.exec(out);\n if (!match) return out;\n\n const openBraceIdx = match.index + match[0].length - 1;\n let depth = 1;\n let i = openBraceIdx + 1;\n while (i < out.length && depth > 0) {\n const ch = out[i];\n if (ch === \"{\") depth++;\n else if (ch === \"}\") depth--;\n i++;\n }\n if (depth !== 0) return out; // unbalanced; bail rather than corrupt\n\n // Consume trailing whitespace through end-of-line.\n let endIdx = i;\n while (endIdx < out.length && /[ \\t]/.test(out[endIdx] ?? \"\")) endIdx++;\n if (out[endIdx] === \"\\n\") endIdx++;\n\n out = out.slice(0, match.index) + out.slice(endIdx);\n }\n}\n\nimport { findStringEnd } from \"../../../util/svelte-source.js\";\n\n/** Mask every `'…'`, `\"…\"`, and template literal in `source` with a placeholder\n * so subsequent regex passes can rewrite identifiers without corrupting string\n * contents. Returns the masked body and a function to restore originals. */\nfunction maskStringLiterals(source: string): {\n masked: string;\n restore: (s: string) => string;\n} {\n const strings: string[] = [];\n let out = \"\";\n let i = 0;\n while (i < source.length) {\n const ch = source[i];\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n const closeIdx = findStringEnd(source, i);\n if (closeIdx === -1) {\n out += source.slice(i);\n break;\n }\n const literal = source.slice(i, closeIdx + 1);\n out += `__RDMNT_STR_${strings.length}__`;\n strings.push(literal);\n i = closeIdx + 1;\n } else {\n out += ch;\n i++;\n }\n }\n return {\n masked: out,\n restore: (s) => s.replace(/__RDMNT_STR_(\\d+)__/g, (_full, idx) => strings[Number(idx)] ?? \"\"),\n };\n}\n\nconst PROPS_DECL = /let\\s*\\{([^}]*)\\}\\s*(?::\\s*\\{([^}]*)\\})?\\s*=\\s*\\$props\\(\\)\\s*;?/;\n\n/** If the script declares `let { … } = $props();` (with or without an inline\n * type annotation) and doesn't already collect `...rest`, inject it. For TS,\n * widen the inline type with an `[key: string]: unknown` index signature so\n * the rest binding actually captures excess attributes (without the widening,\n * TS infers `rest` as `{}` and the spread forwards nothing). */\nfunction injectRestIntoProps(scriptBody: string): string {\n const match = scriptBody.match(PROPS_DECL);\n if (!match) return scriptBody;\n const destructured = match[1] ?? \"\";\n if (/\\.\\.\\.\\s*\\w+/.test(destructured)) return scriptBody; // already has rest\n\n // Strip any trailing comma left over from a multi-line destructuring shape\n // (e.g. `{ foo, bar, }`). Without this, the template literal below emits\n // `bar,, ...rest` — invalid syntax that the codemod was happily committing\n // (regression: caltex's Accordian.svelte, 2026-05-27).\n const trimmed = destructured.trim().replace(/,\\s*$/, \"\");\n const newDestructured = trimmed === \"\" ? \" ...rest \" : ` ${trimmed}, ...rest `;\n\n let replacement: string;\n if (match[2] !== undefined) {\n const typeBody = match[2];\n const hasIndexSig = /\\[\\s*key\\s*:\\s*string\\s*\\]\\s*:/.test(typeBody);\n const newTypeBody = hasIndexSig\n ? typeBody\n : `${typeBody.trimEnd().replace(/;?\\s*$/, \"\")}; [key: string]: unknown `;\n replacement = `let {${newDestructured}}: {${newTypeBody}} = $props();`;\n } else {\n replacement = `let {${newDestructured}} = $props();`;\n }\n return scriptBody.replace(PROPS_DECL, replacement);\n}\n\nconst SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/;\nconst HAS_PROPS_CALL = /\\$props\\(\\s*\\)/;\n\nexport function removeDollarRestProps(source: string): string {\n const next = removeInterfaceBlock(source);\n\n const scriptMatch = next.match(SCRIPT_BLOCK);\n if (!scriptMatch) return next;\n if (!HAS_PROPS_CALL.test(scriptMatch[2] ?? \"\")) {\n // No $props() in this script — refuse to rewrite $$restProps anywhere, since\n // doing so would emit references to an undeclared identifier. The user sees\n // the original $$restProps and a clear Svelte 5 build error to migrate by hand.\n return next;\n }\n\n const scriptInner = scriptMatch[2] ?? \"\";\n const { masked, restore } = maskStringLiterals(scriptInner);\n let processed = injectRestIntoProps(masked);\n processed = processed.replace(/\\$\\$restProps/g, \"rest\");\n const restoredInner = restore(processed);\n\n // Use a function callback so `$$` in the restored script body isn't\n // interpreted as the `$` substitution pattern by String.prototype.replace.\n const newScriptBlock = scriptMatch[0].replace(scriptInner, () => restoredInner);\n const before = next.slice(0, scriptMatch.index!);\n const after = next.slice(scriptMatch.index! + scriptMatch[0].length);\n\n // Template (outside script) gets a plain swap. Template attribute strings\n // containing the literal text \"$$restProps\" are vanishingly rare in practice;\n // accept the limitation rather than parse the whole template.\n return (\n before.replace(/\\$\\$restProps/g, \"rest\") +\n newScriptBlock +\n after.replace(/\\$\\$restProps/g, \"rest\")\n );\n}\n","/**\n * Collapses the \"manual sync state with prop\" anti-pattern into `$derived`.\n *\n * Input:\n * let content = $state(data.page.data);\n * $effect(() => { data; content = data.page.data });\n *\n * Output:\n * let content = $derived(data.page.data);\n *\n * Only transforms when the `$state(...)` initializer expression matches the\n * effect's right-hand assignment exactly (after trim). Intervening statements\n * between the `let` and the `$effect` block prevent the match — keeps the\n * codemod conservative.\n *\n * Triggered by Svelte 5's `state_referenced_locally` warning, which fires\n * whenever a local `let X = $state(prop.expr)` captures a prop reference\n * only at init time.\n */\n// `;?` before the closing `}` so the multi-line effect form matches:\n// $effect(() => {\n// data;\n// content = data.page.data;\n// });\n// as well as the single-line form: $effect(() => { data; content = data.page.data })\nconst PATTERN =\n /let\\s+(\\w+)\\s*=\\s*\\$state\\(\\s*([^)]+?)\\s*\\)\\s*;[ \\t\\r\\n]*\\$effect\\(\\s*\\(\\s*\\)\\s*=>\\s*\\{\\s*\\w+\\s*;\\s*\\1\\s*=\\s*([^;}]+?)\\s*;?\\s*\\}\\s*\\)\\s*;?/g;\n\nexport function stateEffectSyncToDerived(source: string): string {\n return source.replace(PATTERN, (full, name: string, initExpr: string, effectExpr: string) => {\n if (initExpr.trim() !== effectExpr.trim()) return full;\n return `let ${name} = $derived(${initExpr.trim()});`;\n });\n}\n","/**\n * Converts the legacy `$$props.class` pattern (passing extra HTML class from\n * a parent component) to a Svelte 5 named-prop destructuring.\n *\n * Input:\n * <script lang=\"ts\">\n * let { foo }: { foo?: string } = $props();\n * </script>\n * <div class=\"other {$$props.class || ''}\">x</div>\n *\n * Output:\n * <script lang=\"ts\">\n * let { foo, class: className = \"\" }: { foo?: string; class?: string } = $props();\n * </script>\n * <div class=\"other {className || ''}\">x</div>\n *\n * Triggered by Svelte 5 build errors:\n * \"Cannot use `$$props` in runes mode\" (svelte.dev/e/legacy_props_invalid)\n *\n * The original svelte-migrate tool flagged this with a `@migration-task`\n * comment because it couldn't safely combine `$$props` with already-named\n * props. We can: `class` is the dominant case across the reddoor fleet,\n * so we destructure it as `class: className = \"\"` (renamed because `class`\n * is a JS reserved word as a bare binding) and rewrite template references.\n *\n * Conservative: only transforms files that have BOTH a template\n * `$$props.class` reference AND an existing `$props()` destructuring.\n * Files using `$$props.class` without a `$props()` declaration are left\n * for the `exportLetToProps` codemod to handle in a prior pass.\n */\n// Note: lazy `[\\s\\S]*?` (not `[^}]*`) so default values containing braces\n// — `() => {}`, `{ foo: 1 }`, etc. — don't truncate the match early.\nconst PROPS_DESTRUCTURE = /let\\s*\\{([\\s\\S]*?)\\}(\\s*:\\s*\\{([\\s\\S]*?)\\})?\\s*=\\s*\\$props\\(\\)/;\n// Two regexes: a stateless one for \"does this string contain $$props.class?\"\n// existence checks, and the /g one for the iterating template rewrite. Mixing\n// .test() and .replace() on the same /g regex makes lastIndex management\n// fragile — easy to forget the reset on a future edit.\nconst HAS_DOLLAR_PROPS_CLASS = /\\$\\$props\\.class\\b/;\nconst DOLLAR_PROPS_CLASS_GLOBAL = /\\$\\$props\\.class\\b/g;\nconst DOLLAR_PROPS_ANY = /\\$\\$props\\b/;\nconst SCRIPT_BLOCK = /<script\\b[^>]*>[\\s\\S]*?<\\/script>/g;\nconst MIGRATION_TASK = /^<!--\\s*@migration-task[\\s\\S]*?-->\\s*\\n?/gm;\nconst IDENT = \"className\";\n\nfunction maskScripts(source: string): { masked: string; blocks: string[] } {\n const blocks: string[] = [];\n const masked = source.replace(SCRIPT_BLOCK, (m) => {\n blocks.push(m);\n return `__SCRIPT_${blocks.length - 1}__`;\n });\n return { masked, blocks };\n}\n\nfunction restoreScripts(masked: string, blocks: string[]): string {\n let out = masked;\n blocks.forEach((blk, i) => {\n out = out.replace(`__SCRIPT_${i}__`, blk);\n });\n return out;\n}\n\nexport function dollarPropsClass(source: string): string {\n // Bail early if the template doesn't reference $$props.class\n const { masked } = maskScripts(source);\n if (!HAS_DOLLAR_PROPS_CLASS.test(masked)) return source;\n\n // Bail if there's no $props() destructuring to extend\n if (!PROPS_DESTRUCTURE.test(source)) return source;\n\n let updated = source.replace(PROPS_DESTRUCTURE, (full, body, typeAnno, typeBody) => {\n // Already migrated (someone added class manually)\n if (/\\bclass\\s*:/.test(body as string)) return full;\n\n const cleanBody = (body as string).trim().replace(/,\\s*$/, \"\").trim();\n const newBody = cleanBody ? `${cleanBody}, class: ${IDENT} = \"\"` : `class: ${IDENT} = \"\"`;\n\n if (typeAnno) {\n const cleanType = ((typeBody as string) ?? \"\").trim().replace(/;\\s*$/, \"\").trim();\n const newType = cleanType ? `${cleanType}; class?: string` : `class?: string`;\n return `let { ${newBody} }: { ${newType} } = $props()`;\n }\n return `let { ${newBody} } = $props()`;\n });\n\n // Replace $$props.class in template only (re-mask after destructuring update)\n const reMasked = maskScripts(updated);\n const templateRewritten = reMasked.masked.replace(DOLLAR_PROPS_CLASS_GLOBAL, IDENT);\n updated = restoreScripts(templateRewritten, reMasked.blocks);\n\n // Strip @migration-task comments if no $$props references remain anywhere\n // EXCEPT inside those very comments. Strip-then-check, restore if still dirty.\n const stripped = updated.replace(MIGRATION_TASK, \"\");\n if (!DOLLAR_PROPS_ANY.test(stripped)) {\n updated = stripped;\n }\n\n return updated;\n}\n","/**\n * Converts Svelte 4 `$:` reactive statements to Svelte 5 runes.\n *\n * - `$: var = expr;` → `let var = $derived(expr);`\n * - `$: { body }` → `$effect(() => { body });`\n *\n * Triggered by:\n * \"`$:` is not allowed in runes mode, use `$derived` or `$effect` instead\"\n * (svelte.dev/e/legacy_reactive_statement_invalid)\n *\n * Block patterns become `$effect` rather than per-variable `$derived` calls\n * because the block typically mutates multiple already-declared `let`\n * variables with conditional logic — too contextual for a safe automatic\n * decomposition into discrete derived values. The user can refine each\n * `$effect` into idiomatic `$derived` calls afterward if desired.\n *\n * Scoped to `<script>` content only — `$:` in template/style text is left\n * alone (it would only ever appear there as literal text anyway).\n */\nimport { findStringEnd } from \"../../../util/svelte-source.js\";\n\nconst SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/g;\nconst SIMPLE_REACTIVE = /^([ \\t]*)\\$:\\s*(\\w+)\\s*=\\s*([^;\\n]+);?[ \\t]*$/gm;\nconst BLOCK_REACTIVE_HEAD = /(^|\\n)([ \\t]*)\\$:\\s*\\{/g;\n\nfunction findMatchingClose(source: string, openIdx: number): number {\n let depth = 0;\n let i = openIdx;\n while (i < source.length) {\n const ch = source[i];\n // Skip over string literals so braces inside strings don't fool the counter.\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n const closeStr = findStringEnd(source, i);\n if (closeStr === -1) return -1;\n i = closeStr + 1;\n continue;\n }\n // Skip over comments so braces inside `// }` or `/* } */` don't fool the\n // counter. Regression: the old version silently corrupted source (depth\n // went off, real closing brace mis-matched) on inputs like `$: { // } ... }`.\n // The corrupted output still compiles in Svelte 5 — no parser to scream.\n if (ch === \"/\") {\n const next = source[i + 1];\n if (next === \"/\") {\n const eol = source.indexOf(\"\\n\", i + 2);\n i = eol === -1 ? source.length : eol; // step onto newline; outer loop handles it\n continue;\n }\n if (next === \"*\") {\n const end = source.indexOf(\"*/\", i + 2);\n if (end === -1) return -1; // unterminated block comment — bail rather than corrupt\n i = end + 2;\n continue;\n }\n }\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n depth--;\n if (depth === 0) return i;\n }\n i++;\n }\n return -1;\n}\n\n/** Flag each converted `$effect` block for manual review. The conversion is\n * syntactically safe (compiles), but if any of the locals the block mutates\n * was declared as plain `let` (not `$state`), the `$effect` runs once on\n * mount and never again — code silently loses its reactivity. We can't\n * detect that automatically (it would require scope analysis on the\n * declaration sites), so we leave a breadcrumb for the human reviewer. */\nconst MIGRATION_MARKER =\n \"// @migration-task: $effect won't trigger UI updates on plain `let` bindings — refine mutated locals to $state or split into per-variable $derived.\";\n\nfunction transformBlocks(body: string): string {\n const out: string[] = [];\n let last = 0;\n BLOCK_REACTIVE_HEAD.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = BLOCK_REACTIVE_HEAD.exec(body)) !== null) {\n const leadingNewline = m[1] ?? \"\";\n const indent = m[2] ?? \"\";\n const headEnd = m.index + m[0].length; // position just after `{`\n const openBraceIdx = headEnd - 1;\n const closeBraceIdx = findMatchingClose(body, openBraceIdx);\n if (closeBraceIdx === -1) continue;\n out.push(body.slice(last, m.index));\n out.push(leadingNewline);\n const blockBody = body.slice(openBraceIdx + 1, closeBraceIdx);\n out.push(`${indent}${MIGRATION_MARKER}\\n`);\n out.push(`${indent}$effect(() => {${blockBody}});`);\n last = closeBraceIdx + 1;\n BLOCK_REACTIVE_HEAD.lastIndex = last;\n }\n out.push(body.slice(last));\n return out.join(\"\");\n}\n\nfunction transformSimple(body: string): string {\n return body.replace(SIMPLE_REACTIVE, (_full, indent: string, name: string, expr: string) => {\n return `${indent}let ${name} = $derived(${expr.trim()});`;\n });\n}\n\nexport function legacyReactiveToRunes(source: string): string {\n return source.replace(SCRIPT_BLOCK, (full, _attrs: string, body: string) => {\n // Blocks first so an outer `$: { ... }` containing nothing matchable\n // for the simple pass still gets wrapped. Order doesn't matter for the\n // patterns currently in the fleet but keeps the codemod robust to future\n // shapes.\n let next = transformBlocks(body);\n next = transformSimple(next);\n if (next === body) return full;\n return full.replace(body, next);\n });\n}\n"],"mappings":"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","names":["SCRIPT_BLOCK","SCRIPT_BLOCK","SCRIPT_BLOCK","SCRIPT_BLOCK"]}
1
+ {"version":3,"sources":["../src/recipes/svelte-5/step-gotchas.ts","../src/recipes/svelte-5/codemods/on-event-to-handler.ts","../src/recipes/svelte-5/codemods/dollar-props.ts","../src/util/svelte-source.ts","../src/recipes/svelte-5/codemods/dollar-restprops.ts","../src/recipes/svelte-5/codemods/state-effect-sync.ts","../src/recipes/svelte-5/codemods/dollar-props-class.ts","../src/recipes/svelte-5/codemods/legacy-reactive.ts"],"sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { glob } from \"tinyglobby\";\nimport { onEventToHandler } from \"./codemods/on-event-to-handler.js\";\nimport { exportLetToProps } from \"./codemods/dollar-props.js\";\nimport { removeDollarRestProps } from \"./codemods/dollar-restprops.js\";\nimport { stateEffectSyncToDerived } from \"./codemods/state-effect-sync.js\";\nimport { dollarPropsClass } from \"./codemods/dollar-props-class.js\";\nimport { legacyReactiveToRunes } from \"./codemods/legacy-reactive.js\";\n\nconst SVELTE_GLOBS = [\"src/**/*.svelte\"];\nconst IGNORE = [\"node_modules/**\", \".svelte-kit/**\", \"build/**\"];\n\ntype Codemod = (src: string) => string;\n\n// Order matters: exportLetToProps creates the $props() destructuring that\n// dollarPropsClass extends with a `class:` named prop.\nconst CODEMODS: Codemod[] = [\n onEventToHandler,\n exportLetToProps,\n removeDollarRestProps,\n stateEffectSyncToDerived,\n dollarPropsClass,\n legacyReactiveToRunes,\n];\n\nexport type CodemodChange = { rel: string; after: string };\n\nexport async function planGotchaCodemods(cwd: string): Promise<CodemodChange[]> {\n const changes: CodemodChange[] = [];\n const relPaths = await glob(SVELTE_GLOBS, { cwd, ignore: IGNORE, absolute: false });\n for (const rel of relPaths) {\n const path = join(cwd, rel);\n const before = await readFile(path, \"utf-8\");\n const after = CODEMODS.reduce((s, fn) => fn(s), before);\n if (after !== before) changes.push({ rel, after });\n }\n return changes;\n}\n\nexport async function applyGotchaCodemods(cwd: string): Promise<{ filesChanged: number }> {\n const changes = await planGotchaCodemods(cwd);\n for (const c of changes) {\n await writeFile(join(cwd, c.rel), c.after, \"utf-8\");\n }\n return { filesChanged: changes.length };\n}\n","const SCRIPT_BLOCK = /<script\\b[^>]*>[\\s\\S]*?<\\/script>/g;\nconst SIMPLE_ON_EVENT = /\\bon:([a-z]+)(?=\\s*=)/g;\nconst MODIFIER_EVENT = /\\bon:[a-z]+\\|[a-zA-Z]+(?=\\s*=)/g;\n\n/** Svelte 5 removed event modifier syntax (`on:click|preventDefault={fn}`).\n * The rewrite is non-trivial — the modifier behavior must be inlined into\n * the handler body — so this codemod doesn't attempt it automatically.\n * Instead it inserts a `@migration-task` marker immediately above each\n * offending element so the user gets a visible audit trail rather than\n * a silent build error from the Svelte 5 compiler. */\nfunction flagEventModifiers(source: string): string {\n const insertions: Array<{ tagStart: number; indent: string; modifier: string }> = [];\n let m: RegExpExecArray | null;\n MODIFIER_EVENT.lastIndex = 0;\n while ((m = MODIFIER_EVENT.exec(source)) !== null) {\n const tagStart = source.lastIndexOf(\"<\", m.index);\n if (tagStart === -1) continue;\n\n // Idempotency: if the line immediately above the tag already carries an\n // @migration-task marker for this site, don't double-insert on re-run.\n const prevLineEnd = tagStart - 1;\n if (prevLineEnd >= 0) {\n const prevLineStart = source.lastIndexOf(\"\\n\", prevLineEnd - 1) + 1;\n const prevLine = source.slice(prevLineStart, prevLineEnd + 1);\n if (/<!--\\s*@migration-task/.test(prevLine)) continue;\n }\n\n const lineStart = source.lastIndexOf(\"\\n\", tagStart - 1) + 1;\n const indent = source.slice(lineStart, tagStart);\n const safeIndent = /^[ \\t]*$/.test(indent) ? indent : \"\";\n insertions.push({ tagStart, indent: safeIndent, modifier: m[0] });\n }\n\n // Apply back-to-front so earlier insertion offsets stay valid.\n let out = source;\n for (let i = insertions.length - 1; i >= 0; i--) {\n const { tagStart, indent, modifier } = insertions[i]!;\n const comment = `<!-- @migration-task: Svelte 5 removed event modifier syntax (\\`${modifier}\\`). Rewrite inline, e.g. onclick={(e) => { e.preventDefault(); ... }}. -->\\n${indent}`;\n out = out.slice(0, tagStart) + comment + out.slice(tagStart);\n }\n return out;\n}\n\nexport function onEventToHandler(source: string): string {\n const masked: string[] = [];\n const placeholder = (i: number): string => ` SCRIPT_${i} `;\n const intermediate = source.replace(SCRIPT_BLOCK, (match) => {\n masked.push(match);\n return placeholder(masked.length - 1);\n });\n\n let processed = intermediate.replace(SIMPLE_ON_EVENT, (_full, name: string) => `on${name}`);\n processed = flagEventModifiers(processed);\n\n let out = processed;\n masked.forEach((blk, i) => {\n out = out.replace(placeholder(i), blk);\n });\n\n return out;\n}\n","const SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/;\nconst EXPORT_LET = /^\\s*export\\s+let\\s+(\\w+)\\s*(?::\\s*([^=;\\n]+))?\\s*(?:=\\s*([^;\\n]+))?;?\\s*$/gm;\n\ntype Prop = { name: string; type?: string | undefined; defaultExpr?: string | undefined };\n\nfunction transformScript(scriptBody: string, isTs: boolean): { body: string; changed: boolean } {\n const props: Prop[] = [];\n const cleaned = scriptBody.replace(\n EXPORT_LET,\n (_full, name: string, type?: string, defaultExpr?: string) => {\n props.push({\n name,\n type: type?.trim(),\n defaultExpr: defaultExpr?.trim(),\n });\n return \"\";\n },\n );\n if (props.length === 0) return { body: scriptBody, changed: false };\n\n const destructured = props\n .map((p) => (p.defaultExpr ? `${p.name} = ${p.defaultExpr}` : p.name))\n .join(\", \");\n\n let decl: string;\n if (isTs) {\n const typeSig = props\n .map((p) => {\n const optional = p.defaultExpr ? \"?\" : \"\";\n return `${p.name}${optional}: ${p.type ?? \"unknown\"}`;\n })\n .join(\"; \");\n decl = ` let { ${destructured} }: { ${typeSig} } = $props();`;\n } else {\n decl = ` let { ${destructured} } = $props();`;\n }\n\n const next = cleaned.replace(/^(\\s*)/, (m) => `${m}${decl}\\n`);\n return { body: next, changed: true };\n}\n\nexport function exportLetToProps(source: string): string {\n const match = source.match(SCRIPT_BLOCK);\n if (!match) return source;\n const attrs = match[1] ?? \"\";\n const inner = match[2] ?? \"\";\n const isTs = /\\blang=[\"']ts[\"']/.test(attrs);\n const { body, changed } = transformScript(inner, isTs);\n if (!changed) return source;\n return source.replace(SCRIPT_BLOCK, (full) => full.replace(inner, body));\n}\n","/** Find the index of the closing quote for a string literal that opens at\n * `openIdx`. Handles backslash escapes. Returns -1 if the string is\n * unterminated.\n *\n * Treats backtick template literals the same as `'…'` / `\"…\"` — the\n * closing backtick terminates. Callers needing precise `${…}` interpolation\n * handling will need a real parser; this helper is intentionally simple\n * and good enough for the codemod-grade string masking we do today. */\nexport function findStringEnd(source: string, openIdx: number): number {\n const quote = source[openIdx];\n let i = openIdx + 1;\n while (i < source.length) {\n const ch = source[i];\n if (ch === \"\\\\\") {\n i += 2;\n continue;\n }\n if (ch === quote) return i;\n i++;\n }\n return -1;\n}\n","/** Locate `interface $$Props {` declarations and remove them, including\n * the matching closing `}` even if the body has nested braces or spans\n * multiple lines. Regex alone can't do balanced-brace matching, so we\n * walk the string manually. */\nfunction removeInterfaceBlock(source: string): string {\n const re = /^\\s*interface\\s+\\$\\$Props\\s*\\{/m;\n let out = source;\n while (true) {\n const match = re.exec(out);\n if (!match) return out;\n\n const openBraceIdx = match.index + match[0].length - 1;\n let depth = 1;\n let i = openBraceIdx + 1;\n while (i < out.length && depth > 0) {\n const ch = out[i];\n if (ch === \"{\") depth++;\n else if (ch === \"}\") depth--;\n i++;\n }\n if (depth !== 0) return out; // unbalanced; bail rather than corrupt\n\n // Consume trailing whitespace through end-of-line.\n let endIdx = i;\n while (endIdx < out.length && /[ \\t]/.test(out[endIdx] ?? \"\")) endIdx++;\n if (out[endIdx] === \"\\n\") endIdx++;\n\n out = out.slice(0, match.index) + out.slice(endIdx);\n }\n}\n\nimport { findStringEnd } from \"../../../util/svelte-source.js\";\n\n/** Mask every `'…'`, `\"…\"`, and template literal in `source` with a placeholder\n * so subsequent regex passes can rewrite identifiers without corrupting string\n * contents. Returns the masked body and a function to restore originals. */\nfunction maskStringLiterals(source: string): {\n masked: string;\n restore: (s: string) => string;\n} {\n const strings: string[] = [];\n let out = \"\";\n let i = 0;\n while (i < source.length) {\n const ch = source[i];\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n const closeIdx = findStringEnd(source, i);\n if (closeIdx === -1) {\n out += source.slice(i);\n break;\n }\n const literal = source.slice(i, closeIdx + 1);\n out += `__RDMNT_STR_${strings.length}__`;\n strings.push(literal);\n i = closeIdx + 1;\n } else {\n out += ch;\n i++;\n }\n }\n return {\n masked: out,\n restore: (s) => s.replace(/__RDMNT_STR_(\\d+)__/g, (_full, idx) => strings[Number(idx)] ?? \"\"),\n };\n}\n\nconst PROPS_DECL = /let\\s*\\{([^}]*)\\}\\s*(?::\\s*\\{([^}]*)\\})?\\s*=\\s*\\$props\\(\\)\\s*;?/;\n\n/** If the script declares `let { … } = $props();` (with or without an inline\n * type annotation) and doesn't already collect `...rest`, inject it. For TS,\n * widen the inline type with an `[key: string]: unknown` index signature so\n * the rest binding actually captures excess attributes (without the widening,\n * TS infers `rest` as `{}` and the spread forwards nothing). */\nfunction injectRestIntoProps(scriptBody: string): string {\n const match = scriptBody.match(PROPS_DECL);\n if (!match) return scriptBody;\n const destructured = match[1] ?? \"\";\n if (/\\.\\.\\.\\s*\\w+/.test(destructured)) return scriptBody; // already has rest\n\n // Strip any trailing comma left over from a multi-line destructuring shape\n // (e.g. `{ foo, bar, }`). Without this, the template literal below emits\n // `bar,, ...rest` — invalid syntax that the codemod was happily committing\n // (regression: caltex's Accordian.svelte, 2026-05-27).\n const trimmed = destructured.trim().replace(/,\\s*$/, \"\");\n const newDestructured = trimmed === \"\" ? \" ...rest \" : ` ${trimmed}, ...rest `;\n\n let replacement: string;\n if (match[2] !== undefined) {\n const typeBody = match[2];\n const hasIndexSig = /\\[\\s*key\\s*:\\s*string\\s*\\]\\s*:/.test(typeBody);\n const newTypeBody = hasIndexSig\n ? typeBody\n : `${typeBody.trimEnd().replace(/;?\\s*$/, \"\")}; [key: string]: unknown `;\n replacement = `let {${newDestructured}}: {${newTypeBody}} = $props();`;\n } else {\n replacement = `let {${newDestructured}} = $props();`;\n }\n return scriptBody.replace(PROPS_DECL, replacement);\n}\n\nconst SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/;\nconst HAS_PROPS_CALL = /\\$props\\(\\s*\\)/;\n\nexport function removeDollarRestProps(source: string): string {\n const next = removeInterfaceBlock(source);\n\n const scriptMatch = next.match(SCRIPT_BLOCK);\n if (!scriptMatch) return next;\n if (!HAS_PROPS_CALL.test(scriptMatch[2] ?? \"\")) {\n // No $props() in this script — refuse to rewrite $$restProps anywhere, since\n // doing so would emit references to an undeclared identifier. The user sees\n // the original $$restProps and a clear Svelte 5 build error to migrate by hand.\n return next;\n }\n\n const scriptInner = scriptMatch[2] ?? \"\";\n const { masked, restore } = maskStringLiterals(scriptInner);\n let processed = injectRestIntoProps(masked);\n processed = processed.replace(/\\$\\$restProps/g, \"rest\");\n const restoredInner = restore(processed);\n\n // Use a function callback so `$$` in the restored script body isn't\n // interpreted as the `$` substitution pattern by String.prototype.replace.\n const newScriptBlock = scriptMatch[0].replace(scriptInner, () => restoredInner);\n const before = next.slice(0, scriptMatch.index!);\n const after = next.slice(scriptMatch.index! + scriptMatch[0].length);\n\n // Template (outside script) gets a plain swap. Template attribute strings\n // containing the literal text \"$$restProps\" are vanishingly rare in practice;\n // accept the limitation rather than parse the whole template.\n return (\n before.replace(/\\$\\$restProps/g, \"rest\") +\n newScriptBlock +\n after.replace(/\\$\\$restProps/g, \"rest\")\n );\n}\n","/**\n * Collapses the \"manual sync state with prop\" anti-pattern into `$derived`.\n *\n * Input:\n * let content = $state(data.page.data);\n * $effect(() => { data; content = data.page.data });\n *\n * Output:\n * let content = $derived(data.page.data);\n *\n * Only transforms when the `$state(...)` initializer expression matches the\n * effect's right-hand assignment exactly (after trim). Intervening statements\n * between the `let` and the `$effect` block prevent the match — keeps the\n * codemod conservative.\n *\n * Triggered by Svelte 5's `state_referenced_locally` warning, which fires\n * whenever a local `let X = $state(prop.expr)` captures a prop reference\n * only at init time.\n */\n// `;?` before the closing `}` so the multi-line effect form matches:\n// $effect(() => {\n// data;\n// content = data.page.data;\n// });\n// as well as the single-line form: $effect(() => { data; content = data.page.data })\nconst PATTERN =\n /let\\s+(\\w+)\\s*=\\s*\\$state\\(\\s*([^)]+?)\\s*\\)\\s*;[ \\t\\r\\n]*\\$effect\\(\\s*\\(\\s*\\)\\s*=>\\s*\\{\\s*\\w+\\s*;\\s*\\1\\s*=\\s*([^;}]+?)\\s*;?\\s*\\}\\s*\\)\\s*;?/g;\n\nexport function stateEffectSyncToDerived(source: string): string {\n return source.replace(PATTERN, (full, name: string, initExpr: string, effectExpr: string) => {\n if (initExpr.trim() !== effectExpr.trim()) return full;\n return `let ${name} = $derived(${initExpr.trim()});`;\n });\n}\n","/**\n * Converts the legacy `$$props.class` pattern (passing extra HTML class from\n * a parent component) to a Svelte 5 named-prop destructuring.\n *\n * Input:\n * <script lang=\"ts\">\n * let { foo }: { foo?: string } = $props();\n * </script>\n * <div class=\"other {$$props.class || ''}\">x</div>\n *\n * Output:\n * <script lang=\"ts\">\n * let { foo, class: className = \"\" }: { foo?: string; class?: string } = $props();\n * </script>\n * <div class=\"other {className || ''}\">x</div>\n *\n * Triggered by Svelte 5 build errors:\n * \"Cannot use `$$props` in runes mode\" (svelte.dev/e/legacy_props_invalid)\n *\n * The original svelte-migrate tool flagged this with a `@migration-task`\n * comment because it couldn't safely combine `$$props` with already-named\n * props. We can: `class` is the dominant case across the reddoor fleet,\n * so we destructure it as `class: className = \"\"` (renamed because `class`\n * is a JS reserved word as a bare binding) and rewrite template references.\n *\n * Conservative: only transforms files that have BOTH a template\n * `$$props.class` reference AND an existing `$props()` destructuring.\n * Files using `$$props.class` without a `$props()` declaration are left\n * for the `exportLetToProps` codemod to handle in a prior pass.\n */\n// Note: lazy `[\\s\\S]*?` (not `[^}]*`) so default values containing braces\n// — `() => {}`, `{ foo: 1 }`, etc. — don't truncate the match early.\nconst PROPS_DESTRUCTURE = /let\\s*\\{([\\s\\S]*?)\\}(\\s*:\\s*\\{([\\s\\S]*?)\\})?\\s*=\\s*\\$props\\(\\)/;\n// Two regexes: a stateless one for \"does this string contain $$props.class?\"\n// existence checks, and the /g one for the iterating template rewrite. Mixing\n// .test() and .replace() on the same /g regex makes lastIndex management\n// fragile — easy to forget the reset on a future edit.\nconst HAS_DOLLAR_PROPS_CLASS = /\\$\\$props\\.class\\b/;\nconst DOLLAR_PROPS_CLASS_GLOBAL = /\\$\\$props\\.class\\b/g;\nconst DOLLAR_PROPS_ANY = /\\$\\$props\\b/;\nconst SCRIPT_BLOCK = /<script\\b[^>]*>[\\s\\S]*?<\\/script>/g;\nconst MIGRATION_TASK = /^<!--\\s*@migration-task[\\s\\S]*?-->\\s*\\n?/gm;\nconst IDENT = \"className\";\n\nfunction maskScripts(source: string): { masked: string; blocks: string[] } {\n const blocks: string[] = [];\n const masked = source.replace(SCRIPT_BLOCK, (m) => {\n blocks.push(m);\n return `__SCRIPT_${blocks.length - 1}__`;\n });\n return { masked, blocks };\n}\n\nfunction restoreScripts(masked: string, blocks: string[]): string {\n let out = masked;\n blocks.forEach((blk, i) => {\n out = out.replace(`__SCRIPT_${i}__`, blk);\n });\n return out;\n}\n\nexport function dollarPropsClass(source: string): string {\n // Bail early if the template doesn't reference $$props.class\n const { masked } = maskScripts(source);\n if (!HAS_DOLLAR_PROPS_CLASS.test(masked)) return source;\n\n // Bail if there's no $props() destructuring to extend\n if (!PROPS_DESTRUCTURE.test(source)) return source;\n\n let updated = source.replace(PROPS_DESTRUCTURE, (full, body, typeAnno, typeBody) => {\n // Already migrated (someone added class manually)\n if (/\\bclass\\s*:/.test(body as string)) return full;\n\n const cleanBody = (body as string).trim().replace(/,\\s*$/, \"\").trim();\n // A rest element (`...rest`) MUST stay last in a destructuring pattern, so\n // insert `class` BEFORE it rather than appending — otherwise we emit the\n // invalid `{ …, ...rest, class: className }` (\"a rest element must be last\").\n // The destructuring this codemod extends can already carry a trailing rest\n // from exportLetToProps or the official svelte-migrate pass.\n const restMatch = cleanBody.match(/,?\\s*(\\.\\.\\.[A-Za-z_$][\\w$]*)\\s*$/);\n let newBody: string;\n if (restMatch) {\n const beforeRest = cleanBody\n .slice(0, restMatch.index ?? 0)\n .replace(/,\\s*$/, \"\")\n .trim();\n const rest = restMatch[1];\n newBody = beforeRest\n ? `${beforeRest}, class: ${IDENT} = \"\", ${rest}`\n : `class: ${IDENT} = \"\", ${rest}`;\n } else {\n newBody = cleanBody ? `${cleanBody}, class: ${IDENT} = \"\"` : `class: ${IDENT} = \"\"`;\n }\n\n if (typeAnno) {\n const cleanType = ((typeBody as string) ?? \"\").trim().replace(/;\\s*$/, \"\").trim();\n const newType = cleanType ? `${cleanType}; class?: string` : `class?: string`;\n return `let { ${newBody} }: { ${newType} } = $props()`;\n }\n return `let { ${newBody} } = $props()`;\n });\n\n // Replace $$props.class in template only (re-mask after destructuring update)\n const reMasked = maskScripts(updated);\n const templateRewritten = reMasked.masked.replace(DOLLAR_PROPS_CLASS_GLOBAL, IDENT);\n updated = restoreScripts(templateRewritten, reMasked.blocks);\n\n // Strip @migration-task comments if no $$props references remain anywhere\n // EXCEPT inside those very comments. Strip-then-check, restore if still dirty.\n const stripped = updated.replace(MIGRATION_TASK, \"\");\n if (!DOLLAR_PROPS_ANY.test(stripped)) {\n updated = stripped;\n }\n\n return updated;\n}\n","/**\n * Converts Svelte 4 `$:` reactive statements to Svelte 5 runes.\n *\n * - `$: var = expr;` → `let var = $derived(expr);`\n * - `$: { body }` → `$effect(() => { body });`\n *\n * Triggered by:\n * \"`$:` is not allowed in runes mode, use `$derived` or `$effect` instead\"\n * (svelte.dev/e/legacy_reactive_statement_invalid)\n *\n * Block patterns become `$effect` rather than per-variable `$derived` calls\n * because the block typically mutates multiple already-declared `let`\n * variables with conditional logic — too contextual for a safe automatic\n * decomposition into discrete derived values. The user can refine each\n * `$effect` into idiomatic `$derived` calls afterward if desired.\n *\n * Scoped to `<script>` content only — `$:` in template/style text is left\n * alone (it would only ever appear there as literal text anyway).\n */\nimport { findStringEnd } from \"../../../util/svelte-source.js\";\n\nconst SCRIPT_BLOCK = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/g;\nconst SIMPLE_REACTIVE = /^([ \\t]*)\\$:\\s*(\\w+)\\s*=\\s*([^;\\n]+);?[ \\t]*$/gm;\nconst BLOCK_REACTIVE_HEAD = /(^|\\n)([ \\t]*)\\$:\\s*\\{/g;\n\nfunction findMatchingClose(source: string, openIdx: number): number {\n let depth = 0;\n let i = openIdx;\n while (i < source.length) {\n const ch = source[i];\n // Skip over string literals so braces inside strings don't fool the counter.\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n const closeStr = findStringEnd(source, i);\n if (closeStr === -1) return -1;\n i = closeStr + 1;\n continue;\n }\n // Skip over comments so braces inside `// }` or `/* } */` don't fool the\n // counter. Regression: the old version silently corrupted source (depth\n // went off, real closing brace mis-matched) on inputs like `$: { // } ... }`.\n // The corrupted output still compiles in Svelte 5 — no parser to scream.\n if (ch === \"/\") {\n const next = source[i + 1];\n if (next === \"/\") {\n const eol = source.indexOf(\"\\n\", i + 2);\n i = eol === -1 ? source.length : eol; // step onto newline; outer loop handles it\n continue;\n }\n if (next === \"*\") {\n const end = source.indexOf(\"*/\", i + 2);\n if (end === -1) return -1; // unterminated block comment — bail rather than corrupt\n i = end + 2;\n continue;\n }\n }\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n depth--;\n if (depth === 0) return i;\n }\n i++;\n }\n return -1;\n}\n\n/** Flag each converted `$effect` block for manual review. The conversion is\n * syntactically safe (compiles), but if any of the locals the block mutates\n * was declared as plain `let` (not `$state`), the `$effect` runs once on\n * mount and never again — code silently loses its reactivity. We can't\n * detect that automatically (it would require scope analysis on the\n * declaration sites), so we leave a breadcrumb for the human reviewer. */\nconst MIGRATION_MARKER =\n \"// @migration-task: $effect won't trigger UI updates on plain `let` bindings — refine mutated locals to $state or split into per-variable $derived.\";\n\nfunction transformBlocks(body: string): string {\n const out: string[] = [];\n let last = 0;\n BLOCK_REACTIVE_HEAD.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = BLOCK_REACTIVE_HEAD.exec(body)) !== null) {\n const leadingNewline = m[1] ?? \"\";\n const indent = m[2] ?? \"\";\n const headEnd = m.index + m[0].length; // position just after `{`\n const openBraceIdx = headEnd - 1;\n const closeBraceIdx = findMatchingClose(body, openBraceIdx);\n if (closeBraceIdx === -1) continue;\n out.push(body.slice(last, m.index));\n out.push(leadingNewline);\n const blockBody = body.slice(openBraceIdx + 1, closeBraceIdx);\n out.push(`${indent}${MIGRATION_MARKER}\\n`);\n out.push(`${indent}$effect(() => {${blockBody}});`);\n last = closeBraceIdx + 1;\n BLOCK_REACTIVE_HEAD.lastIndex = last;\n }\n out.push(body.slice(last));\n return out.join(\"\");\n}\n\nfunction transformSimple(body: string): string {\n return body.replace(SIMPLE_REACTIVE, (_full, indent: string, name: string, expr: string) => {\n return `${indent}let ${name} = $derived(${expr.trim()});`;\n });\n}\n\nexport function legacyReactiveToRunes(source: string): string {\n return source.replace(SCRIPT_BLOCK, (full, _attrs: string, body: string) => {\n // Blocks first so an outer `$: { ... }` containing nothing matchable\n // for the simple pass still gets wrapped. Order doesn't matter for the\n // patterns currently in the fleet but keeps the codemod robust to future\n // shapes.\n let next = transformBlocks(body);\n next = transformSimple(next);\n if (next === body) return full;\n return full.replace(body, next);\n });\n}\n"],"mappings":";AAAA,SAAS,UAAU,iBAAiB;AACpC,SAAS,YAAY;AACrB,SAAS,YAAY;;;ACFrB,IAAM,eAAe;AACrB,IAAM,kBAAkB;AACxB,IAAM,iBAAiB;AAQvB,SAAS,mBAAmB,QAAwB;AAClD,QAAM,aAA4E,CAAC;AACnF,MAAI;AACJ,iBAAe,YAAY;AAC3B,UAAQ,IAAI,eAAe,KAAK,MAAM,OAAO,MAAM;AACjD,UAAM,WAAW,OAAO,YAAY,KAAK,EAAE,KAAK;AAChD,QAAI,aAAa,GAAI;AAIrB,UAAM,cAAc,WAAW;AAC/B,QAAI,eAAe,GAAG;AACpB,YAAM,gBAAgB,OAAO,YAAY,MAAM,cAAc,CAAC,IAAI;AAClE,YAAM,WAAW,OAAO,MAAM,eAAe,cAAc,CAAC;AAC5D,UAAI,yBAAyB,KAAK,QAAQ,EAAG;AAAA,IAC/C;AAEA,UAAM,YAAY,OAAO,YAAY,MAAM,WAAW,CAAC,IAAI;AAC3D,UAAM,SAAS,OAAO,MAAM,WAAW,QAAQ;AAC/C,UAAM,aAAa,WAAW,KAAK,MAAM,IAAI,SAAS;AACtD,eAAW,KAAK,EAAE,UAAU,QAAQ,YAAY,UAAU,EAAE,CAAC,EAAE,CAAC;AAAA,EAClE;AAGA,MAAI,MAAM;AACV,WAAS,IAAI,WAAW,SAAS,GAAG,KAAK,GAAG,KAAK;AAC/C,UAAM,EAAE,UAAU,QAAQ,SAAS,IAAI,WAAW,CAAC;AACnD,UAAM,UAAU,mEAAmE,QAAQ;AAAA,EAAgF,MAAM;AACjL,UAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,UAAU,IAAI,MAAM,QAAQ;AAAA,EAC7D;AACA,SAAO;AACT;AAEO,SAAS,iBAAiB,QAAwB;AACvD,QAAM,SAAmB,CAAC;AAC1B,QAAM,cAAc,CAAC,MAAsB,WAAW,CAAC;AACvD,QAAM,eAAe,OAAO,QAAQ,cAAc,CAAC,UAAU;AAC3D,WAAO,KAAK,KAAK;AACjB,WAAO,YAAY,OAAO,SAAS,CAAC;AAAA,EACtC,CAAC;AAED,MAAI,YAAY,aAAa,QAAQ,iBAAiB,CAAC,OAAO,SAAiB,KAAK,IAAI,EAAE;AAC1F,cAAY,mBAAmB,SAAS;AAExC,MAAI,MAAM;AACV,SAAO,QAAQ,CAAC,KAAK,MAAM;AACzB,UAAM,IAAI,QAAQ,YAAY,CAAC,GAAG,GAAG;AAAA,EACvC,CAAC;AAED,SAAO;AACT;;;AC5DA,IAAMA,gBAAe;AACrB,IAAM,aAAa;AAInB,SAAS,gBAAgB,YAAoB,MAAmD;AAC9F,QAAM,QAAgB,CAAC;AACvB,QAAM,UAAU,WAAW;AAAA,IACzB;AAAA,IACA,CAAC,OAAO,MAAc,MAAe,gBAAyB;AAC5D,YAAM,KAAK;AAAA,QACT;AAAA,QACA,MAAM,MAAM,KAAK;AAAA,QACjB,aAAa,aAAa,KAAK;AAAA,MACjC,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,MAAM,WAAW,EAAG,QAAO,EAAE,MAAM,YAAY,SAAS,MAAM;AAElE,QAAM,eAAe,MAClB,IAAI,CAAC,MAAO,EAAE,cAAc,GAAG,EAAE,IAAI,MAAM,EAAE,WAAW,KAAK,EAAE,IAAK,EACpE,KAAK,IAAI;AAEZ,MAAI;AACJ,MAAI,MAAM;AACR,UAAM,UAAU,MACb,IAAI,CAAC,MAAM;AACV,YAAM,WAAW,EAAE,cAAc,MAAM;AACvC,aAAO,GAAG,EAAE,IAAI,GAAG,QAAQ,KAAK,EAAE,QAAQ,SAAS;AAAA,IACrD,CAAC,EACA,KAAK,IAAI;AACZ,WAAO,WAAW,YAAY,SAAS,OAAO;AAAA,EAChD,OAAO;AACL,WAAO,WAAW,YAAY;AAAA,EAChC;AAEA,QAAM,OAAO,QAAQ,QAAQ,UAAU,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI;AAAA,CAAI;AAC7D,SAAO,EAAE,MAAM,MAAM,SAAS,KAAK;AACrC;AAEO,SAAS,iBAAiB,QAAwB;AACvD,QAAM,QAAQ,OAAO,MAAMA,aAAY;AACvC,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,QAAQ,MAAM,CAAC,KAAK;AAC1B,QAAM,QAAQ,MAAM,CAAC,KAAK;AAC1B,QAAM,OAAO,oBAAoB,KAAK,KAAK;AAC3C,QAAM,EAAE,MAAM,QAAQ,IAAI,gBAAgB,OAAO,IAAI;AACrD,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,OAAO,QAAQA,eAAc,CAAC,SAAS,KAAK,QAAQ,OAAO,IAAI,CAAC;AACzE;;;AC1CO,SAAS,cAAc,QAAgB,SAAyB;AACrE,QAAM,QAAQ,OAAO,OAAO;AAC5B,MAAI,IAAI,UAAU;AAClB,SAAO,IAAI,OAAO,QAAQ;AACxB,UAAM,KAAK,OAAO,CAAC;AACnB,QAAI,OAAO,MAAM;AACf,WAAK;AACL;AAAA,IACF;AACA,QAAI,OAAO,MAAO,QAAO;AACzB;AAAA,EACF;AACA,SAAO;AACT;;;ACjBA,SAAS,qBAAqB,QAAwB;AACpD,QAAM,KAAK;AACX,MAAI,MAAM;AACV,SAAO,MAAM;AACX,UAAM,QAAQ,GAAG,KAAK,GAAG;AACzB,QAAI,CAAC,MAAO,QAAO;AAEnB,UAAM,eAAe,MAAM,QAAQ,MAAM,CAAC,EAAE,SAAS;AACrD,QAAI,QAAQ;AACZ,QAAI,IAAI,eAAe;AACvB,WAAO,IAAI,IAAI,UAAU,QAAQ,GAAG;AAClC,YAAM,KAAK,IAAI,CAAC;AAChB,UAAI,OAAO,IAAK;AAAA,eACP,OAAO,IAAK;AACrB;AAAA,IACF;AACA,QAAI,UAAU,EAAG,QAAO;AAGxB,QAAI,SAAS;AACb,WAAO,SAAS,IAAI,UAAU,QAAQ,KAAK,IAAI,MAAM,KAAK,EAAE,EAAG;AAC/D,QAAI,IAAI,MAAM,MAAM,KAAM;AAE1B,UAAM,IAAI,MAAM,GAAG,MAAM,KAAK,IAAI,IAAI,MAAM,MAAM;AAAA,EACpD;AACF;AAOA,SAAS,mBAAmB,QAG1B;AACA,QAAM,UAAoB,CAAC;AAC3B,MAAI,MAAM;AACV,MAAI,IAAI;AACR,SAAO,IAAI,OAAO,QAAQ;AACxB,UAAM,KAAK,OAAO,CAAC;AACnB,QAAI,OAAO,OAAO,OAAO,OAAO,OAAO,KAAK;AAC1C,YAAM,WAAW,cAAc,QAAQ,CAAC;AACxC,UAAI,aAAa,IAAI;AACnB,eAAO,OAAO,MAAM,CAAC;AACrB;AAAA,MACF;AACA,YAAM,UAAU,OAAO,MAAM,GAAG,WAAW,CAAC;AAC5C,aAAO,eAAe,QAAQ,MAAM;AACpC,cAAQ,KAAK,OAAO;AACpB,UAAI,WAAW;AAAA,IACjB,OAAO;AACL,aAAO;AACP;AAAA,IACF;AAAA,EACF;AACA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,SAAS,CAAC,MAAM,EAAE,QAAQ,wBAAwB,CAAC,OAAO,QAAQ,QAAQ,OAAO,GAAG,CAAC,KAAK,EAAE;AAAA,EAC9F;AACF;AAEA,IAAM,aAAa;AAOnB,SAAS,oBAAoB,YAA4B;AACvD,QAAM,QAAQ,WAAW,MAAM,UAAU;AACzC,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,eAAe,MAAM,CAAC,KAAK;AACjC,MAAI,eAAe,KAAK,YAAY,EAAG,QAAO;AAM9C,QAAM,UAAU,aAAa,KAAK,EAAE,QAAQ,SAAS,EAAE;AACvD,QAAM,kBAAkB,YAAY,KAAK,cAAc,IAAI,OAAO;AAElE,MAAI;AACJ,MAAI,MAAM,CAAC,MAAM,QAAW;AAC1B,UAAM,WAAW,MAAM,CAAC;AACxB,UAAM,cAAc,iCAAiC,KAAK,QAAQ;AAClE,UAAM,cAAc,cAChB,WACA,GAAG,SAAS,QAAQ,EAAE,QAAQ,UAAU,EAAE,CAAC;AAC/C,kBAAc,QAAQ,eAAe,OAAO,WAAW;AAAA,EACzD,OAAO;AACL,kBAAc,QAAQ,eAAe;AAAA,EACvC;AACA,SAAO,WAAW,QAAQ,YAAY,WAAW;AACnD;AAEA,IAAMC,gBAAe;AACrB,IAAM,iBAAiB;AAEhB,SAAS,sBAAsB,QAAwB;AAC5D,QAAM,OAAO,qBAAqB,MAAM;AAExC,QAAM,cAAc,KAAK,MAAMA,aAAY;AAC3C,MAAI,CAAC,YAAa,QAAO;AACzB,MAAI,CAAC,eAAe,KAAK,YAAY,CAAC,KAAK,EAAE,GAAG;AAI9C,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,YAAY,CAAC,KAAK;AACtC,QAAM,EAAE,QAAQ,QAAQ,IAAI,mBAAmB,WAAW;AAC1D,MAAI,YAAY,oBAAoB,MAAM;AAC1C,cAAY,UAAU,QAAQ,kBAAkB,MAAM;AACtD,QAAM,gBAAgB,QAAQ,SAAS;AAIvC,QAAM,iBAAiB,YAAY,CAAC,EAAE,QAAQ,aAAa,MAAM,aAAa;AAC9E,QAAM,SAAS,KAAK,MAAM,GAAG,YAAY,KAAM;AAC/C,QAAM,QAAQ,KAAK,MAAM,YAAY,QAAS,YAAY,CAAC,EAAE,MAAM;AAKnE,SACE,OAAO,QAAQ,kBAAkB,MAAM,IACvC,iBACA,MAAM,QAAQ,kBAAkB,MAAM;AAE1C;;;AC9GA,IAAM,UACJ;AAEK,SAAS,yBAAyB,QAAwB;AAC/D,SAAO,OAAO,QAAQ,SAAS,CAAC,MAAM,MAAc,UAAkB,eAAuB;AAC3F,QAAI,SAAS,KAAK,MAAM,WAAW,KAAK,EAAG,QAAO;AAClD,WAAO,OAAO,IAAI,eAAe,SAAS,KAAK,CAAC;AAAA,EAClD,CAAC;AACH;;;ACDA,IAAM,oBAAoB;AAK1B,IAAM,yBAAyB;AAC/B,IAAM,4BAA4B;AAClC,IAAM,mBAAmB;AACzB,IAAMC,gBAAe;AACrB,IAAM,iBAAiB;AACvB,IAAM,QAAQ;AAEd,SAAS,YAAY,QAAsD;AACzE,QAAM,SAAmB,CAAC;AAC1B,QAAM,SAAS,OAAO,QAAQA,eAAc,CAAC,MAAM;AACjD,WAAO,KAAK,CAAC;AACb,WAAO,YAAY,OAAO,SAAS,CAAC;AAAA,EACtC,CAAC;AACD,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAEA,SAAS,eAAe,QAAgB,QAA0B;AAChE,MAAI,MAAM;AACV,SAAO,QAAQ,CAAC,KAAK,MAAM;AACzB,UAAM,IAAI,QAAQ,YAAY,CAAC,MAAM,GAAG;AAAA,EAC1C,CAAC;AACD,SAAO;AACT;AAEO,SAAS,iBAAiB,QAAwB;AAEvD,QAAM,EAAE,OAAO,IAAI,YAAY,MAAM;AACrC,MAAI,CAAC,uBAAuB,KAAK,MAAM,EAAG,QAAO;AAGjD,MAAI,CAAC,kBAAkB,KAAK,MAAM,EAAG,QAAO;AAE5C,MAAI,UAAU,OAAO,QAAQ,mBAAmB,CAAC,MAAM,MAAM,UAAU,aAAa;AAElF,QAAI,cAAc,KAAK,IAAc,EAAG,QAAO;AAE/C,UAAM,YAAa,KAAgB,KAAK,EAAE,QAAQ,SAAS,EAAE,EAAE,KAAK;AAMpE,UAAM,YAAY,UAAU,MAAM,mCAAmC;AACrE,QAAI;AACJ,QAAI,WAAW;AACb,YAAM,aAAa,UAChB,MAAM,GAAG,UAAU,SAAS,CAAC,EAC7B,QAAQ,SAAS,EAAE,EACnB,KAAK;AACR,YAAM,OAAO,UAAU,CAAC;AACxB,gBAAU,aACN,GAAG,UAAU,YAAY,KAAK,UAAU,IAAI,KAC5C,UAAU,KAAK,UAAU,IAAI;AAAA,IACnC,OAAO;AACL,gBAAU,YAAY,GAAG,SAAS,YAAY,KAAK,UAAU,UAAU,KAAK;AAAA,IAC9E;AAEA,QAAI,UAAU;AACZ,YAAM,aAAc,YAAuB,IAAI,KAAK,EAAE,QAAQ,SAAS,EAAE,EAAE,KAAK;AAChF,YAAM,UAAU,YAAY,GAAG,SAAS,qBAAqB;AAC7D,aAAO,SAAS,OAAO,SAAS,OAAO;AAAA,IACzC;AACA,WAAO,SAAS,OAAO;AAAA,EACzB,CAAC;AAGD,QAAM,WAAW,YAAY,OAAO;AACpC,QAAM,oBAAoB,SAAS,OAAO,QAAQ,2BAA2B,KAAK;AAClF,YAAU,eAAe,mBAAmB,SAAS,MAAM;AAI3D,QAAM,WAAW,QAAQ,QAAQ,gBAAgB,EAAE;AACnD,MAAI,CAAC,iBAAiB,KAAK,QAAQ,GAAG;AACpC,cAAU;AAAA,EACZ;AAEA,SAAO;AACT;;;AC9FA,IAAMC,gBAAe;AACrB,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAE5B,SAAS,kBAAkB,QAAgB,SAAyB;AAClE,MAAI,QAAQ;AACZ,MAAI,IAAI;AACR,SAAO,IAAI,OAAO,QAAQ;AACxB,UAAM,KAAK,OAAO,CAAC;AAEnB,QAAI,OAAO,OAAO,OAAO,OAAO,OAAO,KAAK;AAC1C,YAAM,WAAW,cAAc,QAAQ,CAAC;AACxC,UAAI,aAAa,GAAI,QAAO;AAC5B,UAAI,WAAW;AACf;AAAA,IACF;AAKA,QAAI,OAAO,KAAK;AACd,YAAM,OAAO,OAAO,IAAI,CAAC;AACzB,UAAI,SAAS,KAAK;AAChB,cAAM,MAAM,OAAO,QAAQ,MAAM,IAAI,CAAC;AACtC,YAAI,QAAQ,KAAK,OAAO,SAAS;AACjC;AAAA,MACF;AACA,UAAI,SAAS,KAAK;AAChB,cAAM,MAAM,OAAO,QAAQ,MAAM,IAAI,CAAC;AACtC,YAAI,QAAQ,GAAI,QAAO;AACvB,YAAI,MAAM;AACV;AAAA,MACF;AAAA,IACF;AACA,QAAI,OAAO,IAAK;AAAA,aACP,OAAO,KAAK;AACnB;AACA,UAAI,UAAU,EAAG,QAAO;AAAA,IAC1B;AACA;AAAA,EACF;AACA,SAAO;AACT;AAQA,IAAM,mBACJ;AAEF,SAAS,gBAAgB,MAAsB;AAC7C,QAAM,MAAgB,CAAC;AACvB,MAAI,OAAO;AACX,sBAAoB,YAAY;AAChC,MAAI;AACJ,UAAQ,IAAI,oBAAoB,KAAK,IAAI,OAAO,MAAM;AACpD,UAAM,iBAAiB,EAAE,CAAC,KAAK;AAC/B,UAAM,SAAS,EAAE,CAAC,KAAK;AACvB,UAAM,UAAU,EAAE,QAAQ,EAAE,CAAC,EAAE;AAC/B,UAAM,eAAe,UAAU;AAC/B,UAAM,gBAAgB,kBAAkB,MAAM,YAAY;AAC1D,QAAI,kBAAkB,GAAI;AAC1B,QAAI,KAAK,KAAK,MAAM,MAAM,EAAE,KAAK,CAAC;AAClC,QAAI,KAAK,cAAc;AACvB,UAAM,YAAY,KAAK,MAAM,eAAe,GAAG,aAAa;AAC5D,QAAI,KAAK,GAAG,MAAM,GAAG,gBAAgB;AAAA,CAAI;AACzC,QAAI,KAAK,GAAG,MAAM,kBAAkB,SAAS,KAAK;AAClD,WAAO,gBAAgB;AACvB,wBAAoB,YAAY;AAAA,EAClC;AACA,MAAI,KAAK,KAAK,MAAM,IAAI,CAAC;AACzB,SAAO,IAAI,KAAK,EAAE;AACpB;AAEA,SAAS,gBAAgB,MAAsB;AAC7C,SAAO,KAAK,QAAQ,iBAAiB,CAAC,OAAO,QAAgB,MAAc,SAAiB;AAC1F,WAAO,GAAG,MAAM,OAAO,IAAI,eAAe,KAAK,KAAK,CAAC;AAAA,EACvD,CAAC;AACH;AAEO,SAAS,sBAAsB,QAAwB;AAC5D,SAAO,OAAO,QAAQA,eAAc,CAAC,MAAM,QAAgB,SAAiB;AAK1E,QAAI,OAAO,gBAAgB,IAAI;AAC/B,WAAO,gBAAgB,IAAI;AAC3B,QAAI,SAAS,KAAM,QAAO;AAC1B,WAAO,KAAK,QAAQ,MAAM,IAAI;AAAA,EAChC,CAAC;AACH;;;APzGA,IAAM,eAAe,CAAC,iBAAiB;AACvC,IAAM,SAAS,CAAC,mBAAmB,kBAAkB,UAAU;AAM/D,IAAM,WAAsB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIA,eAAsB,mBAAmB,KAAuC;AAC9E,QAAM,UAA2B,CAAC;AAClC,QAAM,WAAW,MAAM,KAAK,cAAc,EAAE,KAAK,QAAQ,QAAQ,UAAU,MAAM,CAAC;AAClF,aAAW,OAAO,UAAU;AAC1B,UAAM,OAAO,KAAK,KAAK,GAAG;AAC1B,UAAM,SAAS,MAAM,SAAS,MAAM,OAAO;AAC3C,UAAM,QAAQ,SAAS,OAAO,CAAC,GAAG,OAAO,GAAG,CAAC,GAAG,MAAM;AACtD,QAAI,UAAU,OAAQ,SAAQ,KAAK,EAAE,KAAK,MAAM,CAAC;AAAA,EACnD;AACA,SAAO;AACT;AAEA,eAAsB,oBAAoB,KAAgD;AACxF,QAAM,UAAU,MAAM,mBAAmB,GAAG;AAC5C,aAAW,KAAK,SAAS;AACvB,UAAM,UAAU,KAAK,KAAK,EAAE,GAAG,GAAG,EAAE,OAAO,OAAO;AAAA,EACpD;AACA,SAAO,EAAE,cAAc,QAAQ,OAAO;AACxC;","names":["SCRIPT_BLOCK","SCRIPT_BLOCK","SCRIPT_BLOCK","SCRIPT_BLOCK"]}
@@ -5,7 +5,7 @@ import {
5
5
  renderReportHtml,
6
6
  resolveCopy,
7
7
  uploadAttachment
8
- } from "./chunk-U3MF6RCS.js";
8
+ } from "./chunk-SYTPVYIK.js";
9
9
  import {
10
10
  checklistFor,
11
11
  createDraft,
@@ -17,7 +17,7 @@ import {
17
17
  import {
18
18
  siteSlug,
19
19
  updateAnalyticsHealth
20
- } from "./chunk-7RGQ4KLE.js";
20
+ } from "./chunk-CCXVX5JN.js";
21
21
  import {
22
22
  defaultCredentialsPath
23
23
  } from "./chunk-6VAL7XJT.js";
@@ -441,4 +441,4 @@ export {
441
441
  fetchGaUsers,
442
442
  fetchSearch
443
443
  };
444
- //# sourceMappingURL=chunk-M5LX2VSR.js.map
444
+ //# sourceMappingURL=chunk-JWA4FO2J.js.map
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2X322ZES.js";
4
4
  import {
5
5
  svelteCodemods
6
- } from "./chunk-FUD4I2ZW.js";
6
+ } from "./chunk-BTEBRTW4.js";
7
7
  import {
8
8
  onboard
9
9
  } from "./chunk-ZYBOT5PM.js";
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-XKOZUVK2.js";
16
16
  import {
17
17
  runAudits
18
- } from "./chunk-TC7Z5LG7.js";
18
+ } from "./chunk-WCN54744.js";
19
19
  import {
20
20
  siteLabel
21
21
  } from "./chunk-XXTZBPUY.js";
@@ -135,4 +135,4 @@ export {
135
135
  DEFAULT_INIT_STEPS,
136
136
  init
137
137
  };
138
- //# sourceMappingURL=chunk-FX3QBSUF.js.map
138
+ //# sourceMappingURL=chunk-LCKKIJ22.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  hasRealScores,
3
3
  lighthouseScoresFromResult
4
- } from "./chunk-CSTLFSGM.js";
4
+ } from "./chunk-N5MZ6WRX.js";
5
5
  import {
6
6
  detectAuditEvents
7
7
  } from "./chunk-5L3FPQIJ.js";
@@ -9,7 +9,7 @@ import {
9
9
  normalizeSecurityAdvisory,
10
10
  siteSlug,
11
11
  updateAuditFields
12
- } from "./chunk-7RGQ4KLE.js";
12
+ } from "./chunk-CCXVX5JN.js";
13
13
 
14
14
  // src/audits/a11y-airtable.ts
15
15
  function hasA11yCounts(result) {
@@ -40,7 +40,8 @@ function depsCountsFromResult(result) {
40
40
  const drifted = entries.filter((e) => e.drift !== "same").length;
41
41
  const majorBehind = entries.filter((e) => e.drift === "major").length;
42
42
  const outdated = details.outdated?.outdated ?? null;
43
- return { drifted, majorBehind, outdated };
43
+ const majorOutdated = details.outdated?.major ?? null;
44
+ return { drifted, majorBehind, outdated, majorOutdated };
44
45
  }
45
46
 
46
47
  // src/audits/security-airtable.ts
@@ -236,4 +237,4 @@ export {
236
237
  formatFleetWriteSummary,
237
238
  writeFleetAuditsToAirtable
238
239
  };
239
- //# sourceMappingURL=chunk-LM42CATB.js.map
240
+ //# sourceMappingURL=chunk-MEP6PR32.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/audits/a11y-airtable.ts","../src/audits/deps-airtable.ts","../src/audits/security-airtable.ts","../src/audits/domain-airtable.ts","../src/audits/browser-airtable.ts","../src/audits/netlify-deploy-airtable.ts","../src/audits/write-audits-to-airtable.ts"],"sourcesContent":["import type { AuditResult } from \"../types.js\";\n\ntype A11yDetails = {\n totalViolations: number;\n byImpact: Partial<Record<\"minor\" | \"moderate\" | \"serious\" | \"critical\", number>>;\n};\n\n/** True when an a11y AuditResult carries real counts worth persisting.\n * Mirrors the `hasRealScores` policy on lighthouse: write whenever real\n * data exists, regardless of status (a \"warn\" or \"fail\" with concrete\n * violation counts is exactly what the dashboard needs to track). */\nexport function hasA11yCounts(result: AuditResult): boolean {\n if (result.audit !== \"a11y\") return false;\n const details = result.details as A11yDetails | undefined;\n return typeof details?.totalViolations === \"number\";\n}\n\nexport function a11yCountsFromResult(result: AuditResult): { violations: number } {\n if (result.audit !== \"a11y\") {\n throw new Error(`Expected an 'a11y' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as A11yDetails | undefined;\n return { violations: details?.totalViolations ?? 0 };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { DepsDetails, DepsDriftEntry } from \"./deps.js\";\n\n/** True when a deps AuditResult carries the structured details (an entries\n * array, per {@link DepsDetails}). */\nexport function hasDepsCounts(result: AuditResult): boolean {\n if (result.audit !== \"deps\") return false;\n const d = result.details as Partial<DepsDetails> | undefined;\n return Array.isArray(d?.entries);\n}\n\nexport function depsCountsFromResult(result: AuditResult): {\n drifted: number;\n majorBehind: number;\n outdated: number | null;\n majorOutdated: number | null;\n} {\n if (result.audit !== \"deps\") {\n throw new Error(`Expected a 'deps' AuditResult, got '${result.audit}'`);\n }\n const details = (result.details ?? {}) as Partial<DepsDetails>;\n const entries = (details.entries ?? []) as DepsDriftEntry[];\n // \"drifted\" = drift !== \"same\", which intentionally includes \"newer\" (ahead of\n // baseline) — parity with the deps audit summary text.\n const drifted = entries.filter((e) => e.drift !== \"same\").length;\n const majorBehind = entries.filter((e) => e.drift === \"major\").length;\n // The real installed-version drift, distinct from declared-range drift. Null\n // when the audit couldn't determine it (no/stale lockfile) — kept null (not 0)\n // so the dashboard shows \"—\" rather than a misleading \"clean\".\n const outdated = details.outdated?.outdated ?? null;\n // How many of those outdated installs are a *major* behind the registry's\n // latest — surfaced separately from the total so the cockpit can show\n // \"N major\" available, distinct from the baseline-drift \"major\" count. Shares\n // the outdated signal's null-when-undetermined semantics.\n const majorOutdated = details.outdated?.major ?? null;\n return { drifted, majorBehind, outdated, majorOutdated };\n}\n","import type { AuditResult } from \"../types.js\";\nimport { normalizeSecurityAdvisory, type SecurityAdvisory } from \"../reports/airtable/websites.js\";\n\ntype SecurityDetails = {\n counts: { low: number; moderate: number; high: number; critical: number };\n advisories?: unknown;\n};\n\n/** True when a security AuditResult carries a counts object. Skipped runs\n * (no pnpm + no npm) have no details and return false. */\nexport function hasSecurityCounts(result: AuditResult): boolean {\n if (result.audit !== \"security\") return false;\n const details = result.details as SecurityDetails | undefined;\n return !!details && typeof details.counts === \"object\";\n}\n\nexport function securityCountsFromResult(result: AuditResult): {\n critical: number;\n high: number;\n moderate: number;\n low: number;\n} {\n if (result.audit !== \"security\") {\n throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as SecurityDetails | undefined;\n const c = details?.counts ?? { low: 0, moderate: 0, high: 0, critical: 0 };\n return { critical: c.critical, high: c.high, moderate: c.moderate, low: c.low };\n}\n\n/** The known advisories behind the counts, normalized for persistence. Empty when the\n * run was clean or carried no advisory detail (npm/pnpm sometimes omit it). Malformed\n * entries are dropped via the shared `normalizeSecurityAdvisory`. */\nexport function advisoriesFromResult(result: AuditResult): SecurityAdvisory[] {\n if (result.audit !== \"security\") {\n throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as SecurityDetails | undefined;\n const raw = details?.advisories;\n if (!Array.isArray(raw)) return [];\n return raw.map(normalizeSecurityAdvisory).filter((a): a is SecurityAdvisory => a !== null);\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { DomainResult } from \"../reports/airtable/websites.js\";\n\ntype DomainDetails = { resolved: boolean; certDaysRemaining: number | null; checkedAt: string };\n\n/** True when an AuditResult is a `domain` audit carrying a usable details payload (i.e. it\n * actually ran — a \"skip\" for a site with no deployed URL has no details). */\nexport function hasDomainResult(result: AuditResult): boolean {\n if (result.audit !== \"domain\") return false;\n const d = result.details as DomainDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable domain result (cert days + checked-at) from a `domain` audit. */\nexport function domainResultFromAudit(result: AuditResult): DomainResult {\n if (result.audit !== \"domain\") {\n throw new Error(`Expected a 'domain' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as DomainDetails | undefined;\n return {\n certDaysRemaining: typeof d?.certDaysRemaining === \"number\" ? d.certDaysRemaining : null,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { BrowserAuditFields } from \"../reports/airtable/websites.js\";\n\ntype BrowserDetails = {\n desktopOk: boolean;\n mobileOk: boolean;\n linksOk: boolean;\n brokenLinks: number;\n checkedAt: string;\n};\n\n/** True when an AuditResult is a `browser` audit that actually ran (has a details payload — a\n * \"skip\" for a site with no deployed URL has none). */\nexport function hasBrowserResult(result: AuditResult): boolean {\n if (result.audit !== \"browser\") return false;\n const d = result.details as BrowserDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable browser verdicts from a `browser` audit. */\nexport function browserFieldsFromAudit(result: AuditResult): BrowserAuditFields {\n if (result.audit !== \"browser\") {\n throw new Error(`Expected a 'browser' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as BrowserDetails | undefined;\n return {\n desktopOk: d?.desktopOk === true,\n mobileOk: d?.mobileOk === true,\n linksOk: d?.linksOk === true,\n brokenLinks: typeof d?.brokenLinks === \"number\" ? d.brokenLinks : 0,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { NetlifyDeployResult } from \"../reports/airtable/websites.js\";\n\ntype NetlifyDeployDetails = {\n state: string | null;\n deployedAt: string | null;\n logUrl: string | null;\n checkedAt: string;\n};\n\n/** True when an AuditResult is a `netlify-deploy` audit carrying a usable details\n * payload (i.e. it actually ran — a \"skip\" for a site with no Netlify id / no\n * token has no details). */\nexport function hasNetlifyDeployResult(result: AuditResult): boolean {\n if (result.audit !== \"netlify-deploy\") return false;\n const d = result.details as NetlifyDeployDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable deploy result (state + deployedAt + logUrl +\n * checkedAt) from a `netlify-deploy` audit. */\nexport function netlifyDeployResultFromAudit(result: AuditResult): NetlifyDeployResult {\n if (result.audit !== \"netlify-deploy\") {\n throw new Error(`Expected a 'netlify-deploy' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as NetlifyDeployDetails | undefined;\n return {\n state: typeof d?.state === \"string\" ? d.state : null,\n deployedAt: typeof d?.deployedAt === \"string\" ? d.deployedAt : null,\n logUrl: typeof d?.logUrl === \"string\" ? d.logUrl : null,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { type WebsiteRow, siteSlug, updateAuditFields } from \"../reports/airtable/websites.js\";\nimport type {\n A11yCounts,\n DepsCounts,\n SecurityCounts,\n SecurityAdvisory,\n DomainResult,\n BrowserAuditFields,\n NetlifyDeployResult,\n} from \"../reports/airtable/websites.js\";\nimport type { LighthouseScoreWriteback } from \"../reports/types.js\";\nimport { hasRealScores, lighthouseScoresFromResult } from \"./lighthouse-airtable.js\";\nimport { hasA11yCounts, a11yCountsFromResult } from \"./a11y-airtable.js\";\nimport { hasDepsCounts, depsCountsFromResult } from \"./deps-airtable.js\";\nimport {\n hasSecurityCounts,\n securityCountsFromResult,\n advisoriesFromResult,\n} from \"./security-airtable.js\";\nimport { hasDomainResult, domainResultFromAudit } from \"./domain-airtable.js\";\nimport { hasBrowserResult, browserFieldsFromAudit } from \"./browser-airtable.js\";\nimport { hasNetlifyDeployResult, netlifyDeployResultFromAudit } from \"./netlify-deploy-airtable.js\";\nimport { detectAuditEvents } from \"./fleet-event-detectors.js\";\nimport type { FleetEvent } from \"../db/fleet-events.js\";\n\ntype WriteSummary = {\n siteName: string;\n writes: Array<{\n audit:\n | \"lighthouse\"\n | \"a11y\"\n | \"deps\"\n | \"security\"\n | \"github-signals\"\n | \"domain\"\n | \"browser\"\n | \"netlify-deploy\";\n counts: object;\n }>;\n /** Fleet-activity events detected from this site's prior row vs the fresh audits.\n * Optional: only the fleet path records them; the single-site path ignores them. */\n events?: FleetEvent[];\n};\n\n/** Orchestrates the per-audit Airtable writes for `audit --write-airtable`.\n * Extracted from the CLI command so it can be unit-tested with a fake base\n * and so adding new audit types is a one-line addition here rather than\n * growing the CLI handler.\n *\n * Throws (with .exitCode set) on the failure modes the CLI surfaces today:\n * - 2: --only ran without lighthouse, or no Websites row matched the slug\n * - 1: lighthouse ran but produced no real scores (infrastructure failure).\n * The a11y/deps/security writes still complete FIRST — a Lighthouse\n * miss flags the site without discarding its other audit data.\n *\n * Precedence note: the Websites-row lookup (exitCode 2) is checked BEFORE the\n * no-scores gate (exitCode 1), so the rare no-row + no-scores combo surfaces\n * as exitCode 2. */\nexport async function writeAuditsToAirtable(args: {\n base: AirtableBase;\n websites: WebsiteRow[];\n slug: string;\n results: AuditResult[];\n}): Promise<WriteSummary> {\n const { base, websites, slug, results } = args;\n\n // Lighthouse is OPTIONAL. The checkout-free `--only lighthouse,domain,browser` nightly includes\n // it, but a standalone `--only security` (checkout-ful) sweep legitimately has none. We write\n // whatever audits produced values; only the lighthouse-MISS flag below is lighthouse-specific\n // (so a real Lighthouse infra failure still reds the run without discarding other audit data).\n const lhResult = results.find((r) => r.audit === \"lighthouse\");\n const target = websites.find((w) => siteSlug(w.name) === slug);\n if (!target) {\n throw Object.assign(new Error(`No Websites row matched slug \"${slug}\"`), { exitCode: 2 });\n }\n\n const writes: WriteSummary[\"writes\"] = [];\n const audits: {\n scores?: LighthouseScoreWriteback;\n a11y?: A11yCounts;\n deps?: DepsCounts;\n security?: SecurityCounts;\n securityAdvisories?: SecurityAdvisory[];\n domain?: DomainResult;\n browser?: BrowserAuditFields;\n netlifyDeploy?: NetlifyDeployResult;\n } = {};\n\n // Collect every audit that produced real values into ONE merged input, then do a\n // SINGLE atomic Airtable update (was: up to four sequential updates on the same\n // row — a mid-sequence failure left it half-written yet reported fully failed, at\n // 4× the request volume). Lighthouse is the most timeout-prone audit: a Lighthouse\n // miss must NOT discard the site's valid a11y/deps/security results (morning-brief\n // 2026-06-10 MEDIUM-E). So include Lighthouse scores only when real, include the\n // other audits whenever present, write all of them in one update, then — if\n // Lighthouse missed — throw AFTER that atomic write so the site is still flagged\n // (exitCode 1 / collected in FleetWriteResult.failed) without losing its other data.\n const lhHasScores = lhResult ? hasRealScores(lhResult) : false;\n if (lhResult && lhHasScores) {\n const scores = lighthouseScoresFromResult(lhResult);\n audits.scores = scores;\n writes.push({ audit: \"lighthouse\", counts: scores });\n }\n\n const a11y = results.find((r) => r.audit === \"a11y\");\n if (a11y && hasA11yCounts(a11y)) {\n const counts = a11yCountsFromResult(a11y);\n audits.a11y = counts;\n writes.push({ audit: \"a11y\", counts });\n }\n\n const deps = results.find((r) => r.audit === \"deps\");\n if (deps && hasDepsCounts(deps)) {\n const counts = depsCountsFromResult(deps);\n audits.deps = counts;\n writes.push({ audit: \"deps\", counts });\n }\n\n const sec = results.find((r) => r.audit === \"security\");\n if (sec && hasSecurityCounts(sec)) {\n const counts = securityCountsFromResult(sec);\n audits.security = counts;\n // Persist the advisory list alongside the counts so the dashboard can show which packages\n // are vulnerable. An empty array (clean run) clears any stale list.\n audits.securityAdvisories = advisoriesFromResult(sec);\n writes.push({ audit: \"security\", counts });\n }\n\n const dom = results.find((r) => r.audit === \"domain\");\n if (dom && hasDomainResult(dom)) {\n const result = domainResultFromAudit(dom);\n audits.domain = result;\n writes.push({ audit: \"domain\", counts: result });\n }\n\n const browser = results.find((r) => r.audit === \"browser\");\n if (browser && hasBrowserResult(browser)) {\n const fields = browserFieldsFromAudit(browser);\n audits.browser = fields;\n writes.push({ audit: \"browser\", counts: fields });\n }\n\n const netlifyDeploy = results.find((r) => r.audit === \"netlify-deploy\");\n if (netlifyDeploy && hasNetlifyDeployResult(netlifyDeploy)) {\n const result = netlifyDeployResultFromAudit(netlifyDeploy);\n audits.netlifyDeploy = result;\n writes.push({ audit: \"netlify-deploy\", counts: result });\n }\n\n // One atomic write of everything that ran. Skip the call only if there is nothing\n // to write at all (no real scores AND no other audit produced values) — an empty\n // update is a wasted request.\n if (Object.keys(audits).length > 0) {\n await updateAuditFields(base, target.id, audits);\n }\n\n // Detect fleet-activity transitions from the prior row (`target`, loaded before this\n // write) vs the fresh audits. Computed here where both are in hand; recorded by the\n // caller (fleet path) — single-site callers simply ignore `events`.\n const events = detectAuditEvents(\n target,\n {\n ...(audits.security !== undefined ? { security: audits.security } : {}),\n ...(audits.domain !== undefined ? { domain: audits.domain } : {}),\n },\n new Date().toISOString(),\n );\n\n // Lighthouse-miss flag: only when lighthouse WAS requested (in results) but produced no scores —\n // an infra failure worth reding the run, AFTER persisting the other audits. A sweep that never\n // ran lighthouse (e.g. `--only security`) skips this entirely.\n if (lhResult && !lhHasScores) {\n // Enumerate what WAS persisted so the failure (surfaced to the single-site\n // CLI operator via console.error) reads as a partial write, not a total one.\n const persisted = writes.map((w) => w.audit);\n throw Object.assign(\n new Error(\n `Lighthouse audit produced no scores; ${\n persisted.length ? `wrote ${persisted.join(\"/\")} but refused Lighthouse` : \"wrote nothing\"\n }. Summary: ${lhResult.summary}`,\n ),\n { exitCode: 1 },\n );\n }\n\n return { siteName: target.name, writes, events };\n}\n\nexport type FleetWriteResult = {\n written: WriteSummary[];\n failed: Array<{ slug: string; error: string }>;\n};\n\n/** Render the fleet write-back outcome for the CLI/CI. Beyond the human-readable\n * lines, it emits a single deterministic, machine-parseable line —\n * `FLEET_WRITE_SUMMARY wrote=N failed=M total=T` — that the nightly workflow\n * greps to decide pass/fail. Keying CI on this line (not the prose, and not a\n * \"wrote ≥ 1\" heuristic) lets the gate tolerate a single known flake while\n * still reding on a total or mass write-back failure. */\nexport function formatFleetWriteSummary(result: FleetWriteResult): string {\n const wrote = result.written.length;\n const failed = result.failed.length;\n const total = wrote + failed;\n let out = `→ wrote ${wrote} site(s) to Airtable`;\n if (failed > 0) {\n out += `\\n⚠ ${failed} site(s) not written: ${result.failed\n .map((f) => `${f.slug} (${f.error})`)\n .join(\"; \")}`;\n }\n out += `\\nFLEET_WRITE_SUMMARY wrote=${wrote} failed=${failed} total=${total}`;\n return out;\n}\n\n/** Write each site's pooled audit results back to its own Websites row,\n * best-effort. Results are grouped by `result.site` (the slug the fleet\n * inventory stamped as Site.name). A per-site failure (no scores, no matching\n * row) is collected — not thrown — so one bad site never aborts the batch. */\nexport async function writeFleetAuditsToAirtable(args: {\n base: AirtableBase;\n websites: WebsiteRow[];\n results: AuditResult[];\n}): Promise<FleetWriteResult> {\n const { base, websites, results } = args;\n\n const bySlug = new Map<string, AuditResult[]>();\n for (const r of results) {\n const arr = bySlug.get(r.site) ?? [];\n arr.push(r);\n bySlug.set(r.site, arr);\n }\n\n const written: WriteSummary[] = [];\n const failed: FleetWriteResult[\"failed\"] = [];\n // Serial on purpose: even at one (now atomic) update call per site, Airtable's\n // ~5 req/sec limit means a Promise.all fan-out across the fleet would burst and\n // trip 429s (silently filed as failures). Below a few dozen sites, serial trades\n // wall-clock for safety. (morning-brief 2026-06-09 MEDIUM-3.) Add a bounded pool\n // when the fleet grows.\n for (const [slug, siteResults] of bySlug) {\n try {\n written.push(await writeAuditsToAirtable({ base, websites, slug, results: siteResults }));\n } catch (e) {\n failed.push({ slug, error: (e as Error).message });\n }\n }\n return { written, failed };\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  siteSlug
3
- } from "./chunk-7RGQ4KLE.js";
3
+ } from "./chunk-CCXVX5JN.js";
4
4
 
5
5
  // src/audits/lighthouse-airtable.ts
6
6
  import { readFile } from "fs/promises";
@@ -48,4 +48,4 @@ export {
48
48
  lighthouseScoresFromResult,
49
49
  resolveSlugFromCwd
50
50
  };
51
- //# sourceMappingURL=chunk-CSTLFSGM.js.map
51
+ //# sourceMappingURL=chunk-N5MZ6WRX.js.map
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  fetchGaUsers,
3
3
  fetchSearch
4
- } from "./chunk-M5LX2VSR.js";
4
+ } from "./chunk-JWA4FO2J.js";
5
5
  import {
6
6
  announcementSiteExtras,
7
7
  resolveCopy
8
- } from "./chunk-U3MF6RCS.js";
8
+ } from "./chunk-SYTPVYIK.js";
9
9
  import {
10
10
  siteSlug
11
- } from "./chunk-7RGQ4KLE.js";
11
+ } from "./chunk-CCXVX5JN.js";
12
12
 
13
13
  // src/reports/report-data.ts
14
14
  var PREVIEW_WINDOW_DAYS = 30;
@@ -60,4 +60,4 @@ export {
60
60
  scoresFromRow,
61
61
  buildReportDataForSite
62
62
  };
63
- //# sourceMappingURL=chunk-OYN6H4CX.js.map
63
+ //# sourceMappingURL=chunk-PMIKXAPF.js.map
@@ -239,17 +239,12 @@ function testingChecklistSection(copy) {
239
239
  lastPaddingBottom: "60px"
240
240
  });
241
241
  }
242
- function maintenanceTestingPlaceholder(lastTested) {
242
+ function maintenanceTestingPlaceholder() {
243
243
  return `
244
244
  <mj-section background-color="#F4F4F4">
245
245
  <mj-column>
246
246
  <mj-image href="mailto:info@reddoorla.com" src="${BLURRED_TESTS}" />
247
247
  </mj-column>
248
- </mj-section>
249
- <mj-section background-color="#F4F4F4" padding-top="0px">
250
- <mj-column>
251
- <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">Last Tested: ${fmtDate(lastTested)}</mj-text>
252
- </mj-column>
253
248
  </mj-section>`;
254
249
  }
255
250
  function testingIntroSection(copy) {
@@ -323,14 +318,16 @@ function buildMjml(data) {
323
318
  background: "white",
324
319
  footnoteLines: [escapeXml2(copy.seoCta)]
325
320
  })}
326
- ${isTesting ? testingIntroSection(copy) + testingChecklistSection(copy) : maintenanceTestingPlaceholder(data.lastTestedDate)}
321
+ ${isTesting ? testingIntroSection(copy) + testingChecklistSection(copy) : maintenanceTestingPlaceholder()}
327
322
  ${data.commentary ? commentarySection(data.commentary, copy) : ""}
328
323
  <mj-section background-color="white">
329
324
  <mj-column padding-top="36px">
330
325
  <mj-text color="#C00" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" padding-top="36px" line-height="36px">Any questions, concerns or requests?</mj-text>
331
- ${copy.contact.map(
332
- (line, i) => i === copy.contact.length - 1 ? `<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" padding-top="0px" line-height="30px" padding-bottom="36px">${escapeXml2(line)}</mj-text>` : `<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
333
- ).join("\n ")}
326
+ ${copy.contact.map((line, i) => {
327
+ const isLast = i === copy.contact.length - 1;
328
+ const emphasis = i === 0 ? `color="#C00" font-family="helvetica, sans-serif" font-size="24px" font-weight="700"` : `font-family="helvetica, sans-serif" font-size="24px" font-weight="300"`;
329
+ return isLast ? `<mj-text ${emphasis} padding-top="0px" line-height="30px" padding-bottom="36px">${escapeXml2(line)}</mj-text>` : `<mj-text ${emphasis} line-height="30px">${escapeXml2(line)}</mj-text>`;
330
+ }).join("\n ")}
334
331
  <mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
335
332
  <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()} ${escapeXml2(copy.footerOrg)}. All rights reserved.</mj-text>
336
333
  <mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="700" line-height="16px" padding-top="0" padding-bottom="0px">Our mailing address is:</mj-text>
@@ -432,8 +429,9 @@ function buildAnnouncementMjml(data) {
432
429
  </mj-column>
433
430
  </mj-section>` : "";
434
431
  const contactRows = copy.contact.map(
435
- (line, i) => `
436
- <mj-text ${i === 0 ? "" : `color="${GREY2}" `}font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
432
+ (line, i) => i === 0 ? `
433
+ <mj-text color="${RED2}" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" line-height="30px">${escapeXml2(line)}</mj-text>` : `
434
+ <mj-text color="${GREY2}" font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
437
435
  ).join("");
438
436
  const footerAddressRows = copy.footerAddress.map(
439
437
  (line) => `
@@ -494,7 +492,8 @@ function buildLaunchMjml(data) {
494
492
  <mj-text color="${GREY3}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px" padding-top="4px" padding-bottom="4px">\u2022 ${escapeXml2(item)}</mj-text>`
495
493
  ).join("");
496
494
  const contactRows = copy.contact.map(
497
- (line) => `
495
+ (line, i) => i === 0 ? `
496
+ <mj-text color="${RED3}" font-family="helvetica, sans-serif" font-size="24px" font-weight="700" line-height="30px">${escapeXml2(line)}</mj-text>` : `
498
497
  <mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml2(line)}</mj-text>`
499
498
  ).join("");
500
499
  const footerAddressRows = copy.footerAddress.map(
@@ -601,4 +600,4 @@ export {
601
600
  fetchAttachmentBytes,
602
601
  uploadAttachment
603
602
  };
604
- //# sourceMappingURL=chunk-U3MF6RCS.js.map
603
+ //# sourceMappingURL=chunk-SYTPVYIK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/copy.ts","../src/reports/maintenance-email/assets/index.ts","../src/reports/email-sections.ts","../src/reports/maintenance-email/template.ts","../src/reports/announcement-email/template.ts","../src/reports/render.ts","../src/reports/launch-email/template.ts","../src/reports/airtable/attachments.ts"],"sourcesContent":["import type { WebsiteRow } from \"./airtable/websites.js\";\n\nexport type ResolvedCopy = {\n maintenanceIntro: string;\n maintenanceChecks: string[]; // 6; index 3 is the Google row's no-position default\n testingIntro: string;\n testingChecklist: string[]; // 7\n notesHeader: string;\n seoCta: string;\n contact: string[]; // closing invitation lines\n footerOrg: string;\n footerAddress: string[];\n launchHeading: string;\n launchBody: string;\n launchSetupItems: string[];\n announceHeading: string;\n announceBody: string;\n announceImprovementResend: string;\n announceImprovementSvelte5: string;\n /** The cadence lead-in, ending at the em dash. Rendered followed by `announceCadenceNote`\n * in italics. */\n announceCadence: string;\n /** The reassurance tail, rendered italic after `announceCadence`. */\n announceCadenceNote: string;\n announceOpenDoor: string;\n};\n\nexport const DEFAULT_COPY: ResolvedCopy = {\n maintenanceIntro:\n \"Includes checking the hosting, DNS, Content Management System (CMS, if applicable), search indexing and security of the site for major flaws and updating as necessary.\",\n maintenanceChecks: [\n \"Deploy & Function Health\",\n \"CMS Checked\",\n \"Domain, DNS & SSL\",\n \"Google Indexed\",\n \"Security Updates\",\n \"Uptime Checked\",\n ],\n testingIntro:\n \"Testing includes checks similar to those at launch: testing on common browsers and operating systems, at different screen sizes, and checking every function, and updating all packages for performance rather than just those needed for security.\",\n testingChecklist: [\n \"Desktop Browsers\",\n \"Mobile Browsers\",\n \"Page Titles & Meta\",\n \"Links & Navigation\",\n \"Form Functionality\",\n \"Interactions & Animations\",\n \"Tested After Updates\",\n ],\n notesHeader: \"NOTES\",\n seoCta: \"Contact us if you are interested in more in-depth data or have questions about SEO.\",\n contact: [\"Just hit reply.\", \"We're here to help in any way we can.\"],\n footerOrg: \"Reddoor Creative, LLC\",\n footerAddress: [\"29027 Dapper Dan\", \"Fair Oaks Ranch, TX 78015\"],\n launchHeading: \"LAUNCHED\",\n launchBody:\n \"Your site is live. We've set it up on the Reddoor stack with hosting, security, and automatic maintenance so it stays fast and healthy. Here's what's in place:\",\n launchSetupItems: [\n \"Hosting, DNS, and SSL configured\",\n \"Continuous integration + automatic dependency updates\",\n \"Analytics and uptime monitoring\",\n ],\n announceHeading: \"YOUR ONGOING SITE CARE\",\n announceBody:\n \"We've completed a full test of your site and set it up for ongoing care to keep it fast, secure, and healthy. Here's what you can expect from us going forward:\",\n announceImprovementResend:\n \"Your contact forms now deliver straight to your inbox through reliable infrastructure, so no inquiry slips through the cracks.\",\n announceImprovementSvelte5:\n \"We've modernized your site to the latest framework — it's faster, more secure, and built to last.\",\n announceCadence: \"After each one we'll send you a short report like this —\",\n announceCadenceNote: \"there's nothing you need to do.\",\n announceOpenDoor:\n \"And if you'd ever like to expand the scope, add features, or freshen anything up, just let us know.\",\n};\n\n/** Trim an override to null when blank (mirrors the trim-to-null handling). */\nfunction override(v: string | null): string | null {\n if (typeof v !== \"string\") return null;\n const t = v.trim();\n return t.length > 0 ? t : null;\n}\n\n/**\n * Resolve a site's effective copy: DEFAULT_COPY with the three per-site narrative\n * overrides applied. Only maintenanceIntro/contact/footer are per-site (M6a §2);\n * everything else is the shared default. PURE.\n */\n/** Split an operator override into lines: tolerate CRLF, drop blank lines (a stray\n * blank in the Airtable cell shouldn't render an empty address row). */\nfunction splitLines(s: string): string[] {\n return s.split(/\\r?\\n/).filter((l) => l.trim().length > 0);\n}\n\nexport function resolveCopy(site: WebsiteRow): ResolvedCopy {\n const intro = override(site.copyIntro);\n const contact = override(site.copyContact);\n const footer = override(site.copyFooter);\n const footerLines = footer ? splitLines(footer) : null;\n return {\n ...DEFAULT_COPY,\n maintenanceIntro: intro ?? DEFAULT_COPY.maintenanceIntro,\n contact: contact ? splitLines(contact) : DEFAULT_COPY.contact,\n footerOrg: footerLines?.[0] ?? DEFAULT_COPY.footerOrg,\n footerAddress: footerLines ? footerLines.slice(1) : DEFAULT_COPY.footerAddress,\n };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const CHECK_CID = \"rd-check-png\";\nexport const BLURRED_CID = \"rd-blurred-tests-jpg\";\n\nexport type BundledImage = {\n bytes: Uint8Array;\n contentType: string;\n cid: string;\n filename: string;\n};\n\n// Walk up from the current module's URL looking for the assets dir in either\n// the dev layout (src/reports/maintenance-email/assets/) or the published\n// layout (dist/reports/maintenance-email/assets/). REQUIRED because tsup\n// inlines this module into dist/cli/bin.js — so `import.meta.url`-based\n// sibling resolution looks in dist/cli/ for the PNGs and fails with ENOENT.\n// Regression that shipped in 0.10.0–0.10.1; tests passed in dev because\n// vitest evaluates the source file where import.meta.url is already correct.\nlet cachedAssetsDir: string | null = null;\nfunction resolveAssetsDir(): string {\n if (cachedAssetsDir) return cachedAssetsDir;\n let dir = dirname(fileURLToPath(import.meta.url));\n while (true) {\n // Source layout preferred — single source of truth in the workspace\n // and the only one present in dev/test environments.\n const srcCandidate = join(dir, \"src\", \"reports\", \"maintenance-email\", \"assets\", \"check.png\");\n if (existsSync(srcCandidate)) {\n cachedAssetsDir = dirname(srcCandidate);\n return cachedAssetsDir;\n }\n // Published layout — only `dist/` ships per package.json#files, so\n // consumers fall through to here.\n const distCandidate = join(dir, \"dist\", \"reports\", \"maintenance-email\", \"assets\", \"check.png\");\n if (existsSync(distCandidate)) {\n cachedAssetsDir = dirname(distCandidate);\n return cachedAssetsDir;\n }\n const parent = dirname(dir);\n if (parent === dir) {\n throw new Error(\n `loadBundledImages: could not locate maintenance-email assets dir by walking up from ${fileURLToPath(import.meta.url)}. Checked both src/ and dist/ layouts.`,\n );\n }\n dir = parent;\n }\n}\n\n/**\n * Read the bundled image bytes from disk. Both Maintenance and Testing\n * variants reference `check.png`; only the Maintenance variant references\n * `blurredTests.jpg`.\n */\nexport async function loadBundledImages(): Promise<{\n check: BundledImage;\n blurred: BundledImage;\n}> {\n const assetsDir = resolveAssetsDir();\n const [check, blurred] = await Promise.all([\n readFile(join(assetsDir, \"check.png\")),\n readFile(join(assetsDir, \"blurredTests.jpg\")),\n ]);\n return {\n check: {\n bytes: new Uint8Array(check),\n contentType: \"image/png\",\n cid: CHECK_CID,\n filename: \"check.png\",\n },\n blurred: {\n bytes: new Uint8Array(blurred),\n contentType: \"image/jpeg\",\n cid: BLURRED_CID,\n filename: \"blurredTests.jpg\",\n },\n };\n}\n","import { escapeHtml } from \"../util/html.js\";\nimport { CHECK_CID } from \"./maintenance-email/assets/index.js\";\nimport type { ReportData } from \"./types.js\";\n\n/**\n * Shared MJML section builders for the report family of emails (maintenance/testing report,\n * announcement). Centralizing them here guarantees the announcement renders the SAME polished\n * components as the monthly report — the checklist rows, the full Lighthouse block, and the\n * analytics block — so the two can't drift in design. PURE string builders; no I/O.\n *\n * Escaping: callers pass already-trusted copy for fixed labels, but any site/operator string\n * (check labels, trailing notes) is escaped here via `escapeXml`.\n */\nexport const escapeXml = escapeHtml;\n\nconst RED = \"#C00\";\nconst GREY = \"#757575\";\nconst BORDER = \"#CCCCCC\";\nconst TREND_UP = \"#2E7D32\"; // positive green — growth reads as good\nconst TREND_NEUTRAL = GREY; // muted grey — dips/flat aren't failures (brand red is reserved)\n\n// The report's bundled green check (cid:rd-check-png), attached inline by orchestrate.ts at\n// send time. Standalone previews (no attachments) show the image's alt instead.\nconst CHECK_PNG = `cid:${CHECK_CID}`;\n\n/** Thousands-grouped user/visitor count. */\nexport function fmtUsers(n: number): string {\n return n.toLocaleString(\"en-US\");\n}\n\n/**\n * A checklist \"table\": one ruled row per label, the label on the left and the green check\n * right-aligned, matching the monthly report. `background` tints the band (white for the\n * maintenance list, #F4F4F4 for the testing list); `lastPaddingBottom` is the trailing gap\n * under the final row. Labels are escaped. PURE.\n */\nexport function checklistRowsSection(\n rows: string[],\n opts: { background: string; lastPaddingBottom: string },\n): string {\n return rows\n .map((label, i) => {\n const isLast = i === rows.length - 1;\n const border = isLast ? \"\" : ` border-bottom=\"solid ${BORDER} 1px\"`;\n const lastPad = isLast ? ` padding-bottom=\"${opts.lastPaddingBottom}\"` : \"\";\n return `\n <mj-section background-color=\"${opts.background}\" padding=\"0px\"${lastPad}>\n <mj-group>\n <mj-column padding-left=\"0px\" width=\"90%\"${border}>\n <mj-text height=\"25px\" padding-left=\"0px\" color=\"${GREY}\" padding-top=\"20px\" padding-bottom=\"7.5px\" font-size=\"16px\">${escapeXml(label)}</mj-text>\n </mj-column>\n <mj-column width=\"10%\"${border} padding-top=\"15px\">\n <mj-image align=\"right\" padding-right=\"0px\" width=\"20px\" height=\"20px\" padding-top=\"2.5px\" padding-bottom=\"15px\" src=\"${CHECK_PNG}\" />\n </mj-column>\n </mj-group>\n </mj-section>`;\n })\n .join(\"\");\n}\n\n/** The four Lighthouse scores with their client-facing labels and acceptable/ideal bands.\n * Ideal always tops at 100 (the metric's ceiling). */\nconst LIGHTHOUSE_ROWS: ReadonlyArray<{\n label: string;\n key: keyof ReportData[\"lighthouse\"];\n range: string;\n}> = [\n { label: \"Performance\", key: \"performance\", range: \"Acceptable 50–89 // Ideal 90–100\" },\n { label: \"Readability (A11y)\", key: \"accessibility\", range: \"Acceptable 80–99 // Ideal 100\" },\n { label: \"Best Practices\", key: \"bestPractices\", range: \"Acceptable 60–79 // Ideal 80–100\" },\n { label: \"Site Structure\", key: \"seo\", range: \"Acceptable 50–89 // Ideal 90–100\" },\n];\n\n/**\n * The full \"LIGHTHOUSE SCORES*\" block: each score as a big red number under its label with the\n * acceptable/ideal band beneath, ruled between scores, closed by the explanatory footnote.\n * `background` tints the section (default #F4F4F4, matching the report). When `pad` is given the\n * band's top/bottom padding moves to the section (symmetric bands for the announcement); omitting\n * it keeps the report's original inner paddings. PURE.\n */\nexport function lighthouseScoresSection(\n lighthouse: ReportData[\"lighthouse\"],\n opts: { background?: string; pad?: string } = {},\n): string {\n const background = opts.background ?? \"#F4F4F4\";\n const sectionPad = opts.pad ? ` padding-top=\"${opts.pad}\" padding-bottom=\"${opts.pad}\"` : \"\";\n const labelTop = opts.pad ?? \"55px\";\n const footnoteBottom = opts.pad ? \"0px\" : \"36px\";\n const rows = LIGHTHOUSE_ROWS.map(\n ({ label, key, range }, i) => `\n <mj-text color=\"${RED}\" font-size=\"20px\" font-weight=\"300\" padding-top=\"25px\">${label}</mj-text>\n <mj-text color=\"${RED}\" font-size=\"44px\" font-weight=\"400\" padding-top=\"0px\">${lighthouse[key]}</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"0px\" padding-bottom=\"36px\">${range}</mj-text>${\n i < LIGHTHOUSE_ROWS.length - 1\n ? `\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"${BORDER}\" padding=\"0\" />`\n : \"\"\n }`,\n ).join(\"\");\n return `\n <mj-section background-color=\"${background}\"${sectionPad}>\n <mj-column>\n <mj-text color=\"${RED}\" font-size=\"20px\" font-weight=\"700\" padding-top=\"${labelTop}\">LIGHTHOUSE SCORES*</mj-text>${rows}\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" padding-bottom=\"${footnoteBottom}\" line-height=\"20px\">*A Lighthouse score is a numerical measure provided by <a href=\"https://developer.chrome.com/docs/lighthouse/overview\" style=\"color:${RED}; text-decoration:underline;\">Google's Lighthouse tool</a>, which evaluates various aspects of a web page's quality.</mj-text>\n </mj-column>\n </mj-section>`;\n}\n\n/** The line under \"{N} Users\": a directional trend vs the previous period when both numbers\n * are real, else a graceful fallback. `undefined` = GA unavailable (distinct from a real 0).\n * Up = green; down/flat = muted grey (a traffic dip isn't a failure). PURE. */\nexport function analyticsTrendLine(\n cur: number | undefined,\n prev: number | undefined,\n periodDays?: number,\n): string {\n // The prior window the trend compares against: a concrete \"the previous N days\" when the\n // caller knows the window length, else the generic \"last period\" (keeps the label honest for\n // callers — and tests — that don't supply it).\n const priorLabel =\n periodDays && periodDays > 0 ? `the previous ${periodDays} days` : \"last period\";\n if (cur === undefined || prev === undefined) {\n return trendLine(TREND_NEUTRAL, `Last Period: ${prev !== undefined ? fmtUsers(prev) : \"—\"}`);\n }\n if (prev === 0) {\n return cur > 0\n ? trendLine(TREND_UP, \"▲ New this period (0 last period)\")\n : trendLine(TREND_NEUTRAL, \"Last Period: 0\");\n }\n const pct = Math.round(((cur - prev) / prev) * 100);\n const range = `(${fmtUsers(prev)} → ${fmtUsers(cur)})`;\n if (pct > 0) return trendLine(TREND_UP, `▲ ${pct}% vs ${priorLabel} ${range}`);\n if (pct < 0) return trendLine(TREND_NEUTRAL, `▼ ${Math.abs(pct)}% vs ${priorLabel} ${range}`);\n return trendLine(TREND_NEUTRAL, `No change vs ${priorLabel} (${fmtUsers(prev)})`);\n}\n\n/** A 16px coloured line (the trend, and the announcement's search line). */\nfunction trendLine(color: string, text: string): string {\n return `<mj-text color=\"${color}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${text}</mj-text>`;\n}\n\n/** A 12px muted footnote line (the SEO call-to-action / Lighthouse note style). */\nfunction footnoteLine(text: string): string {\n return `<mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" padding-bottom=\"36px\" line-height=\"20px\">${text}</mj-text>`;\n}\n\n/** True when the analytics block has at least one real datum to show — a GA user count or a body\n * line (e.g. the announcement's page-1 search callout). The static SEO call-to-action footnote\n * alone does NOT qualify: a block with no data is hidden rather than rendered empty. PURE. */\nexport function hasAnalyticsData(opts: {\n current?: number | undefined;\n bodyLines?: string[];\n}): boolean {\n return opts.current !== undefined || (opts.bodyLines?.length ?? 0) > 0;\n}\n\n/**\n * The \"ANALYTICS\" block: a big red user count, the trend line, then any 16px `bodyLines`\n * (e.g. the announcement's Google-position line) and 12px muted `footnoteLines` (e.g. the\n * report's SEO call-to-action). Callers pass already-escaped line text. PURE.\n *\n * Returns \"\" when there's no data ({@link hasAnalyticsData} false) — an empty \"— Users\" block\n * reads as broken, so the section (and its data-contextual SEO footnote) is omitted. When a body\n * line exists but no GA count (a GA-less site still ranking on page 1), the user count + trend\n * are suppressed and only the body shows under the label.\n */\nexport function analyticsSection(opts: {\n current?: number | undefined;\n previous?: number | undefined;\n /** Length in days of the current window; drives the trend's \"vs the previous N days\" label. */\n periodDays?: number | undefined;\n background: string;\n bodyLines?: string[];\n footnoteLines?: string[];\n pad?: string;\n}): string {\n if (!hasAnalyticsData(opts)) return \"\";\n const body = (opts.bodyLines ?? []).map((l) => trendLine(TREND_NEUTRAL, l)).join(\"\\n \");\n const footnotes = (opts.footnoteLines ?? []).map(footnoteLine).join(\"\\n \");\n const sectionPad = opts.pad ? ` padding-top=\"${opts.pad}\" padding-bottom=\"${opts.pad}\"` : \"\";\n const labelTop = opts.pad ?? \"75px\";\n const usersBlock =\n opts.current !== undefined\n ? `\n <mj-text color=\"${RED}\" font-size=\"44px\" font-weight=\"400\">${fmtUsers(opts.current)} Users</mj-text>\n ${analyticsTrendLine(opts.current, opts.previous, opts.periodDays)}`\n : \"\";\n return `\n <mj-section background-color=\"${opts.background}\"${sectionPad}>\n <mj-column>\n <mj-text color=\"${RED}\" font-size=\"20px\" font-weight=\"700\" padding-top=\"${labelTop}\">ANALYTICS</mj-text>${usersBlock}\n ${body}\n ${footnotes}\n </mj-column>\n </mj-section>`;\n}\n","import type { ReportData } from \"../types.js\";\nimport { DEFAULT_COPY, type ResolvedCopy } from \"../copy.js\";\nimport { BLURRED_CID } from \"./assets/index.js\";\nimport {\n checklistRowsSection,\n lighthouseScoresSection,\n analyticsSection,\n} from \"../email-sections.js\";\nimport { escapeHtml } from \"../../util/html.js\";\nimport { isHttpUrl } from \"../../util/url.js\";\n\n/**\n * Escape operator/site-controlled strings before interpolating into the MJML markup.\n * MJML parses as XML with `validationLevel: \"strict\"`. Under mjml@4.18 a raw `&`, `<`,\n * or `>` does NOT throw — it passes straight through into the rendered output, so an\n * unescaped value (e.g. a site name \"Brown & Co\", a URL, or commentary) silently\n * injects HTML/markup into the email. A raw `\"` inside an ATTRIBUTE value (e.g. the\n * image `href`/`alt`) is the one that throws — it terminates the attribute and trips a\n * parse error that blocks the send. So we escape for two reasons: prevent\n * HTML/markup injection in text, and prevent the attribute-quote parse error. Apply\n * to every interpolation of siteName / siteUrl / commentary / copy.\n *\n * This IS `src/util/html.ts`'s `escapeHtml` (the strict-XML set is identical),\n * re-exported under the name the email templates import (the launch template imports\n * `escapeXml` from here).\n */\nexport const escapeXml = escapeHtml;\n\n// Bundled images: shipped in dist/ via tsup onSuccess copy, attached inline via\n// CID by orchestrate.ts at send time. No external CDN dependency. (The green check\n// image lives in the shared email-sections checklist component.)\nconst BLURRED_TESTS = `cid:${BLURRED_CID}`;\n\nexport function fmtDate(d: Date | null): string {\n // Guard BOTH null AND an Invalid Date — `new Date(\"not-a-date\")` (a malformed\n // Airtable date string) is a truthy Date whose getUTC* accessors all return\n // NaN, which would render \"NaN.NaN.NaN\" into a real client email. `!d` alone\n // misses it; `Number.isNaN(d.getTime())` catches it.\n if (!d || Number.isNaN(d.getTime())) return \"\";\n // Airtable date fields are wall-clock YYYY-MM-DD strings parsed as UTC midnight.\n // Use UTC accessors so the rendered date matches what the operator entered.\n // US format: MM.DD.YYYY (Reddoor is Texas-based, clients are US).\n const mm = String(d.getUTCMonth() + 1).padStart(2, \"0\");\n const dd = String(d.getUTCDate()).padStart(2, \"0\");\n const yyyy = d.getUTCFullYear();\n return `${mm}.${dd}.${yyyy}`;\n}\n\nfunction maintenanceChecksSection(copy: ResolvedCopy, searchPosition?: number): string {\n // The Google row shows the live search position when available, else the plain label.\n const googleLabel =\n searchPosition !== undefined\n ? `Page 1 Google Result (#${searchPosition})`\n : (copy.maintenanceChecks[3] ?? \"\");\n const rows = copy.maintenanceChecks.map((label, i) => (i === 3 ? googleLabel : label));\n return checklistRowsSection(rows, { background: \"white\", lastPaddingBottom: \"36px\" });\n}\n\nfunction testingChecklistSection(copy: ResolvedCopy): string {\n return checklistRowsSection(copy.testingChecklist, {\n background: \"#F4F4F4\",\n lastPaddingBottom: \"60px\",\n });\n}\n\nfunction maintenanceTestingPlaceholder(): string {\n return `\n <mj-section background-color=\"#F4F4F4\">\n <mj-column>\n <mj-image href=\"mailto:info@reddoorla.com\" src=\"${BLURRED_TESTS}\" />\n </mj-column>\n </mj-section>`;\n}\n\nfunction testingIntroSection(copy: ResolvedCopy): string {\n return `\n <mj-section background-color=\"#F4F4F4\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">TESTING</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${escapeXml(copy.testingIntro)}</mj-text>\n </mj-column>\n </mj-section>`;\n}\n\nfunction commentarySection(text: string, copy: ResolvedCopy): string {\n return `\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"55px\">${escapeXml(copy.notesHeader)}</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${escapeXml(text).replace(/\\r\\n?|\\n/g, \"<br/>\")}</mj-text>\n </mj-column>\n </mj-section>`;\n}\n\nfunction hasHeaderDims(\n data: ReportData,\n): data is ReportData & { headerWidth: number; headerHeight: number; headerBgColor: string } {\n return Boolean(data.headerWidth && data.headerHeight && data.headerBgColor);\n}\n\nexport function headerImageTag(data: ReportData): string {\n const src = `cid:${data.headerImageCid}`;\n const alt = `${escapeXml(data.siteName)} maintenance report`;\n // escapeXml only escapes markup chars — it does NOT neutralize a dangerous URL\n // scheme. A `javascript:`/`data:` siteUrl would survive escaping and become a live\n // header href. Gate on isHttpUrl (the same http(s) allowlist the audit path uses)\n // and DROP a non-http(s) href entirely (fall back to \"#\") rather than linking it.\n const href = isHttpUrl(data.siteUrl) ? escapeXml(data.siteUrl) : \"#\";\n // Reserve the box and show a matched placeholder while the image loads / if blocked.\n // Critically, we do NOT set an mj-image `height` — MJML would emit `height:<px>` while\n // keeping `width:100%`, locking the height while the width scales and distorting the\n // image at any rendered width != the design width (mobile, narrow panes). Instead the\n // image stays `height:auto` (proportional) and the box is reserved via `aspect-ratio`\n // in the head <mj-style> below (see headerStyleBlock). `container-background-color` is\n // the placeholder; the bare fallback (no dims, e.g. local preview) keeps today's behavior.\n if (hasHeaderDims(data)) {\n return `<mj-image href=\"${href}\" src=\"${src}\" alt=\"${alt}\" width=\"${data.headerWidth}px\" css-class=\"rd-header\" container-background-color=\"${data.headerBgColor}\" />`;\n }\n return `<mj-image href=\"${href}\" src=\"${src}\" alt=\"${alt}\" />`;\n}\n\nexport function headerStyleBlock(data: ReportData): string {\n if (!hasHeaderDims(data)) return \"\";\n // Reserve the header's vertical space by aspect ratio so it scales proportionally with\n // its fluid (width:100%) width — no fixed pixel height, so it never squishes.\n // `height:auto !important` defends against any client honoring MJML's inline height.\n return `<mj-style>.rd-header img { height: auto !important; aspect-ratio: ${data.headerWidth} / ${data.headerHeight}; }</mj-style>`;\n}\n\nexport function buildMjml(data: ReportData): string {\n const copy = data.copy ?? DEFAULT_COPY;\n const isTesting = data.reportType === \"Testing\";\n const previewText = `Checked up on ${escapeXml(data.siteName)}`;\n\n return `<mjml>\n <mj-head>\n <mj-attributes>\n <mj-text font-family=\"helvetica, sans-serif\" padding-left=\"5px\" padding-right=\"5px\" />\n <mj-section padding-left=\"11%\" padding-right=\"11%\"/>\n <mj-image padding=\"0px\" />\n </mj-attributes>\n <mj-preview>${previewText}</mj-preview>\n ${headerStyleBlock(data)}\n </mj-head>\n <mj-body background-color=\"white\">\n <mj-section background-color=\"#F4F4F4\" padding-top=\"0px\" padding-bottom=\"0px\" padding-left=\"0px\" padding-right=\"0px\">\n <mj-column>\n ${headerImageTag(data)}\n </mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">COMPLETED ON</mj-text>\n <mj-text color=\"#C00\" font-size=\"44px\" font-weight=\"400\">${fmtDate(data.completedOn)}</mj-text>\n <mj-text color=\"#C00\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">MAINTENANCE CHECKS</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\">${escapeXml(copy.maintenanceIntro)}</mj-text>\n </mj-column>\n </mj-section>\n ${maintenanceChecksSection(copy, data.searchPosition)}\n ${lighthouseScoresSection(data.lighthouse)}\n ${analyticsSection({\n current: data.gaUsersCurrent,\n previous: data.gaUsersPrevious,\n periodDays: data.gaPeriodDays,\n background: \"white\",\n footnoteLines: [escapeXml(copy.seoCta)],\n })}\n ${isTesting ? testingIntroSection(copy) + testingChecklistSection(copy) : maintenanceTestingPlaceholder()}\n ${data.commentary ? commentarySection(data.commentary, copy) : \"\"}\n <mj-section background-color=\"white\">\n <mj-column padding-top=\"36px\">\n <mj-text color=\"#C00\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"700\" padding-top=\"36px\" line-height=\"36px\">Any questions, concerns or requests?</mj-text>\n ${copy.contact\n .map((line, i) => {\n // First line (\"Just hit reply.\") renders as a red bold heading, matching the\n // \"Any questions, concerns or requests?\" title above it; the last line keeps\n // its closing padding.\n const isLast = i === copy.contact.length - 1;\n const emphasis =\n i === 0\n ? `color=\"#C00\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"700\"`\n : `font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"300\"`;\n return isLast\n ? `<mj-text ${emphasis} padding-top=\"0px\" line-height=\"30px\" padding-bottom=\"36px\">${escapeXml(line)}</mj-text>`\n : `<mj-text ${emphasis} line-height=\"30px\">${escapeXml(line)}</mj-text>`;\n })\n .join(\"\\n \")}\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" line-height=\"20px\" font-style=\"italic\">Copyright ${new Date().getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>\n <mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"700\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">Our mailing address is:</mj-text>\n ${[copy.footerOrg, ...copy.footerAddress]\n .map(\n (line) =>\n `<mj-text color=\"#757575\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\\n \")}\n </mj-column>\n </mj-section>\n </mj-body>\n</mjml>`;\n}\n","import type { ReportData, ReportFrequency } from \"../types.js\";\nimport type { WebsiteRow } from \"../airtable/websites.js\";\nimport { DEFAULT_COPY, type ResolvedCopy } from \"../copy.js\";\nimport { escapeXml, headerImageTag, headerStyleBlock } from \"../maintenance-email/template.js\";\nimport {\n checklistRowsSection,\n lighthouseScoresSection,\n analyticsSection,\n hasAnalyticsData,\n} from \"../email-sections.js\";\n\n/** Frequency → client-facing phrase. \"None\" is never rendered (the line is omitted). */\nconst FREQ_PHRASE: Record<Exclude<ReportFrequency, \"None\">, string> = {\n Monthly: \"every month\",\n Quarterly: \"every quarter\",\n Yearly: \"every year\",\n};\n\nconst RED = \"#C00\";\nconst GREY = \"#757575\";\n\n// Equal top/bottom padding for every alternating-background band, applied at the mj-section\n// level so each colored band has symmetric breathing room. (Starting baseline — easy to tune.)\nconst SECTION_PAD = \"40px\";\n\n/** A red all-caps section label. Top spacing comes from the section padding, so this is flush. */\nfunction sectionLabel(text: string): string {\n return `<mj-text color=\"${RED}\" font-size=\"20px\" font-weight=\"700\" padding-top=\"0px\">${escapeXml(text)}</mj-text>`;\n}\n\n/** A grey 16px body paragraph whose caller passes already-escaped HTML, so partial inline markup\n * (e.g. the cadence line's italic <em>…</em> tail) survives. */\nfunction bodyLineHtml(html: string, paddingTop = \"8px\"): string {\n return `<mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\" padding-top=\"${paddingTop}\">${html}</mj-text>`;\n}\n\n/** A grey 16px body paragraph, matching the report's section intros. Escapes `text`. */\nfunction bodyLine(text: string, paddingTop = \"8px\"): string {\n return bodyLineHtml(escapeXml(text), paddingTop);\n}\n\n/** The go-forward cadence sentence as HTML: the lead-in followed by its reassurance tail in\n * italics (\"…short report like this — <em>there's nothing you need to do.</em>\"). Both escaped. */\nfunction cadenceHtml(copy: ResolvedCopy): string {\n return `${escapeXml(copy.announceCadence)} <em>${escapeXml(copy.announceCadenceNote)}</em>`;\n}\n\n/**\n * The announcement-only ReportData extras derived from the Websites row: the go-forward\n * cadence and the default-on improvement callouts. Used by BOTH the draft (announce recipe)\n * and the send-time re-render (orchestrate) so the sent email matches the reviewed preview —\n * `renderReportHtml` in the send path otherwise omits these, dropping the cadence + improvements.\n */\nexport function announcementSiteExtras(\n site: WebsiteRow,\n): Pick<ReportData, \"cadence\" | \"improvements\"> {\n return {\n cadence: { maintenance: site.maintenanceFreq, testing: site.testingFreq },\n improvements: { resendForms: true, svelte5: true },\n };\n}\n\n/**\n * One-time onboarding announcement, built from the SAME components as the monthly report so it\n * reads as a testing report with extra explanation: header · intro (\"your ongoing care\") ·\n * MAINTENANCE CHECKS · TESTING (each: intro with the cadence baked into the copy + the report's\n * checklist rows) · LIGHTHOUSE SCORES · ANALYTICS (users + trend + the Google-position line) ·\n * RECENT IMPROVEMENTS (conditional; closes with the open-door invitation). Reuses the M6a copy\n * layer (contact/footer honor per-site overrides). No pricing. A pace set to None omits its\n * checklist section. Every band carries equal top/bottom padding (SECTION_PAD).\n */\nexport function buildAnnouncementMjml(data: ReportData): string {\n const copy = data.copy ?? DEFAULT_COPY;\n const previewText = \"Your monthly report from Reddoor\";\n const cad = data.cadence;\n\n const hasMaint = Boolean(cad && cad.maintenance !== \"None\");\n const hasTesting = Boolean(cad && cad.testing !== \"None\");\n const improvementItems: string[] = [];\n if (data.improvements?.resendForms) improvementItems.push(copy.announceImprovementResend);\n if (data.improvements?.svelte5) improvementItems.push(copy.announceImprovementSvelte5);\n const hasImpr = improvementItems.length > 0;\n\n // ANALYTICS renders only with real data — a GA user count or a page-1 search callout (the latter\n // keeps the block alive for a GA-less site that still ranks). Computed up here so the band\n // counter below can SKIP its background slot when analytics is hidden; otherwise a consumed-but-\n // invisible slot would push the next band to the wrong color and two same-color bands would abut.\n const analyticsBodyLines =\n data.searchPosition !== undefined\n ? [`Page 1 Google result (#${data.searchPosition}) for your brand search`]\n : [];\n const hasAnalytics = hasAnalyticsData({\n current: data.gaUsersCurrent,\n bodyLines: analyticsBodyLines,\n });\n\n // Alternating band backgrounds assigned in render order, so the white/#F4F4F4 pattern holds\n // no matter which optional sections (maintenance / testing / improvements) actually render —\n // a counter that only advances for bands that appear avoids two same-colored bands abutting.\n const BANDS = [\"white\", \"#F4F4F4\"] as const;\n let bandN = 0;\n const nextBg = (): string => BANDS[bandN++ % 2]!;\n const introBg = nextBg();\n const maintBg = hasMaint ? nextBg() : \"\";\n const testBg = hasTesting ? nextBg() : \"\";\n const lighthouseBg = nextBg();\n const analyticsBg = hasAnalytics ? nextBg() : \"\";\n const improvementsBg = hasImpr ? nextBg() : \"\";\n const contactBg = nextBg();\n\n // MAINTENANCE CHECKS (first) — intro with the maintenance cadence baked into the copy, then the\n // report's checklist rows. The report-frequency reassurance (announceCadence) trails the LAST\n // check block, so it sits here only when there's no testing block after it. Omitted when None.\n const maintenanceSection =\n cad && cad.maintenance !== \"None\"\n ? `\n <mj-section background-color=\"${maintBg}\" padding-top=\"${SECTION_PAD}\" padding-bottom=\"0px\">\n <mj-column>\n ${sectionLabel(\"MAINTENANCE CHECKS\")}\n ${bodyLineHtml(`${escapeXml(`${copy.maintenanceIntro} We do this ${FREQ_PHRASE[cad.maintenance]}.`)}${cad.testing === \"None\" ? ` ${cadenceHtml(copy)}` : \"\"}`)}\n </mj-column>\n </mj-section>${checklistRowsSection(copy.maintenanceChecks, {\n background: maintBg,\n lastPaddingBottom: SECTION_PAD,\n })}`\n : \"\";\n\n // TESTING (second) — intro with the testing cadence + the report-frequency note baked in, then\n // the report's checklist rows. Omitted when None.\n const testingSection =\n cad && cad.testing !== \"None\"\n ? `\n <mj-section background-color=\"${testBg}\" padding-top=\"${SECTION_PAD}\" padding-bottom=\"0px\">\n <mj-column>\n ${sectionLabel(\"TESTING\")}\n ${bodyLineHtml(`${escapeXml(`${copy.testingIntro} We run a full test ${FREQ_PHRASE[cad.testing]}.`)} ${cadenceHtml(copy)}`)}\n </mj-column>\n </mj-section>${checklistRowsSection(copy.testingChecklist, {\n background: testBg,\n lastPaddingBottom: SECTION_PAD,\n })}`\n : \"\";\n\n // ANALYTICS — the report's big user count + trend, plus the Google search-position line. Hidden\n // entirely when there's no traffic or search data (hasAnalytics false), so it never shows empty.\n const analytics = hasAnalytics\n ? analyticsSection({\n current: data.gaUsersCurrent,\n previous: data.gaUsersPrevious,\n periodDays: data.gaPeriodDays,\n background: analyticsBg,\n pad: SECTION_PAD,\n bodyLines: analyticsBodyLines,\n })\n : \"\";\n\n // RECENT IMPROVEMENTS — the toggled callouts, closed by the open-door invitation. Omitted\n // entirely when there are no improvements (the open-door rides along with this block).\n const improvementsSection = hasImpr\n ? `\n <mj-section background-color=\"${improvementsBg}\" padding-top=\"${SECTION_PAD}\" padding-bottom=\"${SECTION_PAD}\">\n <mj-column>\n ${sectionLabel(\"RECENT IMPROVEMENTS\")}\n ${improvementItems.map((item) => bodyLine(item)).join(\"\\n \")}\n ${bodyLine(copy.announceOpenDoor, \"16px\")}\n </mj-column>\n </mj-section>`\n : \"\";\n\n // First contact line (\"Just hit reply.\") renders as a red bold heading, matching the\n // \"Questions, concerns or requests?\" title above it; any following lines (e.g. \"We're\n // here to help in any way we can.\") render in muted grey.\n const contactRows = copy.contact\n .map((line, i) =>\n i === 0\n ? `\n <mj-text color=\"${RED}\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"700\" line-height=\"30px\">${escapeXml(line)}</mj-text>`\n : `\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"300\" line-height=\"30px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\");\n const footerAddressRows = copy.footerAddress\n .map(\n (line) => `\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\");\n\n return `<mjml>\n <mj-head>\n <mj-attributes>\n <mj-text font-family=\"helvetica, sans-serif\" padding-left=\"5px\" padding-right=\"5px\" />\n <mj-section padding-left=\"11%\" padding-right=\"11%\"/>\n <mj-image padding=\"0px\" />\n </mj-attributes>\n <mj-preview>${escapeXml(previewText)}</mj-preview>\n ${headerStyleBlock(data)}\n </mj-head>\n <mj-body background-color=\"white\">\n <mj-section background-color=\"#F4F4F4\" padding-top=\"0px\" padding-bottom=\"0px\" padding-left=\"0px\" padding-right=\"0px\">\n <mj-column>${headerImageTag(data)}</mj-column>\n </mj-section>\n <mj-section background-color=\"${introBg}\" padding-top=\"${SECTION_PAD}\" padding-bottom=\"${SECTION_PAD}\">\n <mj-column>\n ${sectionLabel(copy.announceHeading)}\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\" padding-top=\"20px\">Prepared for ${escapeXml(data.siteName)}</mj-text>\n ${bodyLine(copy.announceBody)}\n </mj-column>\n </mj-section>\n ${maintenanceSection}\n ${testingSection}\n ${lighthouseScoresSection(data.lighthouse, { background: lighthouseBg, pad: SECTION_PAD })}\n ${analytics}\n ${improvementsSection}\n <mj-section background-color=\"${contactBg}\" padding-top=\"${SECTION_PAD}\" padding-bottom=\"${SECTION_PAD}\">\n <mj-column>\n <mj-text color=\"${RED}\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"700\" padding-top=\"0px\" line-height=\"36px\">Questions, concerns or requests?</mj-text>\n ${contactRows}\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" line-height=\"20px\" font-style=\"italic\">Copyright ${new Date().getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"700\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">Our mailing address is:</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(copy.footerOrg)}</mj-text>\n ${footerAddressRows}\n </mj-column>\n </mj-section>\n </mj-body>\n</mjml>`;\n}\n","import mjml2html from \"mjml\";\nimport type { ReportData } from \"./types.js\";\nimport { buildMjml } from \"./maintenance-email/template.js\";\nimport { buildLaunchMjml } from \"./launch-email/template.js\";\nimport { buildAnnouncementMjml } from \"./announcement-email/template.js\";\n\nexport type RenderResult = {\n html: string;\n warnings: Array<{ line: number; message: string }>;\n};\n\nexport async function renderReportHtml(data: ReportData): Promise<RenderResult> {\n const mjml =\n data.reportType === \"Launch\"\n ? buildLaunchMjml(data)\n : data.reportType === \"Announcement\"\n ? buildAnnouncementMjml(data)\n : buildMjml(data);\n const out = await mjml2html(mjml, { validationLevel: \"strict\" });\n return { html: out.html, warnings: out.errors ?? [] };\n}\n","import type { ReportData } from \"../types.js\";\nimport { DEFAULT_COPY } from \"../copy.js\";\nimport {\n escapeXml,\n fmtDate,\n headerImageTag,\n headerStyleBlock,\n} from \"../maintenance-email/template.js\";\n\nconst RED = \"#C00\";\nconst GREY = \"#757575\";\n\n/** Purpose-built go-live email: header · LAUNCHED + date · message · what-we-set-up\n * · contact · footer. Reuses the M6a copy layer (contact/footer honor per-site\n * overrides). No maintenance checklist / Lighthouse / analytics. */\nexport function buildLaunchMjml(data: ReportData): string {\n const copy = data.copy ?? DEFAULT_COPY;\n const previewText = `${escapeXml(data.siteName)} is live`;\n // All copy — launchHeading/launchBody/launchSetupItems included — is escaped\n // (spec §3.3: all copy escaped). It keeps strict MJML from choking on a stray\n // `&`/`<` if the default copy ever gains one, matching contact/footer below.\n const setupRows = copy.launchSetupItems\n .map(\n (item) => `\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\" padding-top=\"4px\" padding-bottom=\"4px\">• ${escapeXml(item)}</mj-text>`,\n )\n .join(\"\");\n // First contact line (\"Just hit reply.\") renders as a red bold heading, matching the\n // \"Any questions, concerns or requests?\" title above it; following lines stay default.\n const contactRows = copy.contact\n .map((line, i) =>\n i === 0\n ? `\n <mj-text color=\"${RED}\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"700\" line-height=\"30px\">${escapeXml(line)}</mj-text>`\n : `\n <mj-text font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"300\" line-height=\"30px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\");\n const footerAddressRows = copy.footerAddress\n .map(\n (line) => `\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(line)}</mj-text>`,\n )\n .join(\"\");\n\n return `<mjml>\n <mj-head>\n <mj-attributes>\n <mj-text font-family=\"helvetica, sans-serif\" padding-left=\"5px\" padding-right=\"5px\" />\n <mj-section padding-left=\"11%\" padding-right=\"11%\"/>\n <mj-image padding=\"0px\" />\n </mj-attributes>\n <mj-preview>${previewText}</mj-preview>\n ${headerStyleBlock(data)}\n </mj-head>\n <mj-body background-color=\"white\">\n <mj-section background-color=\"#F4F4F4\" padding-top=\"0px\" padding-bottom=\"0px\" padding-left=\"0px\" padding-right=\"0px\">\n <mj-column>${headerImageTag(data)}</mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column>\n <mj-text color=\"${RED}\" font-size=\"20px\" font-weight=\"700\" padding-top=\"75px\">${escapeXml(copy.launchHeading)}</mj-text>\n <mj-text color=\"${RED}\" font-size=\"44px\" font-weight=\"400\">${fmtDate(data.completedOn)}</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"16px\" font-weight=\"300\" line-height=\"24px\" padding-top=\"20px\">${escapeXml(copy.launchBody)}</mj-text>\n ${setupRows}\n </mj-column>\n </mj-section>\n <mj-section background-color=\"white\">\n <mj-column padding-top=\"36px\">\n <mj-text color=\"${RED}\" font-family=\"helvetica, sans-serif\" font-size=\"24px\" font-weight=\"700\" padding-top=\"36px\" line-height=\"36px\">Any questions, concerns or requests?</mj-text>\n ${contactRows}\n <mj-divider border-width=\"1px\" border-style=\"solid\" border-color=\"#CCCCCC\" padding=\"0\" />\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" padding-top=\"24px\" line-height=\"20px\" font-style=\"italic\">Copyright ${new Date().getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"700\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">Our mailing address is:</mj-text>\n <mj-text color=\"${GREY}\" font-family=\"helvetica, sans-serif\" font-size=\"12px\" font-weight=\"300\" line-height=\"16px\" padding-top=\"0\" padding-bottom=\"0px\">${escapeXml(copy.footerOrg)}</mj-text>\n ${footerAddressRows}\n </mj-column>\n </mj-section>\n </mj-body>\n</mjml>`;\n}\n","/** Cheap HTML sniff: an Airtable signed-URL \"200\" that is really a login/error page\n * starts with `<!doctype html`, `<html`, or `<head` after an optional UTF-8 BOM /\n * leading whitespace. We only need to catch the common error-page case, not parse\n * HTML. */\nfunction looksLikeHtml(bytes: Uint8Array): boolean {\n // Inspect the first ~64 bytes as ASCII (1 byte → 1 char; enough for a doctype /\n // opening tag). Skip a leading UTF-8 BOM (bytes EF BB BF) by index, then strip any\n // leading ASCII whitespace, and match the common HTML openers case-insensitively.\n const start = bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ? 3 : 0;\n const head = Buffer.from(bytes.slice(start, start + 64))\n .toString(\"ascii\")\n .replace(/^[\\s]+/, \"\")\n .toLowerCase();\n return head.startsWith(\"<!doctype html\") || head.startsWith(\"<html\") || head.startsWith(\"<head\");\n}\n\nexport async function fetchAttachmentBytes(\n url: string,\n): Promise<{ bytes: Uint8Array; contentType: string }> {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch Airtable attachment ${res.status} ${res.statusText} (url=${url})`,\n );\n }\n const contentType = res.headers.get(\"content-type\") ?? \"application/octet-stream\";\n const ab = await res.arrayBuffer();\n const bytes = new Uint8Array(ab);\n // Sanity-gate the body: a 200 that is actually an HTML error/login page (expired\n // signed URL, auth wall) would otherwise be attached as the \"image\" and ship a\n // broken header. Accept an explicit image/* content-type; otherwise reject anything\n // that sniffs as HTML — so the send fails loudly rather than emailing a broken image.\n const isImageType = contentType.toLowerCase().startsWith(\"image/\");\n if (!isImageType && looksLikeHtml(bytes)) {\n throw new Error(\n `Airtable attachment did not return image data (content-type=\"${contentType}\", ` +\n `body looks like an HTML page — the signed URL may have expired) (url=${url})`,\n );\n }\n return { bytes, contentType };\n}\n\n/**\n * Upload bytes (or a string) as an attachment to a specific record + field.\n * Uses Airtable's content.airtable.com upload endpoint (base64 body) because\n * the standard SDK only accepts public URLs for attachments, and we don't\n * host the generated content anywhere public.\n *\n * Docs: https://airtable.com/developers/web/api/upload-attachment\n *\n * Requires AIRTABLE_PAT + AIRTABLE_BASE_ID in env (same as the rest of the\n * reports module). The fieldName is URL-encoded for the request path.\n */\nexport async function uploadAttachment(\n recordId: string,\n fieldName: string,\n body: Uint8Array | string,\n filename: string,\n contentType: string,\n): Promise<void> {\n const apiKey = process.env.AIRTABLE_PAT;\n const baseId = process.env.AIRTABLE_BASE_ID;\n if (!apiKey || !baseId) {\n throw new Error(\"AIRTABLE_PAT and AIRTABLE_BASE_ID must be set\");\n }\n const base64 =\n typeof body === \"string\"\n ? Buffer.from(body, \"utf-8\").toString(\"base64\")\n : Buffer.from(body).toString(\"base64\");\n const payload = { contentType, file: base64, filename };\n const url = `https://content.airtable.com/v0/${baseId}/${recordId}/${encodeURIComponent(fieldName)}/uploadAttachment`;\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n if (!res.ok) {\n throw new Error(`Airtable upload failed: ${res.status} ${res.statusText} ${await res.text()}`);\n }\n}\n"],"mappings":"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","names":["escapeXml","RED","GREY","escapeXml","RED","GREY","escapeXml"]}