@reddoorla/maintenance 0.55.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/airtable-2BJX4ATJ.js +9 -0
- package/dist/airtable-2BJX4ATJ.js.map +1 -0
- package/dist/announce-JEVCDDKL.js +171 -0
- package/dist/announce-JEVCDDKL.js.map +1 -0
- package/dist/bump-deps-O3SOLQYY.js +57 -0
- package/dist/bump-deps-O3SOLQYY.js.map +1 -0
- package/dist/chunk-2X322ZES.js +99 -0
- package/dist/chunk-2X322ZES.js.map +1 -0
- package/dist/chunk-4NAOR44V.js +153 -0
- package/dist/chunk-4NAOR44V.js.map +1 -0
- package/dist/chunk-4PY4I5OR.js +1167 -0
- package/dist/chunk-4PY4I5OR.js.map +1 -0
- package/dist/chunk-5WL3ET24.js +203 -0
- package/dist/chunk-5WL3ET24.js.map +1 -0
- package/dist/chunk-6LIWDIXU.js +38 -0
- package/dist/chunk-6LIWDIXU.js.map +1 -0
- package/dist/chunk-6VAL7XJT.js +52 -0
- package/dist/chunk-6VAL7XJT.js.map +1 -0
- package/dist/chunk-BWLUNY7L.js +585 -0
- package/dist/chunk-BWLUNY7L.js.map +1 -0
- package/dist/chunk-CASCWQQX.js +161 -0
- package/dist/chunk-CASCWQQX.js.map +1 -0
- package/dist/chunk-CLVTVDOU.js +274 -0
- package/dist/chunk-CLVTVDOU.js.map +1 -0
- package/dist/chunk-D3DQFC32.js +79 -0
- package/dist/chunk-D3DQFC32.js.map +1 -0
- package/dist/chunk-DIVB6ZF7.js +217 -0
- package/dist/chunk-DIVB6ZF7.js.map +1 -0
- package/dist/chunk-EYODPUHV.js +37 -0
- package/dist/chunk-EYODPUHV.js.map +1 -0
- package/dist/chunk-FDIFTIXC.js +70 -0
- package/dist/chunk-FDIFTIXC.js.map +1 -0
- package/dist/chunk-FUD4I2ZW.js +35 -0
- package/dist/chunk-FUD4I2ZW.js.map +1 -0
- package/dist/chunk-HG5DTFNF.js +62 -0
- package/dist/chunk-HG5DTFNF.js.map +1 -0
- package/dist/chunk-HN6I2XWI.js +65 -0
- package/dist/chunk-HN6I2XWI.js.map +1 -0
- package/dist/chunk-HVOOCK6L.js +84 -0
- package/dist/chunk-HVOOCK6L.js.map +1 -0
- package/dist/chunk-IMIR7SC2.js +51 -0
- package/dist/chunk-IMIR7SC2.js.map +1 -0
- package/dist/chunk-J3OPWFZE.js +64 -0
- package/dist/chunk-J3OPWFZE.js.map +1 -0
- package/dist/chunk-JNXCDUYB.js +211 -0
- package/dist/chunk-JNXCDUYB.js.map +1 -0
- package/dist/chunk-JQG222CI.js +49 -0
- package/dist/chunk-JQG222CI.js.map +1 -0
- package/dist/chunk-LKGVSM2O.js +43 -0
- package/dist/chunk-LKGVSM2O.js.map +1 -0
- package/dist/chunk-LYCKN7ZR.js +41 -0
- package/dist/chunk-LYCKN7ZR.js.map +1 -0
- package/dist/chunk-O65I7G7G.js +246 -0
- package/dist/chunk-O65I7G7G.js.map +1 -0
- package/dist/chunk-OCS7SZUZ.js +243 -0
- package/dist/chunk-OCS7SZUZ.js.map +1 -0
- package/dist/chunk-OGGRFBL4.js +14 -0
- package/dist/chunk-OGGRFBL4.js.map +1 -0
- package/dist/chunk-OQPMZ6BM.js +368 -0
- package/dist/chunk-OQPMZ6BM.js.map +1 -0
- package/dist/chunk-OTGB6YQO.js +52 -0
- package/dist/chunk-OTGB6YQO.js.map +1 -0
- package/dist/chunk-PF6BNYGE.js +162 -0
- package/dist/chunk-PF6BNYGE.js.map +1 -0
- package/dist/chunk-QTHKOIFJ.js +13 -0
- package/dist/chunk-QTHKOIFJ.js.map +1 -0
- package/dist/chunk-U5SZ4FOP.js +26 -0
- package/dist/chunk-U5SZ4FOP.js.map +1 -0
- package/dist/chunk-VXSX2FHO.js +269 -0
- package/dist/chunk-VXSX2FHO.js.map +1 -0
- package/dist/chunk-W3G7XFBZ.js +444 -0
- package/dist/chunk-W3G7XFBZ.js.map +1 -0
- package/dist/chunk-WEKL2HYJ.js +31 -0
- package/dist/chunk-WEKL2HYJ.js.map +1 -0
- package/dist/chunk-WVRPOXKH.js +74 -0
- package/dist/chunk-WVRPOXKH.js.map +1 -0
- package/dist/chunk-XKOZUVK2.js +121 -0
- package/dist/chunk-XKOZUVK2.js.map +1 -0
- package/dist/chunk-XO2RUYVM.js +138 -0
- package/dist/chunk-XO2RUYVM.js.map +1 -0
- package/dist/chunk-XTK5VIQB.js +97 -0
- package/dist/chunk-XTK5VIQB.js.map +1 -0
- package/dist/chunk-XXTZBPUY.js +9 -0
- package/dist/chunk-XXTZBPUY.js.map +1 -0
- package/dist/chunk-ZYBOT5PM.js +153 -0
- package/dist/chunk-ZYBOT5PM.js.map +1 -0
- package/dist/cli/bin.js +75 -7785
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +37 -2336
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-EPVO3FEH.js +10 -0
- package/dist/client-EPVO3FEH.js.map +1 -0
- package/dist/client-JA6QLEDW.js +48 -0
- package/dist/client-JA6QLEDW.js.map +1 -0
- package/dist/configs/lighthouse.js +4 -31
- package/dist/configs/lighthouse.js.map +1 -1
- package/dist/configs/playwright-a11y.js +6 -33
- package/dist/configs/playwright-a11y.js.map +1 -1
- package/dist/convert-to-pnpm-UR64X3OX.js +51 -0
- package/dist/convert-to-pnpm-UR64X3OX.js.map +1 -0
- package/dist/db-AXKMRTVA.js +23 -0
- package/dist/db-AXKMRTVA.js.map +1 -0
- package/dist/digest-YTO73BEO.js +408 -0
- package/dist/digest-YTO73BEO.js.map +1 -0
- package/dist/forms/index.js +3 -8
- package/dist/forms/index.js.map +1 -1
- package/dist/github-signals-YD64R2AF.js +122 -0
- package/dist/github-signals-YD64R2AF.js.map +1 -0
- package/dist/index.js +102 -4757
- package/dist/index.js.map +1 -1
- package/dist/init-JITQXFVL.js +85 -0
- package/dist/init-JITQXFVL.js.map +1 -0
- package/dist/launch-HXHEL6ID.js +207 -0
- package/dist/launch-HXHEL6ID.js.map +1 -0
- package/dist/lighthouse-airtable-4FKYKFH6.js +12 -0
- package/dist/lighthouse-airtable-4FKYKFH6.js.map +1 -0
- package/dist/migrate-P2LS4WXI.js +7 -0
- package/dist/migrate-P2LS4WXI.js.map +1 -0
- package/dist/onboard-TCQOP2W4.js +69 -0
- package/dist/onboard-TCQOP2W4.js.map +1 -0
- package/dist/orchestrate-RMSBHLQK.js +19 -0
- package/dist/orchestrate-RMSBHLQK.js.map +1 -0
- package/dist/recipes/sync-configs.js +9 -505
- package/dist/recipes/sync-configs.js.map +1 -1
- package/dist/renovate-dispatch-ZGLXNPEK.js +97 -0
- package/dist/renovate-dispatch-ZGLXNPEK.js.map +1 -0
- package/dist/report-SD3CNBOQ.js +240 -0
- package/dist/report-SD3CNBOQ.js.map +1 -0
- package/dist/resend-G2BJMAQV.js +7 -0
- package/dist/resend-G2BJMAQV.js.map +1 -0
- package/dist/self-updating-CIUMOMYI.js +61 -0
- package/dist/self-updating-CIUMOMYI.js.map +1 -0
- package/dist/svelte-codemods-BYRJAU7I.js +51 -0
- package/dist/svelte-codemods-BYRJAU7I.js.map +1 -0
- package/dist/sync-configs-JPRLAW5J.js +109 -0
- package/dist/sync-configs-JPRLAW5J.js.map +1 -0
- package/dist/upgrade-ERA45EVR.js +64 -0
- package/dist/upgrade-ERA45EVR.js.map +1 -0
- package/dist/util/git.js +19 -76
- package/dist/util/git.js.map +1 -1
- package/dist/util/pkg.js +5 -45
- package/dist/util/pkg.js.map +1 -1
- package/dist/websites-X5DLC2PC.js +43 -0
- package/dist/websites-X5DLC2PC.js.map +1 -0
- package/dist/write-audits-to-airtable-CE4POBAF.js +13 -0
- package/dist/write-audits-to-airtable-CE4POBAF.js.map +1 -0
- package/package.json +13 -12
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// src/recipes/svelte-5/step-gotchas.ts
|
|
2
|
+
import { readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { glob } from "tinyglobby";
|
|
5
|
+
|
|
6
|
+
// src/recipes/svelte-5/codemods/on-event-to-handler.ts
|
|
7
|
+
var SCRIPT_BLOCK = /<script\b[^>]*>[\s\S]*?<\/script>/g;
|
|
8
|
+
var SIMPLE_ON_EVENT = /\bon:([a-z]+)(?=\s*=)/g;
|
|
9
|
+
var MODIFIER_EVENT = /\bon:[a-z]+\|[a-zA-Z]+(?=\s*=)/g;
|
|
10
|
+
function flagEventModifiers(source) {
|
|
11
|
+
const insertions = [];
|
|
12
|
+
let m;
|
|
13
|
+
MODIFIER_EVENT.lastIndex = 0;
|
|
14
|
+
while ((m = MODIFIER_EVENT.exec(source)) !== null) {
|
|
15
|
+
const tagStart = source.lastIndexOf("<", m.index);
|
|
16
|
+
if (tagStart === -1) continue;
|
|
17
|
+
const prevLineEnd = tagStart - 1;
|
|
18
|
+
if (prevLineEnd >= 0) {
|
|
19
|
+
const prevLineStart = source.lastIndexOf("\n", prevLineEnd - 1) + 1;
|
|
20
|
+
const prevLine = source.slice(prevLineStart, prevLineEnd + 1);
|
|
21
|
+
if (/<!--\s*@migration-task/.test(prevLine)) continue;
|
|
22
|
+
}
|
|
23
|
+
const lineStart = source.lastIndexOf("\n", tagStart - 1) + 1;
|
|
24
|
+
const indent = source.slice(lineStart, tagStart);
|
|
25
|
+
const safeIndent = /^[ \t]*$/.test(indent) ? indent : "";
|
|
26
|
+
insertions.push({ tagStart, indent: safeIndent, modifier: m[0] });
|
|
27
|
+
}
|
|
28
|
+
let out = source;
|
|
29
|
+
for (let i = insertions.length - 1; i >= 0; i--) {
|
|
30
|
+
const { tagStart, indent, modifier } = insertions[i];
|
|
31
|
+
const comment = `<!-- @migration-task: Svelte 5 removed event modifier syntax (\`${modifier}\`). Rewrite inline, e.g. onclick={(e) => { e.preventDefault(); ... }}. -->
|
|
32
|
+
${indent}`;
|
|
33
|
+
out = out.slice(0, tagStart) + comment + out.slice(tagStart);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function onEventToHandler(source) {
|
|
38
|
+
const masked = [];
|
|
39
|
+
const placeholder = (i) => ` SCRIPT_${i} `;
|
|
40
|
+
const intermediate = source.replace(SCRIPT_BLOCK, (match) => {
|
|
41
|
+
masked.push(match);
|
|
42
|
+
return placeholder(masked.length - 1);
|
|
43
|
+
});
|
|
44
|
+
let processed = intermediate.replace(SIMPLE_ON_EVENT, (_full, name) => `on${name}`);
|
|
45
|
+
processed = flagEventModifiers(processed);
|
|
46
|
+
let out = processed;
|
|
47
|
+
masked.forEach((blk, i) => {
|
|
48
|
+
out = out.replace(placeholder(i), blk);
|
|
49
|
+
});
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/recipes/svelte-5/codemods/dollar-props.ts
|
|
54
|
+
var SCRIPT_BLOCK2 = /<script\b([^>]*)>([\s\S]*?)<\/script>/;
|
|
55
|
+
var EXPORT_LET = /^\s*export\s+let\s+(\w+)\s*(?::\s*([^=;\n]+))?\s*(?:=\s*([^;\n]+))?;?\s*$/gm;
|
|
56
|
+
function transformScript(scriptBody, isTs) {
|
|
57
|
+
const props = [];
|
|
58
|
+
const cleaned = scriptBody.replace(
|
|
59
|
+
EXPORT_LET,
|
|
60
|
+
(_full, name, type, defaultExpr) => {
|
|
61
|
+
props.push({
|
|
62
|
+
name,
|
|
63
|
+
type: type?.trim(),
|
|
64
|
+
defaultExpr: defaultExpr?.trim()
|
|
65
|
+
});
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
if (props.length === 0) return { body: scriptBody, changed: false };
|
|
70
|
+
const destructured = props.map((p) => p.defaultExpr ? `${p.name} = ${p.defaultExpr}` : p.name).join(", ");
|
|
71
|
+
let decl;
|
|
72
|
+
if (isTs) {
|
|
73
|
+
const typeSig = props.map((p) => {
|
|
74
|
+
const optional = p.defaultExpr ? "?" : "";
|
|
75
|
+
return `${p.name}${optional}: ${p.type ?? "unknown"}`;
|
|
76
|
+
}).join("; ");
|
|
77
|
+
decl = ` let { ${destructured} }: { ${typeSig} } = $props();`;
|
|
78
|
+
} else {
|
|
79
|
+
decl = ` let { ${destructured} } = $props();`;
|
|
80
|
+
}
|
|
81
|
+
const next = cleaned.replace(/^(\s*)/, (m) => `${m}${decl}
|
|
82
|
+
`);
|
|
83
|
+
return { body: next, changed: true };
|
|
84
|
+
}
|
|
85
|
+
function exportLetToProps(source) {
|
|
86
|
+
const match = source.match(SCRIPT_BLOCK2);
|
|
87
|
+
if (!match) return source;
|
|
88
|
+
const attrs = match[1] ?? "";
|
|
89
|
+
const inner = match[2] ?? "";
|
|
90
|
+
const isTs = /\blang=["']ts["']/.test(attrs);
|
|
91
|
+
const { body, changed } = transformScript(inner, isTs);
|
|
92
|
+
if (!changed) return source;
|
|
93
|
+
return source.replace(SCRIPT_BLOCK2, (full) => full.replace(inner, body));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/util/svelte-source.ts
|
|
97
|
+
function findStringEnd(source, openIdx) {
|
|
98
|
+
const quote = source[openIdx];
|
|
99
|
+
let i = openIdx + 1;
|
|
100
|
+
while (i < source.length) {
|
|
101
|
+
const ch = source[i];
|
|
102
|
+
if (ch === "\\") {
|
|
103
|
+
i += 2;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (ch === quote) return i;
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
return -1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/recipes/svelte-5/codemods/dollar-restprops.ts
|
|
113
|
+
function removeInterfaceBlock(source) {
|
|
114
|
+
const re = /^\s*interface\s+\$\$Props\s*\{/m;
|
|
115
|
+
let out = source;
|
|
116
|
+
while (true) {
|
|
117
|
+
const match = re.exec(out);
|
|
118
|
+
if (!match) return out;
|
|
119
|
+
const openBraceIdx = match.index + match[0].length - 1;
|
|
120
|
+
let depth = 1;
|
|
121
|
+
let i = openBraceIdx + 1;
|
|
122
|
+
while (i < out.length && depth > 0) {
|
|
123
|
+
const ch = out[i];
|
|
124
|
+
if (ch === "{") depth++;
|
|
125
|
+
else if (ch === "}") depth--;
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
if (depth !== 0) return out;
|
|
129
|
+
let endIdx = i;
|
|
130
|
+
while (endIdx < out.length && /[ \t]/.test(out[endIdx] ?? "")) endIdx++;
|
|
131
|
+
if (out[endIdx] === "\n") endIdx++;
|
|
132
|
+
out = out.slice(0, match.index) + out.slice(endIdx);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function maskStringLiterals(source) {
|
|
136
|
+
const strings = [];
|
|
137
|
+
let out = "";
|
|
138
|
+
let i = 0;
|
|
139
|
+
while (i < source.length) {
|
|
140
|
+
const ch = source[i];
|
|
141
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
142
|
+
const closeIdx = findStringEnd(source, i);
|
|
143
|
+
if (closeIdx === -1) {
|
|
144
|
+
out += source.slice(i);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
const literal = source.slice(i, closeIdx + 1);
|
|
148
|
+
out += `__RDMNT_STR_${strings.length}__`;
|
|
149
|
+
strings.push(literal);
|
|
150
|
+
i = closeIdx + 1;
|
|
151
|
+
} else {
|
|
152
|
+
out += ch;
|
|
153
|
+
i++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
masked: out,
|
|
158
|
+
restore: (s) => s.replace(/__RDMNT_STR_(\d+)__/g, (_full, idx) => strings[Number(idx)] ?? "")
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
var PROPS_DECL = /let\s*\{([^}]*)\}\s*(?::\s*\{([^}]*)\})?\s*=\s*\$props\(\)\s*;?/;
|
|
162
|
+
function injectRestIntoProps(scriptBody) {
|
|
163
|
+
const match = scriptBody.match(PROPS_DECL);
|
|
164
|
+
if (!match) return scriptBody;
|
|
165
|
+
const destructured = match[1] ?? "";
|
|
166
|
+
if (/\.\.\.\s*\w+/.test(destructured)) return scriptBody;
|
|
167
|
+
const trimmed = destructured.trim().replace(/,\s*$/, "");
|
|
168
|
+
const newDestructured = trimmed === "" ? " ...rest " : ` ${trimmed}, ...rest `;
|
|
169
|
+
let replacement;
|
|
170
|
+
if (match[2] !== void 0) {
|
|
171
|
+
const typeBody = match[2];
|
|
172
|
+
const hasIndexSig = /\[\s*key\s*:\s*string\s*\]\s*:/.test(typeBody);
|
|
173
|
+
const newTypeBody = hasIndexSig ? typeBody : `${typeBody.trimEnd().replace(/;?\s*$/, "")}; [key: string]: unknown `;
|
|
174
|
+
replacement = `let {${newDestructured}}: {${newTypeBody}} = $props();`;
|
|
175
|
+
} else {
|
|
176
|
+
replacement = `let {${newDestructured}} = $props();`;
|
|
177
|
+
}
|
|
178
|
+
return scriptBody.replace(PROPS_DECL, replacement);
|
|
179
|
+
}
|
|
180
|
+
var SCRIPT_BLOCK3 = /<script\b([^>]*)>([\s\S]*?)<\/script>/;
|
|
181
|
+
var HAS_PROPS_CALL = /\$props\(\s*\)/;
|
|
182
|
+
function removeDollarRestProps(source) {
|
|
183
|
+
const next = removeInterfaceBlock(source);
|
|
184
|
+
const scriptMatch = next.match(SCRIPT_BLOCK3);
|
|
185
|
+
if (!scriptMatch) return next;
|
|
186
|
+
if (!HAS_PROPS_CALL.test(scriptMatch[2] ?? "")) {
|
|
187
|
+
return next;
|
|
188
|
+
}
|
|
189
|
+
const scriptInner = scriptMatch[2] ?? "";
|
|
190
|
+
const { masked, restore } = maskStringLiterals(scriptInner);
|
|
191
|
+
let processed = injectRestIntoProps(masked);
|
|
192
|
+
processed = processed.replace(/\$\$restProps/g, "rest");
|
|
193
|
+
const restoredInner = restore(processed);
|
|
194
|
+
const newScriptBlock = scriptMatch[0].replace(scriptInner, () => restoredInner);
|
|
195
|
+
const before = next.slice(0, scriptMatch.index);
|
|
196
|
+
const after = next.slice(scriptMatch.index + scriptMatch[0].length);
|
|
197
|
+
return before.replace(/\$\$restProps/g, "rest") + newScriptBlock + after.replace(/\$\$restProps/g, "rest");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/recipes/svelte-5/codemods/state-effect-sync.ts
|
|
201
|
+
var PATTERN = /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;
|
|
202
|
+
function stateEffectSyncToDerived(source) {
|
|
203
|
+
return source.replace(PATTERN, (full, name, initExpr, effectExpr) => {
|
|
204
|
+
if (initExpr.trim() !== effectExpr.trim()) return full;
|
|
205
|
+
return `let ${name} = $derived(${initExpr.trim()});`;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/recipes/svelte-5/codemods/dollar-props-class.ts
|
|
210
|
+
var PROPS_DESTRUCTURE = /let\s*\{([\s\S]*?)\}(\s*:\s*\{([\s\S]*?)\})?\s*=\s*\$props\(\)/;
|
|
211
|
+
var HAS_DOLLAR_PROPS_CLASS = /\$\$props\.class\b/;
|
|
212
|
+
var DOLLAR_PROPS_CLASS_GLOBAL = /\$\$props\.class\b/g;
|
|
213
|
+
var DOLLAR_PROPS_ANY = /\$\$props\b/;
|
|
214
|
+
var SCRIPT_BLOCK4 = /<script\b[^>]*>[\s\S]*?<\/script>/g;
|
|
215
|
+
var MIGRATION_TASK = /^<!--\s*@migration-task[\s\S]*?-->\s*\n?/gm;
|
|
216
|
+
var IDENT = "className";
|
|
217
|
+
function maskScripts(source) {
|
|
218
|
+
const blocks = [];
|
|
219
|
+
const masked = source.replace(SCRIPT_BLOCK4, (m) => {
|
|
220
|
+
blocks.push(m);
|
|
221
|
+
return `__SCRIPT_${blocks.length - 1}__`;
|
|
222
|
+
});
|
|
223
|
+
return { masked, blocks };
|
|
224
|
+
}
|
|
225
|
+
function restoreScripts(masked, blocks) {
|
|
226
|
+
let out = masked;
|
|
227
|
+
blocks.forEach((blk, i) => {
|
|
228
|
+
out = out.replace(`__SCRIPT_${i}__`, blk);
|
|
229
|
+
});
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
function dollarPropsClass(source) {
|
|
233
|
+
const { masked } = maskScripts(source);
|
|
234
|
+
if (!HAS_DOLLAR_PROPS_CLASS.test(masked)) return source;
|
|
235
|
+
if (!PROPS_DESTRUCTURE.test(source)) return source;
|
|
236
|
+
let updated = source.replace(PROPS_DESTRUCTURE, (full, body, typeAnno, typeBody) => {
|
|
237
|
+
if (/\bclass\s*:/.test(body)) return full;
|
|
238
|
+
const cleanBody = body.trim().replace(/,\s*$/, "").trim();
|
|
239
|
+
const newBody = cleanBody ? `${cleanBody}, class: ${IDENT} = ""` : `class: ${IDENT} = ""`;
|
|
240
|
+
if (typeAnno) {
|
|
241
|
+
const cleanType = (typeBody ?? "").trim().replace(/;\s*$/, "").trim();
|
|
242
|
+
const newType = cleanType ? `${cleanType}; class?: string` : `class?: string`;
|
|
243
|
+
return `let { ${newBody} }: { ${newType} } = $props()`;
|
|
244
|
+
}
|
|
245
|
+
return `let { ${newBody} } = $props()`;
|
|
246
|
+
});
|
|
247
|
+
const reMasked = maskScripts(updated);
|
|
248
|
+
const templateRewritten = reMasked.masked.replace(DOLLAR_PROPS_CLASS_GLOBAL, IDENT);
|
|
249
|
+
updated = restoreScripts(templateRewritten, reMasked.blocks);
|
|
250
|
+
const stripped = updated.replace(MIGRATION_TASK, "");
|
|
251
|
+
if (!DOLLAR_PROPS_ANY.test(stripped)) {
|
|
252
|
+
updated = stripped;
|
|
253
|
+
}
|
|
254
|
+
return updated;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/recipes/svelte-5/codemods/legacy-reactive.ts
|
|
258
|
+
var SCRIPT_BLOCK5 = /<script\b([^>]*)>([\s\S]*?)<\/script>/g;
|
|
259
|
+
var SIMPLE_REACTIVE = /^([ \t]*)\$:\s*(\w+)\s*=\s*([^;\n]+);?[ \t]*$/gm;
|
|
260
|
+
var BLOCK_REACTIVE_HEAD = /(^|\n)([ \t]*)\$:\s*\{/g;
|
|
261
|
+
function findMatchingClose(source, openIdx) {
|
|
262
|
+
let depth = 0;
|
|
263
|
+
let i = openIdx;
|
|
264
|
+
while (i < source.length) {
|
|
265
|
+
const ch = source[i];
|
|
266
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
267
|
+
const closeStr = findStringEnd(source, i);
|
|
268
|
+
if (closeStr === -1) return -1;
|
|
269
|
+
i = closeStr + 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (ch === "/") {
|
|
273
|
+
const next = source[i + 1];
|
|
274
|
+
if (next === "/") {
|
|
275
|
+
const eol = source.indexOf("\n", i + 2);
|
|
276
|
+
i = eol === -1 ? source.length : eol;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (next === "*") {
|
|
280
|
+
const end = source.indexOf("*/", i + 2);
|
|
281
|
+
if (end === -1) return -1;
|
|
282
|
+
i = end + 2;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (ch === "{") depth++;
|
|
287
|
+
else if (ch === "}") {
|
|
288
|
+
depth--;
|
|
289
|
+
if (depth === 0) return i;
|
|
290
|
+
}
|
|
291
|
+
i++;
|
|
292
|
+
}
|
|
293
|
+
return -1;
|
|
294
|
+
}
|
|
295
|
+
var MIGRATION_MARKER = "// @migration-task: $effect won't trigger UI updates on plain `let` bindings \u2014 refine mutated locals to $state or split into per-variable $derived.";
|
|
296
|
+
function transformBlocks(body) {
|
|
297
|
+
const out = [];
|
|
298
|
+
let last = 0;
|
|
299
|
+
BLOCK_REACTIVE_HEAD.lastIndex = 0;
|
|
300
|
+
let m;
|
|
301
|
+
while ((m = BLOCK_REACTIVE_HEAD.exec(body)) !== null) {
|
|
302
|
+
const leadingNewline = m[1] ?? "";
|
|
303
|
+
const indent = m[2] ?? "";
|
|
304
|
+
const headEnd = m.index + m[0].length;
|
|
305
|
+
const openBraceIdx = headEnd - 1;
|
|
306
|
+
const closeBraceIdx = findMatchingClose(body, openBraceIdx);
|
|
307
|
+
if (closeBraceIdx === -1) continue;
|
|
308
|
+
out.push(body.slice(last, m.index));
|
|
309
|
+
out.push(leadingNewline);
|
|
310
|
+
const blockBody = body.slice(openBraceIdx + 1, closeBraceIdx);
|
|
311
|
+
out.push(`${indent}${MIGRATION_MARKER}
|
|
312
|
+
`);
|
|
313
|
+
out.push(`${indent}$effect(() => {${blockBody}});`);
|
|
314
|
+
last = closeBraceIdx + 1;
|
|
315
|
+
BLOCK_REACTIVE_HEAD.lastIndex = last;
|
|
316
|
+
}
|
|
317
|
+
out.push(body.slice(last));
|
|
318
|
+
return out.join("");
|
|
319
|
+
}
|
|
320
|
+
function transformSimple(body) {
|
|
321
|
+
return body.replace(SIMPLE_REACTIVE, (_full, indent, name, expr) => {
|
|
322
|
+
return `${indent}let ${name} = $derived(${expr.trim()});`;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function legacyReactiveToRunes(source) {
|
|
326
|
+
return source.replace(SCRIPT_BLOCK5, (full, _attrs, body) => {
|
|
327
|
+
let next = transformBlocks(body);
|
|
328
|
+
next = transformSimple(next);
|
|
329
|
+
if (next === body) return full;
|
|
330
|
+
return full.replace(body, next);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/recipes/svelte-5/step-gotchas.ts
|
|
335
|
+
var SVELTE_GLOBS = ["src/**/*.svelte"];
|
|
336
|
+
var IGNORE = ["node_modules/**", ".svelte-kit/**", "build/**"];
|
|
337
|
+
var CODEMODS = [
|
|
338
|
+
onEventToHandler,
|
|
339
|
+
exportLetToProps,
|
|
340
|
+
removeDollarRestProps,
|
|
341
|
+
stateEffectSyncToDerived,
|
|
342
|
+
dollarPropsClass,
|
|
343
|
+
legacyReactiveToRunes
|
|
344
|
+
];
|
|
345
|
+
async function planGotchaCodemods(cwd) {
|
|
346
|
+
const changes = [];
|
|
347
|
+
const relPaths = await glob(SVELTE_GLOBS, { cwd, ignore: IGNORE, absolute: false });
|
|
348
|
+
for (const rel of relPaths) {
|
|
349
|
+
const path = join(cwd, rel);
|
|
350
|
+
const before = await readFile(path, "utf-8");
|
|
351
|
+
const after = CODEMODS.reduce((s, fn) => fn(s), before);
|
|
352
|
+
if (after !== before) changes.push({ rel, after });
|
|
353
|
+
}
|
|
354
|
+
return changes;
|
|
355
|
+
}
|
|
356
|
+
async function applyGotchaCodemods(cwd) {
|
|
357
|
+
const changes = await planGotchaCodemods(cwd);
|
|
358
|
+
for (const c of changes) {
|
|
359
|
+
await writeFile(join(cwd, c.rel), c.after, "utf-8");
|
|
360
|
+
}
|
|
361
|
+
return { filesChanged: changes.length };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export {
|
|
365
|
+
planGotchaCodemods,
|
|
366
|
+
applyGotchaCodemods
|
|
367
|
+
};
|
|
368
|
+
//# sourceMappingURL=chunk-OQPMZ6BM.js.map
|
|
@@ -0,0 +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":";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;AACpE,UAAM,UAAU,YAAY,GAAG,SAAS,YAAY,KAAK,UAAU,UAAU,KAAK;AAElF,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;;;AC5EA,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"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/util/pkg.ts
|
|
2
|
+
import { readFile, writeFile } from "fs/promises";
|
|
3
|
+
async function readPackageJson(path) {
|
|
4
|
+
const raw = await readFile(path, "utf-8");
|
|
5
|
+
return JSON.parse(raw);
|
|
6
|
+
}
|
|
7
|
+
function detectIndentFromContent(raw) {
|
|
8
|
+
const match = raw.match(/\n([ \t]+)"/);
|
|
9
|
+
return match ? match[1] ?? " " : " ";
|
|
10
|
+
}
|
|
11
|
+
async function writePackageJson(path, pkg) {
|
|
12
|
+
let indent = " ";
|
|
13
|
+
try {
|
|
14
|
+
const existing = await readFile(path, "utf-8");
|
|
15
|
+
indent = detectIndentFromContent(existing);
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
18
|
+
const content = JSON.stringify(pkg, null, indent) + "\n";
|
|
19
|
+
await writeFile(path, content, "utf-8");
|
|
20
|
+
}
|
|
21
|
+
function bumpDep(pkg, name, version, opts = {}) {
|
|
22
|
+
const mode = opts.mode ?? "ensure";
|
|
23
|
+
const next = {
|
|
24
|
+
...pkg
|
|
25
|
+
};
|
|
26
|
+
if (pkg.dependencies) {
|
|
27
|
+
next.dependencies = { ...pkg.dependencies };
|
|
28
|
+
}
|
|
29
|
+
if (pkg.devDependencies) {
|
|
30
|
+
next.devDependencies = { ...pkg.devDependencies };
|
|
31
|
+
}
|
|
32
|
+
if (next.dependencies && name in next.dependencies) {
|
|
33
|
+
if (next.dependencies[name] === version) return pkg;
|
|
34
|
+
next.dependencies[name] = version;
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
if (next.devDependencies && name in next.devDependencies) {
|
|
38
|
+
if (next.devDependencies[name] === version) return pkg;
|
|
39
|
+
next.devDependencies[name] = version;
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
if (mode === "bump-only") return pkg;
|
|
43
|
+
next.devDependencies = { ...next.devDependencies ?? {}, [name]: version };
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
readPackageJson,
|
|
49
|
+
writePackageJson,
|
|
50
|
+
bumpDep
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=chunk-OTGB6YQO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util/pkg.ts"],"sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\n\nexport type PackageJsonLike = {\n name?: string;\n version?: string;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n [key: string]: unknown;\n};\n\nexport async function readPackageJson(path: string): Promise<PackageJsonLike> {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as PackageJsonLike;\n}\n\n/** Sniff the indent style (tab vs 2 vs 4 vs N spaces) from existing package.json\n * content by looking at the first indented `\"key\"` line. Defaults to two spaces. */\nfunction detectIndentFromContent(raw: string): string {\n const match = raw.match(/\\n([ \\t]+)\"/);\n return match ? (match[1] ?? \" \") : \" \";\n}\n\nexport async function writePackageJson(path: string, pkg: PackageJsonLike): Promise<void> {\n let indent = \" \";\n try {\n const existing = await readFile(path, \"utf-8\");\n indent = detectIndentFromContent(existing);\n } catch {\n // file doesn't exist yet — first write — keep the 2-space default\n }\n const content = JSON.stringify(pkg, null, indent) + \"\\n\";\n await writeFile(path, content, \"utf-8\");\n}\n\nexport type BumpDepMode =\n | \"ensure\" // default: add to devDependencies if missing\n | \"bump-only\"; // never add; only update existing entries\n\nexport type BumpDepOptions = {\n mode?: BumpDepMode;\n};\n\nexport function bumpDep(\n pkg: PackageJsonLike,\n name: string,\n version: string,\n opts: BumpDepOptions = {},\n): PackageJsonLike {\n const mode = opts.mode ?? \"ensure\";\n\n const next: PackageJsonLike = {\n ...pkg,\n };\n\n if (pkg.dependencies) {\n next.dependencies = { ...pkg.dependencies };\n }\n if (pkg.devDependencies) {\n next.devDependencies = { ...pkg.devDependencies };\n }\n\n if (next.dependencies && name in next.dependencies) {\n if (next.dependencies[name] === version) return pkg;\n next.dependencies[name] = version;\n return next;\n }\n if (next.devDependencies && name in next.devDependencies) {\n if (next.devDependencies[name] === version) return pkg;\n next.devDependencies[name] = version;\n return next;\n }\n // Not present in either map. bump-only leaves the pkg alone so recipes\n // can express \"raise the floor on packages this site already uses\" without\n // also installing every related dep across the fleet.\n if (mode === \"bump-only\") return pkg;\n next.devDependencies = { ...(next.devDependencies ?? {}), [name]: version };\n return next;\n}\n"],"mappings":";AAAA,SAAS,UAAU,iBAAiB;AAUpC,eAAsB,gBAAgB,MAAwC;AAC5E,QAAM,MAAM,MAAM,SAAS,MAAM,OAAO;AACxC,SAAO,KAAK,MAAM,GAAG;AACvB;AAIA,SAAS,wBAAwB,KAAqB;AACpD,QAAM,QAAQ,IAAI,MAAM,aAAa;AACrC,SAAO,QAAS,MAAM,CAAC,KAAK,OAAQ;AACtC;AAEA,eAAsB,iBAAiB,MAAc,KAAqC;AACxF,MAAI,SAAS;AACb,MAAI;AACF,UAAM,WAAW,MAAM,SAAS,MAAM,OAAO;AAC7C,aAAS,wBAAwB,QAAQ;AAAA,EAC3C,QAAQ;AAAA,EAER;AACA,QAAM,UAAU,KAAK,UAAU,KAAK,MAAM,MAAM,IAAI;AACpD,QAAM,UAAU,MAAM,SAAS,OAAO;AACxC;AAUO,SAAS,QACd,KACA,MACA,SACA,OAAuB,CAAC,GACP;AACjB,QAAM,OAAO,KAAK,QAAQ;AAE1B,QAAM,OAAwB;AAAA,IAC5B,GAAG;AAAA,EACL;AAEA,MAAI,IAAI,cAAc;AACpB,SAAK,eAAe,EAAE,GAAG,IAAI,aAAa;AAAA,EAC5C;AACA,MAAI,IAAI,iBAAiB;AACvB,SAAK,kBAAkB,EAAE,GAAG,IAAI,gBAAgB;AAAA,EAClD;AAEA,MAAI,KAAK,gBAAgB,QAAQ,KAAK,cAAc;AAClD,QAAI,KAAK,aAAa,IAAI,MAAM,QAAS,QAAO;AAChD,SAAK,aAAa,IAAI,IAAI;AAC1B,WAAO;AAAA,EACT;AACA,MAAI,KAAK,mBAAmB,QAAQ,KAAK,iBAAiB;AACxD,QAAI,KAAK,gBAAgB,IAAI,MAAM,QAAS,QAAO;AACnD,SAAK,gBAAgB,IAAI,IAAI;AAC7B,WAAO;AAAA,EACT;AAIA,MAAI,SAAS,YAAa,QAAO;AACjC,OAAK,kBAAkB,EAAE,GAAI,KAAK,mBAAmB,CAAC,GAAI,CAAC,IAAI,GAAG,QAAQ;AAC1E,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
makeGitHub
|
|
3
|
+
} from "./chunk-DIVB6ZF7.js";
|
|
4
|
+
import {
|
|
5
|
+
templatesByName
|
|
6
|
+
} from "./chunk-CASCWQQX.js";
|
|
7
|
+
import {
|
|
8
|
+
siteLabel
|
|
9
|
+
} from "./chunk-XXTZBPUY.js";
|
|
10
|
+
import {
|
|
11
|
+
branchName,
|
|
12
|
+
checkoutBranch,
|
|
13
|
+
commit,
|
|
14
|
+
createBranch,
|
|
15
|
+
currentBranch,
|
|
16
|
+
getRemoteUrl,
|
|
17
|
+
isOwnerRepo,
|
|
18
|
+
isWorkingTreeClean,
|
|
19
|
+
parseOwnerRepo,
|
|
20
|
+
push
|
|
21
|
+
} from "./chunk-XTK5VIQB.js";
|
|
22
|
+
|
|
23
|
+
// src/recipes/self-updating/index.ts
|
|
24
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
25
|
+
import { dirname, join } from "path";
|
|
26
|
+
|
|
27
|
+
// src/github/config.ts
|
|
28
|
+
function readGitHubConfig() {
|
|
29
|
+
const token = process.env.GITHUB_TOKEN?.trim();
|
|
30
|
+
if (!token) return null;
|
|
31
|
+
const renovateToken = process.env.RENOVATE_TOKEN?.trim() || token;
|
|
32
|
+
return { token, renovateToken };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/recipes/self-updating/index.ts
|
|
36
|
+
var SELF_UPDATING_CONFIGS = ["ci", "renovate-action", "renovate-config"];
|
|
37
|
+
var REQUIRED_CHECK = "ci / ci";
|
|
38
|
+
function resultOf(site, status, notes, commits = []) {
|
|
39
|
+
return { recipe: "self-updating", site: siteLabel(site), status, commits, notes };
|
|
40
|
+
}
|
|
41
|
+
async function resolveRepo(site) {
|
|
42
|
+
if (site.gitRepo) {
|
|
43
|
+
if (!isOwnerRepo(site.gitRepo)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`refusing to act on malformed repo identity: expected "owner/repo", got ${JSON.stringify(site.gitRepo)}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return site.gitRepo;
|
|
49
|
+
}
|
|
50
|
+
let fromOrigin;
|
|
51
|
+
try {
|
|
52
|
+
fromOrigin = parseOwnerRepo(await getRemoteUrl(site.path));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (fromOrigin === null) return null;
|
|
57
|
+
if (!isOwnerRepo(fromOrigin)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`refusing to act on malformed repo identity from origin: ${JSON.stringify(fromOrigin)}`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return fromOrigin;
|
|
63
|
+
}
|
|
64
|
+
async function selfUpdating(site, deps = {}) {
|
|
65
|
+
const templates = templatesByName([...SELF_UPDATING_CONFIGS]);
|
|
66
|
+
const paths = templates.map((t) => t.path);
|
|
67
|
+
let repo;
|
|
68
|
+
try {
|
|
69
|
+
repo = await resolveRepo(site);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return resultOf(site, "failed", err instanceof Error ? err.message : String(err));
|
|
72
|
+
}
|
|
73
|
+
if (!repo) {
|
|
74
|
+
return resultOf(
|
|
75
|
+
site,
|
|
76
|
+
"failed",
|
|
77
|
+
"no Git repo (set Airtable 'Git repo' or add an origin remote)"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const cfg = readGitHubConfig();
|
|
81
|
+
const renovateToken = deps.renovateToken ?? cfg?.renovateToken;
|
|
82
|
+
if (!deps.github && !cfg) return resultOf(site, "failed", "GITHUB_TOKEN not set");
|
|
83
|
+
if (!renovateToken) return resultOf(site, "failed", "no RENOVATE_TOKEN available");
|
|
84
|
+
const github = deps.github ?? makeGitHub({ token: cfg.token });
|
|
85
|
+
const base = await github.defaultBranch(repo).catch(() => "main");
|
|
86
|
+
const actions = [];
|
|
87
|
+
const commits = [];
|
|
88
|
+
let original = null;
|
|
89
|
+
let maintBranch = null;
|
|
90
|
+
try {
|
|
91
|
+
const present = await github.filesOnBranch(repo, base, paths);
|
|
92
|
+
if (present.length < paths.length) {
|
|
93
|
+
const existingPR = await github.findOpenSelfUpdatingPR(repo);
|
|
94
|
+
if (existingPR) {
|
|
95
|
+
actions.push(`bootstrap PR already open: ${existingPR}`);
|
|
96
|
+
} else {
|
|
97
|
+
if (!await isWorkingTreeClean(site.path)) {
|
|
98
|
+
return resultOf(site, "failed", "working tree not clean \u2014 commit or stash first");
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
original = await currentBranch(site.path);
|
|
102
|
+
} catch {
|
|
103
|
+
original = null;
|
|
104
|
+
}
|
|
105
|
+
maintBranch = branchName("self-updating");
|
|
106
|
+
await createBranch(site.path, maintBranch);
|
|
107
|
+
for (const t of templates) {
|
|
108
|
+
const dest = join(site.path, t.path);
|
|
109
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
110
|
+
await writeFile(dest, t.contents, "utf-8");
|
|
111
|
+
}
|
|
112
|
+
const sha = await commit(
|
|
113
|
+
site.path,
|
|
114
|
+
"ci: enable self-updating (CI + Renovate auto-merge)"
|
|
115
|
+
);
|
|
116
|
+
if (sha) commits.push(sha);
|
|
117
|
+
await (deps.pushBranch ?? push)(site.path, maintBranch);
|
|
118
|
+
const pr = await github.openPullRequest(repo, {
|
|
119
|
+
head: maintBranch,
|
|
120
|
+
base,
|
|
121
|
+
title: "Enable self-updating (CI + Renovate)",
|
|
122
|
+
body: "Adds the unified CI gate, nightly Renovate, and auto-merge for patch/minor updates."
|
|
123
|
+
});
|
|
124
|
+
actions.push(`opened PR ${pr.url}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!await github.autoMergeEnabled(repo)) {
|
|
128
|
+
await github.enableRepoAutoMerge(repo);
|
|
129
|
+
actions.push("enabled auto-merge");
|
|
130
|
+
}
|
|
131
|
+
const existingContexts = await github.branchProtectionContexts(repo, base);
|
|
132
|
+
if (!existingContexts.includes(REQUIRED_CHECK)) {
|
|
133
|
+
const contexts = [.../* @__PURE__ */ new Set([...existingContexts, REQUIRED_CHECK])];
|
|
134
|
+
await github.protectBranch(repo, base, contexts);
|
|
135
|
+
actions.push(`required "${REQUIRED_CHECK}" check on ${base}`);
|
|
136
|
+
}
|
|
137
|
+
if (!await github.secretExists(repo, "RENOVATE_TOKEN")) {
|
|
138
|
+
await github.setRepoSecret(repo, "RENOVATE_TOKEN", renovateToken);
|
|
139
|
+
actions.push("set RENOVATE_TOKEN secret");
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
const done = actions.length ? ` (completed: ${actions.join("; ")})` : "";
|
|
143
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
144
|
+
return resultOf(site, "failed", `${message}${done}`, commits);
|
|
145
|
+
} finally {
|
|
146
|
+
if (maintBranch !== null && original !== null && original !== maintBranch) {
|
|
147
|
+
try {
|
|
148
|
+
await checkoutBranch(site.path, original);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.warn(
|
|
151
|
+
`warning: could not restore branch ${original} after self-updating: ${err instanceof Error ? err.message : String(err)}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return actions.length ? resultOf(site, "applied", actions.join("; "), commits) : resultOf(site, "noop", "already self-updating", commits);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export {
|
|
160
|
+
selfUpdating
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=chunk-PF6BNYGE.js.map
|