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.
- package/BASELINE.md +169 -0
- package/CLAUDE.md +518 -0
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/ROADMAP.md +198 -0
- package/build.mjs +101 -0
- package/client/control.ts +247 -0
- package/client/hydrate.ts +37 -0
- package/client/index.ts +19 -0
- package/client/jsx-runtime.ts +209 -0
- package/client/resource.ts +122 -0
- package/client/signal.ts +211 -0
- package/client/store.ts +110 -0
- package/client/useHead.ts +63 -0
- package/dist/build/config.d.ts +3 -0
- package/dist/build/config.d.ts.map +1 -0
- package/dist/build/config.js +38 -0
- package/dist/build/config.js.map +7 -0
- package/dist/build/index.d.ts +2 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +13 -0
- package/dist/build/index.js.map +7 -0
- package/dist/build/plugins.d.ts +7 -0
- package/dist/build/plugins.d.ts.map +1 -0
- package/dist/build/plugins.js +85 -0
- package/dist/build/plugins.js.map +7 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +427 -0
- package/dist/cli.js.map +7 -0
- package/dist/client/control.d.ts +49 -0
- package/dist/client/control.d.ts.map +1 -0
- package/dist/client/control.js +154 -0
- package/dist/client/control.js.map +7 -0
- package/dist/client/hydrate.d.ts +7 -0
- package/dist/client/hydrate.d.ts.map +1 -0
- package/dist/client/hydrate.js +23 -0
- package/dist/client/hydrate.js.map +7 -0
- package/dist/client/index.d.ts +12 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +32 -0
- package/dist/client/index.js.map +7 -0
- package/dist/client/jsx-runtime.d.ts +40 -0
- package/dist/client/jsx-runtime.d.ts.map +1 -0
- package/dist/client/jsx-runtime.js +139 -0
- package/dist/client/jsx-runtime.js.map +7 -0
- package/dist/client/resource.d.ts +31 -0
- package/dist/client/resource.d.ts.map +1 -0
- package/dist/client/resource.js +64 -0
- package/dist/client/resource.js.map +7 -0
- package/dist/client/signal.d.ts +90 -0
- package/dist/client/signal.d.ts.map +1 -0
- package/dist/client/signal.js +115 -0
- package/dist/client/signal.js.map +7 -0
- package/dist/client/store.d.ts +26 -0
- package/dist/client/store.d.ts.map +1 -0
- package/dist/client/store.js +63 -0
- package/dist/client/store.js.map +7 -0
- package/dist/client/useHead.d.ts +28 -0
- package/dist/client/useHead.d.ts.map +1 -0
- package/dist/client/useHead.js +33 -0
- package/dist/client/useHead.js.map +7 -0
- package/dist/config.d.ts +182 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +21 -0
- package/dist/config.js.map +7 -0
- package/dist/create-multisite.d.ts +2 -0
- package/dist/create-multisite.d.ts.map +1 -0
- package/dist/create-multisite.js +291 -0
- package/dist/create-multisite.js.map +7 -0
- package/dist/create.d.ts +2 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +179 -0
- package/dist/create.js.map +7 -0
- package/dist/dev/blueprints.d.ts +11 -0
- package/dist/dev/blueprints.d.ts.map +1 -0
- package/dist/dev/blueprints.js +65 -0
- package/dist/dev/blueprints.js.map +7 -0
- package/dist/dev/components.d.ts +19 -0
- package/dist/dev/components.d.ts.map +1 -0
- package/dist/dev/components.js +87 -0
- package/dist/dev/components.js.map +7 -0
- package/dist/dev/insert.d.ts +11 -0
- package/dist/dev/insert.d.ts.map +1 -0
- package/dist/dev/insert.js +160 -0
- package/dist/dev/insert.js.map +7 -0
- package/dist/dev/remove.d.ts +53 -0
- package/dist/dev/remove.d.ts.map +1 -0
- package/dist/dev/remove.js +518 -0
- package/dist/dev/remove.js.map +7 -0
- package/dist/dev/watch.d.ts +26 -0
- package/dist/dev/watch.d.ts.map +1 -0
- package/dist/dev/watch.js +2905 -0
- package/dist/dev/watch.js.map +7 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +63 -0
- package/dist/errors.js.map +7 -0
- package/dist/generate.d.ts +2 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +191 -0
- package/dist/generate.js.map +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +7 -0
- package/dist/island.d.ts +24 -0
- package/dist/island.d.ts.map +1 -0
- package/dist/island.js +15 -0
- package/dist/island.js.map +7 -0
- package/dist/jsx-runtime.d.ts +406 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +90 -0
- package/dist/jsx-runtime.js.map +7 -0
- package/dist/link.d.ts +27 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +29 -0
- package/dist/link.js.map +7 -0
- package/dist/oml/fragment.d.ts +16 -0
- package/dist/oml/fragment.d.ts.map +1 -0
- package/dist/oml/fragment.js +26 -0
- package/dist/oml/fragment.js.map +7 -0
- package/dist/oml/index.d.ts +11 -0
- package/dist/oml/index.d.ts.map +1 -0
- package/dist/oml/index.js +21 -0
- package/dist/oml/index.js.map +7 -0
- package/dist/oml/jsx-runtime.d.ts +34 -0
- package/dist/oml/jsx-runtime.d.ts.map +1 -0
- package/dist/oml/jsx-runtime.js +59 -0
- package/dist/oml/jsx-runtime.js.map +7 -0
- package/dist/oml/jsx.d.ts +14 -0
- package/dist/oml/jsx.d.ts.map +1 -0
- package/dist/oml/jsx.js +96 -0
- package/dist/oml/jsx.js.map +7 -0
- package/dist/oml/page.d.ts +7 -0
- package/dist/oml/page.d.ts.map +1 -0
- package/dist/oml/page.js +6 -0
- package/dist/oml/page.js.map +7 -0
- package/dist/oml/render.d.ts +13 -0
- package/dist/oml/render.d.ts.map +1 -0
- package/dist/oml/render.js +117 -0
- package/dist/oml/render.js.map +7 -0
- package/dist/oml/types.d.ts +79 -0
- package/dist/oml/types.d.ts.map +1 -0
- package/dist/oml/types.js +64 -0
- package/dist/oml/types.js.map +7 -0
- package/dist/router/handler.d.ts +53 -0
- package/dist/router/handler.d.ts.map +1 -0
- package/dist/router/handler.js +342 -0
- package/dist/router/handler.js.map +7 -0
- package/dist/router/matcher.d.ts +21 -0
- package/dist/router/matcher.d.ts.map +1 -0
- package/dist/router/matcher.js +28 -0
- package/dist/router/matcher.js.map +7 -0
- package/dist/router/scanner.d.ts +17 -0
- package/dist/router/scanner.d.ts.map +1 -0
- package/dist/router/scanner.js +197 -0
- package/dist/router/scanner.js.map +7 -0
- package/dist/server/index.d.ts +23 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +29 -0
- package/dist/server/index.js.map +7 -0
- package/dist/signal.d.ts +15 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +29 -0
- package/dist/signal.js.map +7 -0
- package/dist/ssg.d.ts +45 -0
- package/dist/ssg.d.ts.map +1 -0
- package/dist/ssg.js +175 -0
- package/dist/ssg.js.map +7 -0
- package/dist/test/actions.test.d.ts +2 -0
- package/dist/test/actions.test.d.ts.map +1 -0
- package/dist/test/body-limits.test.d.ts +2 -0
- package/dist/test/body-limits.test.d.ts.map +1 -0
- package/dist/test/errors.test.d.ts +2 -0
- package/dist/test/errors.test.d.ts.map +1 -0
- package/dist/test/fixtures/routes/[id].page.d.ts +4 -0
- package/dist/test/fixtures/routes/[id].page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_error.d.ts +3 -0
- package/dist/test/fixtures/routes/_error.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_global.d.ts +3 -0
- package/dist/test/fixtures/routes/_global.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout-template.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout-template.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout_scripts.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout_scripts.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_middleware.d.ts +3 -0
- package/dist/test/fixtures/routes/_middleware.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_redirect301_mw.d.ts +3 -0
- package/dist/test/fixtures/routes/_redirect301_mw.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_redirect_mw.d.ts +3 -0
- package/dist/test/fixtures/routes/_redirect_mw.d.ts.map +1 -0
- package/dist/test/fixtures/routes/about.page.d.ts +3 -0
- package/dist/test/fixtures/routes/about.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/action.page.d.ts +6 -0
- package/dist/test/fixtures/routes/action.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/form-all.post.d.ts +3 -0
- package/dist/test/fixtures/routes/api/form-all.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/form-limited.post.d.ts +6 -0
- package/dist/test/fixtures/routes/api/form-limited.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/response-obj.get.d.ts +3 -0
- package/dist/test/fixtures/routes/api/response-obj.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/upload.post.d.ts +12 -0
- package/dist/test/fixtures/routes/api/upload.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/users.get.d.ts +6 -0
- package/dist/test/fixtures/routes/api/users.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/xml.get.d.ts +3 -0
- package/dist/test/fixtures/routes/api/xml.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/auth/_middleware.d.ts +3 -0
- package/dist/test/fixtures/routes/auth/_middleware.d.ts.map +1 -0
- package/dist/test/fixtures/routes/auth/protected.page.d.ts +3 -0
- package/dist/test/fixtures/routes/auth/protected.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/index.page.d.ts +3 -0
- package/dist/test/fixtures/routes/index.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/oml.page.d.ts +3 -0
- package/dist/test/fixtures/routes/oml.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/redirect.page.d.ts +3 -0
- package/dist/test/fixtures/routes/redirect.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/ssg/[slug].page.d.ts +5 -0
- package/dist/test/fixtures/routes/ssg/[slug].page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/ssg/server.page.d.ts +4 -0
- package/dist/test/fixtures/routes/ssg/server.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/state.page.d.ts +3 -0
- package/dist/test/fixtures/routes/state.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/throw.page.d.ts +3 -0
- package/dist/test/fixtures/routes/throw.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts +3 -0
- package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts.map +1 -0
- package/dist/test/helpers.d.ts +37 -0
- package/dist/test/helpers.d.ts.map +1 -0
- package/dist/test/layouts.test.d.ts +2 -0
- package/dist/test/layouts.test.d.ts.map +1 -0
- package/dist/test/middleware.test.d.ts +2 -0
- package/dist/test/middleware.test.d.ts.map +1 -0
- package/dist/test/multipart.test.d.ts +2 -0
- package/dist/test/multipart.test.d.ts.map +1 -0
- package/dist/test/oml-routing.test.d.ts +2 -0
- package/dist/test/oml-routing.test.d.ts.map +1 -0
- package/dist/test/oml.test.d.ts +2 -0
- package/dist/test/oml.test.d.ts.map +1 -0
- package/dist/test/redirects.test.d.ts +2 -0
- package/dist/test/redirects.test.d.ts.map +1 -0
- package/dist/test/routing.test.d.ts +2 -0
- package/dist/test/routing.test.d.ts.map +1 -0
- package/dist/test/ssg.test.d.ts +2 -0
- package/dist/test/ssg.test.d.ts.map +1 -0
- package/dist/test/web-response.test.d.ts +2 -0
- package/dist/test/web-response.test.d.ts.map +1 -0
- package/dist/types.d.ts +314 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +292 -0
- package/dist/types.js.map +7 -0
- package/package.json +103 -0
- package/pka.config.json +32 -0
- package/src/build/config.ts +42 -0
- package/src/build/index.ts +6 -0
- package/src/build/plugins.ts +118 -0
- package/src/cli.ts +502 -0
- package/src/config.ts +197 -0
- package/src/create-multisite.ts +310 -0
- package/src/create.ts +194 -0
- package/src/dev/blueprints.ts +75 -0
- package/src/dev/components.ts +108 -0
- package/src/dev/insert.ts +221 -0
- package/src/dev/remove.ts +677 -0
- package/src/dev/watch.ts +3098 -0
- package/src/env.d.ts +5 -0
- package/src/errors.ts +64 -0
- package/src/generate.ts +228 -0
- package/src/index.ts +67 -0
- package/src/island.ts +47 -0
- package/src/jsx-runtime.d.ts +408 -0
- package/src/jsx-runtime.d.ts.map +1 -0
- package/src/jsx-runtime.ts +536 -0
- package/src/link.ts +49 -0
- package/src/oml/fragment.ts +54 -0
- package/src/oml/index.ts +21 -0
- package/src/oml/jsx-runtime.ts +121 -0
- package/src/oml/jsx.ts +151 -0
- package/src/oml/page.ts +13 -0
- package/src/oml/render.ts +181 -0
- package/src/oml/types.ts +159 -0
- package/src/router/handler.ts +515 -0
- package/src/router/matcher.ts +52 -0
- package/src/router/scanner.ts +272 -0
- package/src/server/index.ts +49 -0
- package/src/signal.ts +39 -0
- package/src/ssg.ts +253 -0
- package/src/test/actions.test.ts +40 -0
- package/src/test/body-limits.test.ts +83 -0
- package/src/test/errors.test.ts +53 -0
- package/src/test/fixtures/routes/[id].page.ts +3 -0
- package/src/test/fixtures/routes/_error.ts +6 -0
- package/src/test/fixtures/routes/_global.ts +8 -0
- package/src/test/fixtures/routes/_layout-template.ts +7 -0
- package/src/test/fixtures/routes/_layout.ts +7 -0
- package/src/test/fixtures/routes/_layout_scripts.ts +8 -0
- package/src/test/fixtures/routes/_middleware.ts +8 -0
- package/src/test/fixtures/routes/_redirect301_mw.ts +5 -0
- package/src/test/fixtures/routes/_redirect_mw.ts +5 -0
- package/src/test/fixtures/routes/about.page.ts +6 -0
- package/src/test/fixtures/routes/action.page.ts +11 -0
- package/src/test/fixtures/routes/api/form-all.post.ts +5 -0
- package/src/test/fixtures/routes/api/form-limited.post.ts +6 -0
- package/src/test/fixtures/routes/api/response-obj.get.ts +17 -0
- package/src/test/fixtures/routes/api/upload.post.ts +14 -0
- package/src/test/fixtures/routes/api/users.get.ts +3 -0
- package/src/test/fixtures/routes/api/xml.get.ts +5 -0
- package/src/test/fixtures/routes/auth/_middleware.ts +11 -0
- package/src/test/fixtures/routes/auth/protected.page.ts +3 -0
- package/src/test/fixtures/routes/index.page.ts +3 -0
- package/src/test/fixtures/routes/oml.page.ts +7 -0
- package/src/test/fixtures/routes/redirect.page.ts +3 -0
- package/src/test/fixtures/routes/ssg/[slug].page.ts +8 -0
- package/src/test/fixtures/routes/ssg/server.page.ts +5 -0
- package/src/test/fixtures/routes/state.page.ts +4 -0
- package/src/test/fixtures/routes/throw.page.ts +5 -0
- package/src/test/fixtures/routes/wiki/[...slug].page.ts +3 -0
- package/src/test/helpers.ts +132 -0
- package/src/test/layouts.test.ts +76 -0
- package/src/test/middleware.test.ts +69 -0
- package/src/test/multipart.test.ts +91 -0
- package/src/test/oml-routing.test.ts +59 -0
- package/src/test/oml.test.ts +429 -0
- package/src/test/redirects.test.ts +32 -0
- package/src/test/routing.test.ts +118 -0
- package/src/test/ssg.test.ts +273 -0
- package/src/test/web-response.test.ts +33 -0
- package/src/types.ts +670 -0
- package/tsconfig.client.json +17 -0
- 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
|
+
}
|