davaux 0.8.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.
Files changed (333) hide show
  1. package/BASELINE.md +169 -0
  2. package/CLAUDE.md +518 -0
  3. package/LICENSE +21 -0
  4. package/README.md +36 -0
  5. package/ROADMAP.md +198 -0
  6. package/build.mjs +101 -0
  7. package/client/control.ts +247 -0
  8. package/client/hydrate.ts +37 -0
  9. package/client/index.ts +19 -0
  10. package/client/jsx-runtime.ts +209 -0
  11. package/client/resource.ts +122 -0
  12. package/client/signal.ts +211 -0
  13. package/client/store.ts +110 -0
  14. package/client/useHead.ts +63 -0
  15. package/dist/build/config.d.ts +3 -0
  16. package/dist/build/config.d.ts.map +1 -0
  17. package/dist/build/config.js +38 -0
  18. package/dist/build/config.js.map +7 -0
  19. package/dist/build/index.d.ts +2 -0
  20. package/dist/build/index.d.ts.map +1 -0
  21. package/dist/build/index.js +13 -0
  22. package/dist/build/index.js.map +7 -0
  23. package/dist/build/plugins.d.ts +7 -0
  24. package/dist/build/plugins.d.ts.map +1 -0
  25. package/dist/build/plugins.js +85 -0
  26. package/dist/build/plugins.js.map +7 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +427 -0
  30. package/dist/cli.js.map +7 -0
  31. package/dist/client/control.d.ts +49 -0
  32. package/dist/client/control.d.ts.map +1 -0
  33. package/dist/client/control.js +154 -0
  34. package/dist/client/control.js.map +7 -0
  35. package/dist/client/hydrate.d.ts +7 -0
  36. package/dist/client/hydrate.d.ts.map +1 -0
  37. package/dist/client/hydrate.js +23 -0
  38. package/dist/client/hydrate.js.map +7 -0
  39. package/dist/client/index.d.ts +12 -0
  40. package/dist/client/index.d.ts.map +1 -0
  41. package/dist/client/index.js +32 -0
  42. package/dist/client/index.js.map +7 -0
  43. package/dist/client/jsx-runtime.d.ts +40 -0
  44. package/dist/client/jsx-runtime.d.ts.map +1 -0
  45. package/dist/client/jsx-runtime.js +139 -0
  46. package/dist/client/jsx-runtime.js.map +7 -0
  47. package/dist/client/resource.d.ts +31 -0
  48. package/dist/client/resource.d.ts.map +1 -0
  49. package/dist/client/resource.js +64 -0
  50. package/dist/client/resource.js.map +7 -0
  51. package/dist/client/signal.d.ts +90 -0
  52. package/dist/client/signal.d.ts.map +1 -0
  53. package/dist/client/signal.js +115 -0
  54. package/dist/client/signal.js.map +7 -0
  55. package/dist/client/store.d.ts +26 -0
  56. package/dist/client/store.d.ts.map +1 -0
  57. package/dist/client/store.js +63 -0
  58. package/dist/client/store.js.map +7 -0
  59. package/dist/client/useHead.d.ts +28 -0
  60. package/dist/client/useHead.d.ts.map +1 -0
  61. package/dist/client/useHead.js +33 -0
  62. package/dist/client/useHead.js.map +7 -0
  63. package/dist/config.d.ts +182 -0
  64. package/dist/config.d.ts.map +1 -0
  65. package/dist/config.js +21 -0
  66. package/dist/config.js.map +7 -0
  67. package/dist/create-multisite.d.ts +2 -0
  68. package/dist/create-multisite.d.ts.map +1 -0
  69. package/dist/create-multisite.js +291 -0
  70. package/dist/create-multisite.js.map +7 -0
  71. package/dist/create.d.ts +2 -0
  72. package/dist/create.d.ts.map +1 -0
  73. package/dist/create.js +179 -0
  74. package/dist/create.js.map +7 -0
  75. package/dist/dev/blueprints.d.ts +11 -0
  76. package/dist/dev/blueprints.d.ts.map +1 -0
  77. package/dist/dev/blueprints.js +65 -0
  78. package/dist/dev/blueprints.js.map +7 -0
  79. package/dist/dev/components.d.ts +19 -0
  80. package/dist/dev/components.d.ts.map +1 -0
  81. package/dist/dev/components.js +87 -0
  82. package/dist/dev/components.js.map +7 -0
  83. package/dist/dev/insert.d.ts +11 -0
  84. package/dist/dev/insert.d.ts.map +1 -0
  85. package/dist/dev/insert.js +160 -0
  86. package/dist/dev/insert.js.map +7 -0
  87. package/dist/dev/remove.d.ts +53 -0
  88. package/dist/dev/remove.d.ts.map +1 -0
  89. package/dist/dev/remove.js +518 -0
  90. package/dist/dev/remove.js.map +7 -0
  91. package/dist/dev/watch.d.ts +26 -0
  92. package/dist/dev/watch.d.ts.map +1 -0
  93. package/dist/dev/watch.js +2905 -0
  94. package/dist/dev/watch.js.map +7 -0
  95. package/dist/errors.d.ts +6 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +63 -0
  98. package/dist/errors.js.map +7 -0
  99. package/dist/generate.d.ts +2 -0
  100. package/dist/generate.d.ts.map +1 -0
  101. package/dist/generate.js +191 -0
  102. package/dist/generate.js.map +7 -0
  103. package/dist/index.d.ts +9 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +57 -0
  106. package/dist/index.js.map +7 -0
  107. package/dist/island.d.ts +24 -0
  108. package/dist/island.d.ts.map +1 -0
  109. package/dist/island.js +15 -0
  110. package/dist/island.js.map +7 -0
  111. package/dist/jsx-runtime.d.ts +406 -0
  112. package/dist/jsx-runtime.d.ts.map +1 -0
  113. package/dist/jsx-runtime.js +90 -0
  114. package/dist/jsx-runtime.js.map +7 -0
  115. package/dist/link.d.ts +27 -0
  116. package/dist/link.d.ts.map +1 -0
  117. package/dist/link.js +29 -0
  118. package/dist/link.js.map +7 -0
  119. package/dist/oml/fragment.d.ts +16 -0
  120. package/dist/oml/fragment.d.ts.map +1 -0
  121. package/dist/oml/fragment.js +26 -0
  122. package/dist/oml/fragment.js.map +7 -0
  123. package/dist/oml/index.d.ts +11 -0
  124. package/dist/oml/index.d.ts.map +1 -0
  125. package/dist/oml/index.js +21 -0
  126. package/dist/oml/index.js.map +7 -0
  127. package/dist/oml/jsx-runtime.d.ts +34 -0
  128. package/dist/oml/jsx-runtime.d.ts.map +1 -0
  129. package/dist/oml/jsx-runtime.js +59 -0
  130. package/dist/oml/jsx-runtime.js.map +7 -0
  131. package/dist/oml/jsx.d.ts +14 -0
  132. package/dist/oml/jsx.d.ts.map +1 -0
  133. package/dist/oml/jsx.js +96 -0
  134. package/dist/oml/jsx.js.map +7 -0
  135. package/dist/oml/page.d.ts +7 -0
  136. package/dist/oml/page.d.ts.map +1 -0
  137. package/dist/oml/page.js +6 -0
  138. package/dist/oml/page.js.map +7 -0
  139. package/dist/oml/render.d.ts +13 -0
  140. package/dist/oml/render.d.ts.map +1 -0
  141. package/dist/oml/render.js +117 -0
  142. package/dist/oml/render.js.map +7 -0
  143. package/dist/oml/types.d.ts +79 -0
  144. package/dist/oml/types.d.ts.map +1 -0
  145. package/dist/oml/types.js +64 -0
  146. package/dist/oml/types.js.map +7 -0
  147. package/dist/router/handler.d.ts +53 -0
  148. package/dist/router/handler.d.ts.map +1 -0
  149. package/dist/router/handler.js +342 -0
  150. package/dist/router/handler.js.map +7 -0
  151. package/dist/router/matcher.d.ts +21 -0
  152. package/dist/router/matcher.d.ts.map +1 -0
  153. package/dist/router/matcher.js +28 -0
  154. package/dist/router/matcher.js.map +7 -0
  155. package/dist/router/scanner.d.ts +17 -0
  156. package/dist/router/scanner.d.ts.map +1 -0
  157. package/dist/router/scanner.js +197 -0
  158. package/dist/router/scanner.js.map +7 -0
  159. package/dist/server/index.d.ts +23 -0
  160. package/dist/server/index.d.ts.map +1 -0
  161. package/dist/server/index.js +29 -0
  162. package/dist/server/index.js.map +7 -0
  163. package/dist/signal.d.ts +15 -0
  164. package/dist/signal.d.ts.map +1 -0
  165. package/dist/signal.js +29 -0
  166. package/dist/signal.js.map +7 -0
  167. package/dist/ssg.d.ts +45 -0
  168. package/dist/ssg.d.ts.map +1 -0
  169. package/dist/ssg.js +175 -0
  170. package/dist/ssg.js.map +7 -0
  171. package/dist/test/actions.test.d.ts +2 -0
  172. package/dist/test/actions.test.d.ts.map +1 -0
  173. package/dist/test/body-limits.test.d.ts +2 -0
  174. package/dist/test/body-limits.test.d.ts.map +1 -0
  175. package/dist/test/errors.test.d.ts +2 -0
  176. package/dist/test/errors.test.d.ts.map +1 -0
  177. package/dist/test/fixtures/routes/[id].page.d.ts +4 -0
  178. package/dist/test/fixtures/routes/[id].page.d.ts.map +1 -0
  179. package/dist/test/fixtures/routes/_error.d.ts +3 -0
  180. package/dist/test/fixtures/routes/_error.d.ts.map +1 -0
  181. package/dist/test/fixtures/routes/_global.d.ts +3 -0
  182. package/dist/test/fixtures/routes/_global.d.ts.map +1 -0
  183. package/dist/test/fixtures/routes/_layout-template.d.ts +3 -0
  184. package/dist/test/fixtures/routes/_layout-template.d.ts.map +1 -0
  185. package/dist/test/fixtures/routes/_layout.d.ts +3 -0
  186. package/dist/test/fixtures/routes/_layout.d.ts.map +1 -0
  187. package/dist/test/fixtures/routes/_layout_scripts.d.ts +3 -0
  188. package/dist/test/fixtures/routes/_layout_scripts.d.ts.map +1 -0
  189. package/dist/test/fixtures/routes/_middleware.d.ts +3 -0
  190. package/dist/test/fixtures/routes/_middleware.d.ts.map +1 -0
  191. package/dist/test/fixtures/routes/_redirect301_mw.d.ts +3 -0
  192. package/dist/test/fixtures/routes/_redirect301_mw.d.ts.map +1 -0
  193. package/dist/test/fixtures/routes/_redirect_mw.d.ts +3 -0
  194. package/dist/test/fixtures/routes/_redirect_mw.d.ts.map +1 -0
  195. package/dist/test/fixtures/routes/about.page.d.ts +3 -0
  196. package/dist/test/fixtures/routes/about.page.d.ts.map +1 -0
  197. package/dist/test/fixtures/routes/action.page.d.ts +6 -0
  198. package/dist/test/fixtures/routes/action.page.d.ts.map +1 -0
  199. package/dist/test/fixtures/routes/api/form-all.post.d.ts +3 -0
  200. package/dist/test/fixtures/routes/api/form-all.post.d.ts.map +1 -0
  201. package/dist/test/fixtures/routes/api/form-limited.post.d.ts +6 -0
  202. package/dist/test/fixtures/routes/api/form-limited.post.d.ts.map +1 -0
  203. package/dist/test/fixtures/routes/api/response-obj.get.d.ts +3 -0
  204. package/dist/test/fixtures/routes/api/response-obj.get.d.ts.map +1 -0
  205. package/dist/test/fixtures/routes/api/upload.post.d.ts +12 -0
  206. package/dist/test/fixtures/routes/api/upload.post.d.ts.map +1 -0
  207. package/dist/test/fixtures/routes/api/users.get.d.ts +6 -0
  208. package/dist/test/fixtures/routes/api/users.get.d.ts.map +1 -0
  209. package/dist/test/fixtures/routes/api/xml.get.d.ts +3 -0
  210. package/dist/test/fixtures/routes/api/xml.get.d.ts.map +1 -0
  211. package/dist/test/fixtures/routes/auth/_middleware.d.ts +3 -0
  212. package/dist/test/fixtures/routes/auth/_middleware.d.ts.map +1 -0
  213. package/dist/test/fixtures/routes/auth/protected.page.d.ts +3 -0
  214. package/dist/test/fixtures/routes/auth/protected.page.d.ts.map +1 -0
  215. package/dist/test/fixtures/routes/index.page.d.ts +3 -0
  216. package/dist/test/fixtures/routes/index.page.d.ts.map +1 -0
  217. package/dist/test/fixtures/routes/oml.page.d.ts +3 -0
  218. package/dist/test/fixtures/routes/oml.page.d.ts.map +1 -0
  219. package/dist/test/fixtures/routes/redirect.page.d.ts +3 -0
  220. package/dist/test/fixtures/routes/redirect.page.d.ts.map +1 -0
  221. package/dist/test/fixtures/routes/ssg/[slug].page.d.ts +5 -0
  222. package/dist/test/fixtures/routes/ssg/[slug].page.d.ts.map +1 -0
  223. package/dist/test/fixtures/routes/ssg/server.page.d.ts +4 -0
  224. package/dist/test/fixtures/routes/ssg/server.page.d.ts.map +1 -0
  225. package/dist/test/fixtures/routes/state.page.d.ts +3 -0
  226. package/dist/test/fixtures/routes/state.page.d.ts.map +1 -0
  227. package/dist/test/fixtures/routes/throw.page.d.ts +3 -0
  228. package/dist/test/fixtures/routes/throw.page.d.ts.map +1 -0
  229. package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts +3 -0
  230. package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts.map +1 -0
  231. package/dist/test/helpers.d.ts +37 -0
  232. package/dist/test/helpers.d.ts.map +1 -0
  233. package/dist/test/layouts.test.d.ts +2 -0
  234. package/dist/test/layouts.test.d.ts.map +1 -0
  235. package/dist/test/middleware.test.d.ts +2 -0
  236. package/dist/test/middleware.test.d.ts.map +1 -0
  237. package/dist/test/multipart.test.d.ts +2 -0
  238. package/dist/test/multipart.test.d.ts.map +1 -0
  239. package/dist/test/oml-routing.test.d.ts +2 -0
  240. package/dist/test/oml-routing.test.d.ts.map +1 -0
  241. package/dist/test/oml.test.d.ts +2 -0
  242. package/dist/test/oml.test.d.ts.map +1 -0
  243. package/dist/test/redirects.test.d.ts +2 -0
  244. package/dist/test/redirects.test.d.ts.map +1 -0
  245. package/dist/test/routing.test.d.ts +2 -0
  246. package/dist/test/routing.test.d.ts.map +1 -0
  247. package/dist/test/ssg.test.d.ts +2 -0
  248. package/dist/test/ssg.test.d.ts.map +1 -0
  249. package/dist/test/web-response.test.d.ts +2 -0
  250. package/dist/test/web-response.test.d.ts.map +1 -0
  251. package/dist/types.d.ts +314 -0
  252. package/dist/types.d.ts.map +1 -0
  253. package/dist/types.js +292 -0
  254. package/dist/types.js.map +7 -0
  255. package/package.json +103 -0
  256. package/pka.config.json +32 -0
  257. package/src/build/config.ts +42 -0
  258. package/src/build/index.ts +6 -0
  259. package/src/build/plugins.ts +118 -0
  260. package/src/cli.ts +502 -0
  261. package/src/config.ts +197 -0
  262. package/src/create-multisite.ts +310 -0
  263. package/src/create.ts +194 -0
  264. package/src/dev/blueprints.ts +75 -0
  265. package/src/dev/components.ts +108 -0
  266. package/src/dev/insert.ts +221 -0
  267. package/src/dev/remove.ts +677 -0
  268. package/src/dev/watch.ts +3098 -0
  269. package/src/env.d.ts +5 -0
  270. package/src/errors.ts +64 -0
  271. package/src/generate.ts +228 -0
  272. package/src/index.ts +67 -0
  273. package/src/island.ts +47 -0
  274. package/src/jsx-runtime.d.ts +408 -0
  275. package/src/jsx-runtime.d.ts.map +1 -0
  276. package/src/jsx-runtime.ts +536 -0
  277. package/src/link.ts +49 -0
  278. package/src/oml/fragment.ts +54 -0
  279. package/src/oml/index.ts +21 -0
  280. package/src/oml/jsx-runtime.ts +121 -0
  281. package/src/oml/jsx.ts +151 -0
  282. package/src/oml/page.ts +13 -0
  283. package/src/oml/render.ts +181 -0
  284. package/src/oml/types.ts +159 -0
  285. package/src/router/handler.ts +515 -0
  286. package/src/router/matcher.ts +52 -0
  287. package/src/router/scanner.ts +272 -0
  288. package/src/server/index.ts +49 -0
  289. package/src/signal.ts +39 -0
  290. package/src/ssg.ts +253 -0
  291. package/src/test/actions.test.ts +40 -0
  292. package/src/test/body-limits.test.ts +83 -0
  293. package/src/test/errors.test.ts +53 -0
  294. package/src/test/fixtures/routes/[id].page.ts +3 -0
  295. package/src/test/fixtures/routes/_error.ts +6 -0
  296. package/src/test/fixtures/routes/_global.ts +8 -0
  297. package/src/test/fixtures/routes/_layout-template.ts +7 -0
  298. package/src/test/fixtures/routes/_layout.ts +7 -0
  299. package/src/test/fixtures/routes/_layout_scripts.ts +8 -0
  300. package/src/test/fixtures/routes/_middleware.ts +8 -0
  301. package/src/test/fixtures/routes/_redirect301_mw.ts +5 -0
  302. package/src/test/fixtures/routes/_redirect_mw.ts +5 -0
  303. package/src/test/fixtures/routes/about.page.ts +6 -0
  304. package/src/test/fixtures/routes/action.page.ts +11 -0
  305. package/src/test/fixtures/routes/api/form-all.post.ts +5 -0
  306. package/src/test/fixtures/routes/api/form-limited.post.ts +6 -0
  307. package/src/test/fixtures/routes/api/response-obj.get.ts +17 -0
  308. package/src/test/fixtures/routes/api/upload.post.ts +14 -0
  309. package/src/test/fixtures/routes/api/users.get.ts +3 -0
  310. package/src/test/fixtures/routes/api/xml.get.ts +5 -0
  311. package/src/test/fixtures/routes/auth/_middleware.ts +11 -0
  312. package/src/test/fixtures/routes/auth/protected.page.ts +3 -0
  313. package/src/test/fixtures/routes/index.page.ts +3 -0
  314. package/src/test/fixtures/routes/oml.page.ts +7 -0
  315. package/src/test/fixtures/routes/redirect.page.ts +3 -0
  316. package/src/test/fixtures/routes/ssg/[slug].page.ts +8 -0
  317. package/src/test/fixtures/routes/ssg/server.page.ts +5 -0
  318. package/src/test/fixtures/routes/state.page.ts +4 -0
  319. package/src/test/fixtures/routes/throw.page.ts +5 -0
  320. package/src/test/fixtures/routes/wiki/[...slug].page.ts +3 -0
  321. package/src/test/helpers.ts +132 -0
  322. package/src/test/layouts.test.ts +76 -0
  323. package/src/test/middleware.test.ts +69 -0
  324. package/src/test/multipart.test.ts +91 -0
  325. package/src/test/oml-routing.test.ts +59 -0
  326. package/src/test/oml.test.ts +429 -0
  327. package/src/test/redirects.test.ts +32 -0
  328. package/src/test/routing.test.ts +118 -0
  329. package/src/test/ssg.test.ts +273 -0
  330. package/src/test/web-response.test.ts +33 -0
  331. package/src/types.ts +670 -0
  332. package/tsconfig.client.json +17 -0
  333. package/tsconfig.json +20 -0
@@ -0,0 +1,677 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+
3
+ export type RemoveResult = { removed: true; file: string } | { removed: false; error: string }
4
+
5
+ function findComponentSpan(lines: string[], name: string, startFrom = 0): [number, number] | null {
6
+ const openRe = new RegExp(`^\\s*<${name}(\\s|/>|>|$)`)
7
+ let openIdx = -1
8
+ for (let i = startFrom; i < lines.length; i++) {
9
+ if (openRe.test(lines[i])) {
10
+ openIdx = i
11
+ break
12
+ }
13
+ }
14
+ if (openIdx === -1) return null
15
+
16
+ const openIndent = (lines[openIdx].match(/^(\s*)/) ?? ['', ''])[1]
17
+ const tagStart = lines[openIdx].indexOf(`<${name}`)
18
+ const rest = lines[openIdx].slice(tagStart)
19
+
20
+ // Single-line self-close or same-line children close
21
+ if (/\/>/.test(rest) || new RegExp(`</${name}>`).test(rest)) return [openIdx, openIdx]
22
+
23
+ // Multi-line: scan forward for /> or </Name> at same/shallower indent
24
+ for (let i = openIdx + 1; i < lines.length; i++) {
25
+ const t = lines[i].trim()
26
+ const ind = (lines[i].match(/^(\s*)/) ?? ['', ''])[1]
27
+ if ((t === '/>' || t.startsWith(`</${name}>`)) && ind.length <= openIndent.length + 2) {
28
+ return [openIdx, i]
29
+ }
30
+ // Bail if back to same/shallower indent on a non-close, non-blank line
31
+ if (t && ind.length <= openIndent.length && i > openIdx + 1) break
32
+ }
33
+
34
+ return [openIdx, openIdx]
35
+ }
36
+
37
+ export function removeJsx(filePath: string, componentName: string, instanceIndex?: number): RemoveResult {
38
+ let source: string
39
+ try {
40
+ source = readFileSync(filePath, 'utf-8')
41
+ } catch {
42
+ return { removed: false, error: `Cannot read file: ${filePath}` }
43
+ }
44
+
45
+ const lines = source.split('\n')
46
+
47
+ // Collect all spans for this component
48
+ const spans: [number, number][] = []
49
+ let from = 0
50
+ while (true) {
51
+ const span = findComponentSpan(lines, componentName, from)
52
+ if (!span) break
53
+ spans.push(span)
54
+ from = span[1] + 1
55
+ }
56
+
57
+ if (spans.length === 0) {
58
+ return { removed: false, error: `No <${componentName}> found in source.` }
59
+ }
60
+ const idx = instanceIndex ?? (spans.length === 1 ? 0 : -1)
61
+ if (idx === -1) {
62
+ return {
63
+ removed: false,
64
+ error: `${spans.length} instances of <${componentName}> found — click the specific one you want to remove, or edit the file directly.`,
65
+ }
66
+ }
67
+ if (idx >= spans.length) {
68
+ return { removed: false, error: `Instance ${idx} not found (${spans.length} instances exist).` }
69
+ }
70
+
71
+ const [start, end] = spans[idx]
72
+ lines.splice(start, end - start + 1)
73
+
74
+ // Collapse consecutive blank lines left by the removal
75
+ const cleaned: string[] = []
76
+ let prevBlank = false
77
+ for (const line of lines) {
78
+ const blank = line.trim() === ''
79
+ if (blank && prevBlank) continue
80
+ cleaned.push(line)
81
+ prevBlank = blank
82
+ }
83
+
84
+ try {
85
+ writeFileSync(filePath, cleaned.join('\n'), 'utf-8')
86
+ } catch {
87
+ return { removed: false, error: `Cannot write file: ${filePath}` }
88
+ }
89
+
90
+ return { removed: true, file: filePath }
91
+ }
92
+
93
+ export function removeElement(filePath: string, tagName: string, instanceIndex: number): RemoveResult {
94
+ let source: string
95
+ try {
96
+ source = readFileSync(filePath, 'utf-8')
97
+ } catch {
98
+ return { removed: false, error: `Cannot read file: ${filePath}` }
99
+ }
100
+
101
+ const lines = source.split('\n')
102
+ const span = findFullElementSpan(lines, tagName, instanceIndex)
103
+ if (!span) return { removed: false, error: `No <${tagName}> found at index ${instanceIndex}` }
104
+
105
+ const [start, end] = span
106
+ lines.splice(start, end - start + 1)
107
+
108
+ const cleaned: string[] = []
109
+ let prevBlank = false
110
+ for (const line of lines) {
111
+ const blank = line.trim() === ''
112
+ if (blank && prevBlank) continue
113
+ cleaned.push(line)
114
+ prevBlank = blank
115
+ }
116
+
117
+ try {
118
+ writeFileSync(filePath, cleaned.join('\n'), 'utf-8')
119
+ } catch {
120
+ return { removed: false, error: `Cannot write file: ${filePath}` }
121
+ }
122
+
123
+ return { removed: true, file: filePath }
124
+ }
125
+
126
+ export type ReplaceResult = { replaced: true; file: string } | { replaced: false; error: string }
127
+
128
+ export type ReplaceElemResult = { replaced: true; file: string } | { replaced: false; error: string }
129
+
130
+ // Find the span (start line, end line) of the Nth opening tag for `tagName`.
131
+ // Matches <tag>, <tag />, <tag attr>, but NOT </tag>.
132
+ function findElementTagSpan(lines: string[], tagName: string, instanceIndex: number): [number, number] | null {
133
+ const openRe = new RegExp(`<${tagName}(\\s|>|\\/)`)
134
+ let count = 0
135
+ for (let i = 0; i < lines.length; i++) {
136
+ if (openRe.test(lines[i])) {
137
+ if (count === instanceIndex) {
138
+ // If the opening tag closes on the same line, it's a one-liner
139
+ const tagPos = lines[i].search(openRe)
140
+ const rest = lines[i].slice(tagPos)
141
+ if (rest.includes('>')) return [i, i]
142
+ // Multi-line: scan forward for a line that is just > or />
143
+ for (let j = i + 1; j < lines.length; j++) {
144
+ const t = lines[j].trim()
145
+ if (t === '>' || t === '/>') return [i, j]
146
+ }
147
+ return [i, i]
148
+ }
149
+ count++
150
+ }
151
+ }
152
+ return null
153
+ }
154
+
155
+ function buildOpenTag(tagName: string, props: Record<string, unknown>, selfClose: boolean): string {
156
+ const entries = Object.entries(props).filter(([, v]) => v !== undefined && v !== null)
157
+ if (!entries.length) return selfClose ? `<${tagName} />` : `<${tagName}>`
158
+ const attrs = entries
159
+ .map(([k, v]) => {
160
+ if (v === true) return k
161
+ if (v === false) return null
162
+ if (typeof v === 'string') return `${k}="${v}"`
163
+ return `${k}={${JSON.stringify(v)}}`
164
+ })
165
+ .filter(Boolean)
166
+ .join(' ')
167
+ return attrs ? (selfClose ? `<${tagName} ${attrs} />` : `<${tagName} ${attrs}>`) : (selfClose ? `<${tagName} />` : `<${tagName}>`)
168
+ }
169
+
170
+ export function replaceElementAttrs(
171
+ filePath: string,
172
+ tagName: string,
173
+ newProps: Record<string, unknown>,
174
+ instanceIndex = 0,
175
+ ): ReplaceElemResult {
176
+ let source: string
177
+ try {
178
+ source = readFileSync(filePath, 'utf-8')
179
+ } catch {
180
+ return { replaced: false, error: `Cannot read file: ${filePath}` }
181
+ }
182
+
183
+ const lines = source.split('\n')
184
+ const span = findElementTagSpan(lines, tagName, instanceIndex)
185
+ if (!span) return { replaced: false, error: `No <${tagName}> found at index ${instanceIndex}` }
186
+
187
+ const [startLine, endLine] = span
188
+ const newLines = [...lines]
189
+ const openRe = new RegExp(`<${tagName}(\\s|>|\\/)`)
190
+
191
+ if (startLine === endLine) {
192
+ const line = lines[startLine]
193
+ const tagPos = line.search(openRe)
194
+ const before = line.slice(0, tagPos)
195
+ // Scan for closing > of the opening tag (simple scan — no nested > expected in common attrs)
196
+ let closePos = -1
197
+ let selfClose = false
198
+ for (let i = tagPos + tagName.length + 1; i < line.length; i++) {
199
+ if (line[i] === '>') {
200
+ if (line[i - 1] === '/') {
201
+ selfClose = true
202
+ closePos = i - 1
203
+ } else {
204
+ closePos = i
205
+ }
206
+ break
207
+ }
208
+ }
209
+ if (closePos === -1) return { replaced: false, error: `Cannot parse opening tag of <${tagName}>` }
210
+ const after = line.slice(selfClose ? closePos + 2 : closePos + 1)
211
+ newLines[startLine] = before + buildOpenTag(tagName, newProps, selfClose) + after
212
+ } else {
213
+ const firstLine = lines[startLine]
214
+ const tagPos = firstLine.search(openRe)
215
+ const before = firstLine.slice(0, tagPos)
216
+ const lastLine = lines[endLine]
217
+ const selfClose = lastLine.trim() === '/>'
218
+ const closeIdx = lastLine.indexOf(selfClose ? '/>' : '>')
219
+ const after = lastLine.slice(closeIdx + (selfClose ? 2 : 1))
220
+ newLines.splice(startLine, endLine - startLine + 1, before + buildOpenTag(tagName, newProps, selfClose) + after)
221
+ }
222
+
223
+ try {
224
+ writeFileSync(filePath, newLines.join('\n'), 'utf-8')
225
+ } catch {
226
+ return { replaced: false, error: `Cannot write file: ${filePath}` }
227
+ }
228
+
229
+ return { replaced: true, file: filePath }
230
+ }
231
+
232
+ // Replaces the inner text content of an element on a single line.
233
+ // e.g. <h1>Old text</h1> → <h1>New text</h1>
234
+ // Only works when the opening tag and closing tag are on the same line.
235
+ export function replaceTextContent(
236
+ filePath: string,
237
+ tagName: string,
238
+ newText: string,
239
+ instanceIndex = 0,
240
+ ): ReplaceElemResult {
241
+ let source: string
242
+ try {
243
+ source = readFileSync(filePath, 'utf-8')
244
+ } catch {
245
+ return { replaced: false, error: `Cannot read file: ${filePath}` }
246
+ }
247
+
248
+ const lines = source.split('\n')
249
+ const span = findElementTagSpan(lines, tagName, instanceIndex)
250
+ if (!span) return { replaced: false, error: `No <${tagName}> found at index ${instanceIndex}` }
251
+
252
+ const [startLine, endLine] = span
253
+ const contentLine = endLine
254
+ const line = lines[contentLine]
255
+
256
+ const closeTag = `</${tagName}>`
257
+ const closeIdx = line.indexOf(closeTag)
258
+ if (closeIdx === -1) {
259
+ return { replaced: false, error: `<${tagName}> has multi-line content — not yet supported` }
260
+ }
261
+
262
+ // Find the end of the opening tag (simple > scan — no nested > expected in common attrs)
263
+ const openRe = new RegExp(`<${tagName}(\\s|>|\\/)`)
264
+ const tagStart = startLine === endLine ? line.search(openRe) : 0
265
+ let openEnd = -1
266
+ for (let i = tagStart; i < closeIdx; i++) {
267
+ if (line[i] === '>') {
268
+ openEnd = i
269
+ break
270
+ }
271
+ }
272
+ if (openEnd === -1) return { replaced: false, error: `Cannot parse opening tag of <${tagName}>` }
273
+
274
+ lines[contentLine] = line.slice(0, openEnd + 1) + newText + line.slice(closeIdx)
275
+
276
+ try {
277
+ writeFileSync(filePath, lines.join('\n'), 'utf-8')
278
+ } catch {
279
+ return { replaced: false, error: `Cannot write file: ${filePath}` }
280
+ }
281
+
282
+ return { replaced: true, file: filePath }
283
+ }
284
+
285
+ export function replaceJsx(
286
+ filePath: string,
287
+ componentName: string,
288
+ newJsx: string,
289
+ instanceIndex = 0,
290
+ ): ReplaceResult {
291
+ let source: string
292
+ try {
293
+ source = readFileSync(filePath, 'utf-8')
294
+ } catch {
295
+ return { replaced: false, error: `Cannot read file: ${filePath}` }
296
+ }
297
+
298
+ const lines = source.split('\n')
299
+ const spans: [number, number][] = []
300
+ let from = 0
301
+ while (true) {
302
+ const span = findComponentSpan(lines, componentName, from)
303
+ if (!span) break
304
+ spans.push(span)
305
+ from = span[1] + 1
306
+ }
307
+
308
+ if (spans.length === 0) {
309
+ return { replaced: false, error: `No <${componentName}> found in source.` }
310
+ }
311
+ if (instanceIndex >= spans.length) {
312
+ return {
313
+ replaced: false,
314
+ error: `Instance index ${instanceIndex} out of range (${spans.length} found).`,
315
+ }
316
+ }
317
+
318
+ const [start, end] = spans[instanceIndex]
319
+ const existingIndent = (lines[start].match(/^(\s*)/) ?? ['', ''])[1]
320
+ // Prepend existing indent to each line; newJsx lines already carry their own relative indent
321
+ const newLines = newJsx.split('\n').map((l) => (l === '' ? '' : existingIndent + l))
322
+
323
+ lines.splice(start, end - start + 1, ...newLines)
324
+
325
+ try {
326
+ writeFileSync(filePath, lines.join('\n'), 'utf-8')
327
+ } catch {
328
+ return { replaced: false, error: `Cannot write file: ${filePath}` }
329
+ }
330
+
331
+ return { replaced: true, file: filePath }
332
+ }
333
+
334
+ // Full element span: opening <tag through matching </tag> or />, using indent tracking.
335
+ function findFullElementSpan(lines: string[], tagName: string, instanceIndex: number): [number, number] | null {
336
+ const openRe = new RegExp(`<${tagName}(\\s|>|\\/)`)
337
+ let count = 0
338
+ for (let i = 0; i < lines.length; i++) {
339
+ if (openRe.test(lines[i])) {
340
+ if (count === instanceIndex) {
341
+ const tagPos = lines[i].search(openRe)
342
+ const openIndent = (lines[i].match(/^(\s*)/) ?? ['', ''])[1]
343
+ const rest = lines[i].slice(tagPos)
344
+ if (/\/>/.test(rest) || new RegExp(`</${tagName}>`).test(rest)) return [i, i]
345
+ for (let j = i + 1; j < lines.length; j++) {
346
+ const t = lines[j].trim()
347
+ const ind = (lines[j].match(/^(\s*)/) ?? ['', ''])[1]
348
+ if (t.startsWith(`</${tagName}>`) && ind.length <= openIndent.length) return [i, j]
349
+ if (t && ind.length < openIndent.length) break
350
+ }
351
+ return [i, i]
352
+ }
353
+ count++
354
+ }
355
+ }
356
+ return null
357
+ }
358
+
359
+ export type FragmentResult =
360
+ | { found: true; content: string; file: string; name: string; instanceIndex: number }
361
+ | { found: false; error: string }
362
+
363
+ /** Finds the content between a component's opening > and closing </Name>. */
364
+ export function getComponentFragment(
365
+ filePath: string,
366
+ name: string,
367
+ instanceIndex: number,
368
+ ): FragmentResult {
369
+ let source: string
370
+ try {
371
+ source = readFileSync(filePath, 'utf-8')
372
+ } catch {
373
+ return { found: false, error: `Cannot read file: ${filePath}` }
374
+ }
375
+ const lines = source.split('\n')
376
+
377
+ // Collect all spans for this component
378
+ const spans: [number, number][] = []
379
+ let from = 0
380
+ while (true) {
381
+ const span = findComponentSpan(lines, name, from)
382
+ if (!span) break
383
+ spans.push(span)
384
+ from = span[1] + 1
385
+ }
386
+ if (instanceIndex >= spans.length)
387
+ return { found: false, error: `Instance ${instanceIndex} not found (${spans.length} instances exist)` }
388
+
389
+ const [spanStart, spanEnd] = spans[instanceIndex]
390
+ const startLine = lines[spanStart]
391
+ const tagStart = startLine.indexOf(`<${name}`)
392
+
393
+ // Self-closing — no children
394
+ if (/\/>/.test(startLine.slice(tagStart)))
395
+ return { found: false, error: `<${name}> is self-closing` }
396
+ if (spanStart === spanEnd)
397
+ return { found: false, error: `<${name}> opens and closes on the same line` }
398
+
399
+ // Find where the opening tag ends (the > that closes it)
400
+ const { line: openEndLine, col: openEndCol } = findOpenTagEnd(lines, spanStart, name)
401
+ if (openEndCol === -1)
402
+ return { found: false, error: `Cannot find end of opening tag for <${name}>` }
403
+
404
+ // Content = from after > on openEndLine up to (not including) the closing tag line
405
+ const contentLines: string[] = []
406
+ const afterOpen = lines[openEndLine].slice(openEndCol + 1)
407
+ if (afterOpen.trim()) contentLines.push(afterOpen)
408
+ for (let i = openEndLine + 1; i < spanEnd; i++) contentLines.push(lines[i])
409
+
410
+ return { found: true, content: contentLines.join('\n'), file: filePath, name, instanceIndex }
411
+ }
412
+
413
+ /** Replaces the children content of a component, preserving its opening and closing tags. */
414
+ export function replaceComponentFragment(
415
+ filePath: string,
416
+ name: string,
417
+ instanceIndex: number,
418
+ newContent: string,
419
+ ): ReplaceResult {
420
+ let source: string
421
+ try {
422
+ source = readFileSync(filePath, 'utf-8')
423
+ } catch {
424
+ return { replaced: false, error: `Cannot read file: ${filePath}` }
425
+ }
426
+ const lines = source.split('\n')
427
+
428
+ const spans: [number, number][] = []
429
+ let from = 0
430
+ while (true) {
431
+ const span = findComponentSpan(lines, name, from)
432
+ if (!span) break
433
+ spans.push(span)
434
+ from = span[1] + 1
435
+ }
436
+ if (instanceIndex >= spans.length)
437
+ return { replaced: false, error: `Instance ${instanceIndex} not found` }
438
+
439
+ const [spanStart, spanEnd] = spans[instanceIndex]
440
+ const startLine = lines[spanStart]
441
+ const tagStart = startLine.indexOf(`<${name}`)
442
+ if (/\/>/.test(startLine.slice(tagStart)))
443
+ return { replaced: false, error: `<${name}> is self-closing` }
444
+ if (spanStart === spanEnd)
445
+ return { replaced: false, error: `<${name}> opens and closes on the same line` }
446
+
447
+ const { line: openEndLine, col: openEndCol } = findOpenTagEnd(lines, spanStart, name)
448
+ if (openEndCol === -1)
449
+ return { replaced: false, error: `Cannot find end of opening tag for <${name}>` }
450
+
451
+ // Preserve the opening tag lines (up to and including the >)
452
+ const openingLines = lines.slice(spanStart, openEndLine + 1)
453
+ openingLines[openingLines.length - 1] = openingLines[openingLines.length - 1].slice(0, openEndCol + 1)
454
+
455
+ const contentLines = newContent.trim() === '' ? [] : newContent.split('\n')
456
+ const replacement = [...openingLines, ...contentLines, lines[spanEnd]]
457
+
458
+ const newLines = [...lines]
459
+ newLines.splice(spanStart, spanEnd - spanStart + 1, ...replacement)
460
+
461
+ try {
462
+ writeFileSync(filePath, newLines.join('\n'), 'utf-8')
463
+ } catch {
464
+ return { replaced: false, error: `Cannot write file: ${filePath}` }
465
+ }
466
+ return { replaced: true, file: filePath }
467
+ }
468
+
469
+ function findOpenTagEnd(
470
+ lines: string[],
471
+ spanStart: number,
472
+ name: string,
473
+ ): { line: number; col: number } {
474
+ const firstLine = lines[spanStart]
475
+ const tagStart = firstLine.indexOf(`<${name}`)
476
+ // Scan the opening line for the closing >
477
+ for (let i = tagStart + name.length + 1; i < firstLine.length; i++) {
478
+ if (firstLine[i] === '>') return { line: spanStart, col: i }
479
+ }
480
+ // Multi-line tag — scan forward for a line that is just >
481
+ for (let i = spanStart + 1; i < lines.length; i++) {
482
+ const t = lines[i].trim()
483
+ if (t === '>') return { line: i, col: lines[i].indexOf('>') }
484
+ }
485
+ return { line: spanStart, col: -1 }
486
+ }
487
+
488
+ /** Finds the content between an HTML element's opening > and closing </tag>. */
489
+ export function getElementFragment(
490
+ filePath: string,
491
+ tagName: string,
492
+ instanceIndex: number,
493
+ ): FragmentResult {
494
+ let source: string
495
+ try {
496
+ source = readFileSync(filePath, 'utf-8')
497
+ } catch {
498
+ return { found: false, error: `Cannot read file: ${filePath}` }
499
+ }
500
+ const lines = source.split('\n')
501
+ const span = findFullElementSpan(lines, tagName, instanceIndex)
502
+ if (!span) return { found: false, error: `No <${tagName}> found at index ${instanceIndex}` }
503
+
504
+ const [spanStart, spanEnd] = span
505
+ const { line: openEndLine, col: openEndCol } = findOpenTagEnd(lines, spanStart, tagName)
506
+ if (openEndCol === -1) return { found: false, error: `Cannot find end of opening tag for <${tagName}>` }
507
+
508
+ const contentLines: string[] = []
509
+ if (spanStart === spanEnd) {
510
+ // Single line: content between > and </tag>
511
+ const line = lines[spanStart]
512
+ const closeIdx = line.lastIndexOf(`</${tagName}>`)
513
+ if (closeIdx === -1) return { found: false, error: `Cannot find closing tag on same line` }
514
+ contentLines.push(line.slice(openEndCol + 1, closeIdx))
515
+ } else {
516
+ const afterOpen = lines[openEndLine].slice(openEndCol + 1)
517
+ if (afterOpen.trim()) contentLines.push(afterOpen)
518
+ for (let i = openEndLine + 1; i < spanEnd; i++) contentLines.push(lines[i])
519
+ }
520
+
521
+ return { found: true, content: contentLines.join('\n'), file: filePath, name: tagName, instanceIndex }
522
+ }
523
+
524
+ /** Replaces the children content of an HTML element, preserving its opening and closing tags. */
525
+ export function replaceElementFragment(
526
+ filePath: string,
527
+ tagName: string,
528
+ instanceIndex: number,
529
+ newContent: string,
530
+ ): ReplaceResult {
531
+ let source: string
532
+ try {
533
+ source = readFileSync(filePath, 'utf-8')
534
+ } catch {
535
+ return { replaced: false, error: `Cannot read file: ${filePath}` }
536
+ }
537
+ const lines = source.split('\n')
538
+ const span = findFullElementSpan(lines, tagName, instanceIndex)
539
+ if (!span) return { replaced: false, error: `No <${tagName}> found at index ${instanceIndex}` }
540
+
541
+ const [spanStart, spanEnd] = span
542
+ const { line: openEndLine, col: openEndCol } = findOpenTagEnd(lines, spanStart, tagName)
543
+ if (openEndCol === -1) return { replaced: false, error: `Cannot find end of opening tag for <${tagName}>` }
544
+
545
+ const newLines = [...lines]
546
+ if (spanStart === spanEnd) {
547
+ const line = lines[spanStart]
548
+ const closeIdx = line.lastIndexOf(`</${tagName}>`)
549
+ if (closeIdx === -1) return { replaced: false, error: `Cannot find closing tag on same line` }
550
+ const before = line.slice(0, openEndCol + 1)
551
+ const after = line.slice(closeIdx)
552
+ const contentLines = newContent.trim() === '' ? [] : newContent.split('\n')
553
+ newLines.splice(spanStart, 1, ...(contentLines.length ? [before + contentLines[0], ...contentLines.slice(1), after] : [before + after]))
554
+ } else {
555
+ const openingLines = lines.slice(spanStart, openEndLine + 1)
556
+ openingLines[openingLines.length - 1] = openingLines[openingLines.length - 1].slice(0, openEndCol + 1)
557
+ const contentLines = newContent.trim() === '' ? [] : newContent.split('\n')
558
+ const replacement = [...openingLines, ...contentLines, lines[spanEnd]]
559
+ newLines.splice(spanStart, spanEnd - spanStart + 1, ...replacement)
560
+ }
561
+
562
+ try {
563
+ writeFileSync(filePath, newLines.join('\n'), 'utf-8')
564
+ } catch {
565
+ return { replaced: false, error: `Cannot write file: ${filePath}` }
566
+ }
567
+ return { replaced: true, file: filePath }
568
+ }
569
+
570
+ export type MoveResult = { moved: true; file: string } | { moved: false; error: string }
571
+
572
+ // Moves a component or element up or down past its adjacent sibling in source.
573
+ // Determines the sibling's span by scanning forward/backward using indent level —
574
+ // no OML tree needed on the server side.
575
+ export function moveNode(
576
+ filePath: string,
577
+ name: string,
578
+ isComponent: boolean,
579
+ instanceIndex: number,
580
+ direction: 'up' | 'down',
581
+ ): MoveResult {
582
+ let source: string
583
+ try {
584
+ source = readFileSync(filePath, 'utf-8')
585
+ } catch {
586
+ return { moved: false, error: `Cannot read file: ${filePath}` }
587
+ }
588
+
589
+ const lines = source.split('\n')
590
+
591
+ // Find the Nth instance of the target node
592
+ let targetSpan: [number, number] | null = null
593
+ if (isComponent) {
594
+ let from = 0
595
+ for (let idx = 0; idx <= instanceIndex; idx++) {
596
+ const s = findComponentSpan(lines, name, from)
597
+ if (!s) break
598
+ if (idx === instanceIndex) targetSpan = s
599
+ from = s[1] + 1
600
+ }
601
+ } else {
602
+ targetSpan = findFullElementSpan(lines, name, instanceIndex)
603
+ }
604
+
605
+ if (!targetSpan) return { moved: false, error: `No <${name}> found at index ${instanceIndex}` }
606
+
607
+ const [startA, endA] = targetSpan
608
+ const indA = (lines[startA].match(/^(\s*)/) ?? ['', ''])[1].length
609
+
610
+ let siblingSpan: [number, number] | null = null
611
+
612
+ if (direction === 'up') {
613
+ // Preceding sibling: its end is the first non-blank line before startA
614
+ let endB = startA - 1
615
+ while (endB >= 0 && lines[endB].trim() === '') endB--
616
+ if (endB < 0) return { moved: false, error: 'Already at top' }
617
+
618
+ // Scan backward from endB to find the sibling's start: first line at indA that opens a tag
619
+ let startB = endB
620
+ for (let j = endB - 1; j >= 0; j--) {
621
+ const t = lines[j].trim()
622
+ if (!t) continue
623
+ const ind = (lines[j].match(/^(\s*)/) ?? ['', ''])[1].length
624
+ if (ind < indA) break
625
+ if (ind === indA && t.startsWith('<') && !t.startsWith('</') && !t.startsWith('<!--')) {
626
+ startB = j
627
+ break
628
+ }
629
+ }
630
+ siblingSpan = [startB, endB]
631
+ } else {
632
+ // Following sibling: its start is the first non-blank line after endA
633
+ let startB = endA + 1
634
+ while (startB < lines.length && lines[startB].trim() === '') startB++
635
+ if (startB >= lines.length) return { moved: false, error: 'Already at bottom' }
636
+
637
+ const indB = (lines[startB].match(/^(\s*)/) ?? ['', ''])[1].length
638
+ const tagMatch = lines[startB].match(/<([A-Za-z][A-Za-z0-9_:-]*)/)
639
+ if (!tagMatch) return { moved: false, error: 'Cannot identify next sibling' }
640
+
641
+ const tagB = tagMatch[1]
642
+ const tagPos = lines[startB].indexOf(`<${tagB}`)
643
+ const restB = lines[startB].slice(tagPos)
644
+ let endB = startB
645
+
646
+ if (/\/>/.test(restB) || new RegExp(`</${tagB}>`).test(restB)) {
647
+ endB = startB
648
+ } else {
649
+ for (let j = startB + 1; j < lines.length; j++) {
650
+ const t = lines[j].trim()
651
+ const ind = (lines[j].match(/^(\s*)/) ?? ['', ''])[1].length
652
+ if ((t.startsWith(`</${tagB}>`) || t === '/>') && ind <= indB) { endB = j; break }
653
+ if (t && ind < indB) { endB = j - 1; break }
654
+ }
655
+ }
656
+ siblingSpan = [startB, endB]
657
+ }
658
+
659
+ // Swap the two spans (process in reverse order to preserve line indices)
660
+ const [first, second] =
661
+ targetSpan[0] < siblingSpan[0] ? [targetSpan, siblingSpan] : [siblingSpan, targetSpan]
662
+
663
+ const linesFirst = lines.slice(first[0], first[1] + 1)
664
+ const linesSecond = lines.slice(second[0], second[1] + 1)
665
+
666
+ const newLines = [...lines]
667
+ newLines.splice(second[0], second[1] - second[0] + 1, ...linesFirst)
668
+ newLines.splice(first[0], first[1] - first[0] + 1, ...linesSecond)
669
+
670
+ try {
671
+ writeFileSync(filePath, newLines.join('\n'), 'utf-8')
672
+ } catch {
673
+ return { moved: false, error: `Cannot write file: ${filePath}` }
674
+ }
675
+
676
+ return { moved: true, file: filePath }
677
+ }