@upstart.gg/vite-plugins 0.0.139
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/dist/vite-plugin-upstart-attrs.d.ts +29 -0
- package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-attrs.js +286 -0
- package/dist/vite-plugin-upstart-attrs.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/plugin.d.ts +15 -0
- package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/plugin.js +55 -0
- package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts +12 -0
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +57 -0
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts +12 -0
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +91 -0
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +22 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.js +62 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts +15 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +292 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +126 -0
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/types.js +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts +15 -0
- package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/utils.js +26 -0
- package/dist/vite-plugin-upstart-editor/runtime/utils.js.map +1 -0
- package/dist/vite-plugin-upstart-routes.d.ts +20 -0
- package/dist/vite-plugin-upstart-routes.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-routes.js +143 -0
- package/dist/vite-plugin-upstart-routes.js.map +1 -0
- package/dist/vite-plugin-upstart-theme.d.ts +22 -0
- package/dist/vite-plugin-upstart-theme.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-theme.js +180 -0
- package/dist/vite-plugin-upstart-theme.js.map +1 -0
- package/package.json +63 -0
- package/src/tests/fixtures/routes/default-layout.tsx +10 -0
- package/src/tests/fixtures/routes/dynamic-route.tsx +10 -0
- package/src/tests/fixtures/routes/missing-attributes.tsx +8 -0
- package/src/tests/fixtures/routes/missing-path.tsx +9 -0
- package/src/tests/fixtures/routes/valid-full.tsx +15 -0
- package/src/tests/fixtures/routes/valid-minimal.tsx +10 -0
- package/src/tests/fixtures/routes/with-comments.tsx +12 -0
- package/src/tests/fixtures/routes/with-nested-objects.tsx +15 -0
- package/src/tests/upstart-editor-api.test.ts +367 -0
- package/src/tests/vite-plugin-upstart-attrs.test.ts +957 -0
- package/src/tests/vite-plugin-upstart-editor.test.ts +81 -0
- package/src/tests/vite-plugin-upstart-routes.test.ts +220 -0
- package/src/upstart-editor-api.ts +204 -0
- package/src/vite-plugin-upstart-attrs.ts +616 -0
- package/src/vite-plugin-upstart-editor/PLAN.md +1391 -0
- package/src/vite-plugin-upstart-editor/plugin.ts +73 -0
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +80 -0
- package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +135 -0
- package/src/vite-plugin-upstart-editor/runtime/index.ts +90 -0
- package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +401 -0
- package/src/vite-plugin-upstart-editor/runtime/types.ts +120 -0
- package/src/vite-plugin-upstart-editor/runtime/utils.ts +34 -0
- package/src/vite-plugin-upstart-theme.ts +321 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { upstartEditor } from "../vite-plugin-upstart-editor/plugin";
|
|
3
|
+
import type { UpstartEditorPluginOptions } from "../vite-plugin-upstart-editor/runtime/types";
|
|
4
|
+
|
|
5
|
+
const MAIN_ID = "/project/src/main.tsx";
|
|
6
|
+
const OTHER_ID = "/project/src/other.tsx";
|
|
7
|
+
|
|
8
|
+
type EditorPlugin = {
|
|
9
|
+
name?: string;
|
|
10
|
+
transform?: ((code: string, id: string) => unknown) | { handler: (code: string, id: string) => unknown };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function getPlugin(options: UpstartEditorPluginOptions): EditorPlugin {
|
|
14
|
+
const plugin = upstartEditor.vite(options) as unknown;
|
|
15
|
+
return (Array.isArray(plugin) ? plugin[0] : plugin) as EditorPlugin;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function runTransform(plugin: EditorPlugin, code: string, id: string): { code: string } | null {
|
|
19
|
+
const transform = plugin.transform;
|
|
20
|
+
if (!transform) {
|
|
21
|
+
throw new Error("Expected transform hook to be defined");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const handler = typeof transform === "function" ? transform : transform.handler;
|
|
25
|
+
const result = handler(code, id);
|
|
26
|
+
|
|
27
|
+
if (typeof result === "string") {
|
|
28
|
+
return { code: result };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (result && typeof result === "object" && "code" in result) {
|
|
32
|
+
return { code: String((result as { code?: unknown }).code ?? "") };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("upstart-editor plugin", () => {
|
|
39
|
+
test("returns disabled plugin when not enabled", () => {
|
|
40
|
+
const plugin = getPlugin({ enabled: false });
|
|
41
|
+
expect(plugin.name).toBe("upstart-editor-disabled");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("injects runtime init into entry file", () => {
|
|
45
|
+
const plugin = getPlugin({ enabled: true, autoInject: true });
|
|
46
|
+
|
|
47
|
+
const result = runTransform(plugin, "console.log('app');", MAIN_ID);
|
|
48
|
+
|
|
49
|
+
expect(result).not.toBeNull();
|
|
50
|
+
expect(result?.code).toContain("initUpstartEditor");
|
|
51
|
+
expect(result?.code).toContain("DOMContentLoaded");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("does not inject into non-entry files", () => {
|
|
55
|
+
const plugin = getPlugin({ enabled: true, autoInject: true });
|
|
56
|
+
|
|
57
|
+
const result = runTransform(plugin, "console.log('app');", OTHER_ID);
|
|
58
|
+
|
|
59
|
+
expect(result).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("does not inject when initUpstartEditor is already present", () => {
|
|
63
|
+
const plugin = getPlugin({ enabled: true, autoInject: true });
|
|
64
|
+
|
|
65
|
+
const result = runTransform(
|
|
66
|
+
plugin,
|
|
67
|
+
"import { initUpstartEditor } from 'x'; initUpstartEditor();",
|
|
68
|
+
MAIN_ID,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(result).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("does not inject when autoInject is false", () => {
|
|
75
|
+
const plugin = getPlugin({ enabled: true, autoInject: false });
|
|
76
|
+
|
|
77
|
+
const result = runTransform(plugin, "console.log('app');", MAIN_ID);
|
|
78
|
+
|
|
79
|
+
expect(result).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
interface PageAttributes {
|
|
6
|
+
path?: string;
|
|
7
|
+
layout?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PageInfo {
|
|
12
|
+
filename: string;
|
|
13
|
+
path: string;
|
|
14
|
+
layout: string;
|
|
15
|
+
attributes: PageAttributes;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract the attributes export from source code using regex
|
|
20
|
+
* (Same as in the plugin)
|
|
21
|
+
*/
|
|
22
|
+
function extractAttributesFromSource(code: string): Partial<PageAttributes> {
|
|
23
|
+
const match = code.match(
|
|
24
|
+
/export\s+const\s+attributes\s*=\s*definePageAttributes\s*\(\s*(\{[\s\S]*?\})\s*\)\s*;?/,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!match) {
|
|
28
|
+
throw new Error("attributes export not found in source code");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const objectStr = match[1];
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// eslint-disable-next-line no-new-func
|
|
35
|
+
const obj = new Function(`return ${objectStr}`)() as Partial<PageAttributes>;
|
|
36
|
+
return obj;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Failed to parse attributes object: ${error instanceof Error ? error.message : String(error)}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract page attributes from a TypeScript file
|
|
46
|
+
* (Same as in the plugin, with optional ssrLoadModule)
|
|
47
|
+
*/
|
|
48
|
+
async function extractPageAttributes(filePath: string): Promise<PageInfo> {
|
|
49
|
+
// For testing, we don't have ssrLoadModule, so it falls back to source parsing
|
|
50
|
+
const ssrLoadModule = undefined;
|
|
51
|
+
|
|
52
|
+
// Fallback: extract from source code
|
|
53
|
+
try {
|
|
54
|
+
const code = readFileSync(filePath, "utf-8");
|
|
55
|
+
const attributes = extractAttributesFromSource(code);
|
|
56
|
+
|
|
57
|
+
if (!attributes || typeof attributes !== "object" || !("path" in attributes)) {
|
|
58
|
+
throw new Error("attributes export not found");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const path = attributes.path as string;
|
|
62
|
+
const layout = (attributes.layout as string) ?? "default";
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
filename: filePath,
|
|
66
|
+
path,
|
|
67
|
+
layout,
|
|
68
|
+
attributes: attributes as PageAttributes,
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const fullError = error instanceof Error ? error.message : String(error);
|
|
72
|
+
console.error(`[upstart-routes] Import error for ${filePath}:\n${fullError}`);
|
|
73
|
+
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Page attributes not found or invalid. " +
|
|
76
|
+
"Make sure to use `export const attributes = definePageAttributes(...)`",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("vite-plugin-upstart-routes", () => {
|
|
82
|
+
const fixturesDir = join(__dirname, "fixtures/routes");
|
|
83
|
+
|
|
84
|
+
describe("extractPageAttributes", () => {
|
|
85
|
+
describe("Valid Pages", () => {
|
|
86
|
+
it("should extract minimal attributes", async () => {
|
|
87
|
+
const result = await extractPageAttributes(join(fixturesDir, "valid-minimal.tsx"));
|
|
88
|
+
|
|
89
|
+
expect(result.path).toBe("/about");
|
|
90
|
+
expect(result.layout).toBe("default");
|
|
91
|
+
expect(result.attributes.label).toBe("About Page");
|
|
92
|
+
expect(result.attributes.title).toBe("About Us");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should extract full attributes with all fields", async () => {
|
|
96
|
+
const result = await extractPageAttributes(join(fixturesDir, "valid-full.tsx"));
|
|
97
|
+
|
|
98
|
+
expect(result.path).toBe("/contact");
|
|
99
|
+
expect(result.layout).toBe("sidebar");
|
|
100
|
+
expect(result.attributes.label).toBe("Contact Us");
|
|
101
|
+
expect(result.attributes.title).toBe("Contact Our Team");
|
|
102
|
+
expect(result.attributes.description).toBe("Get in touch with our team");
|
|
103
|
+
expect(result.attributes.keywords).toBe("contact, email, support");
|
|
104
|
+
expect(result.attributes.tags).toEqual(["navbar", "important"]);
|
|
105
|
+
expect(result.attributes.robotsIndexing).toBe(true);
|
|
106
|
+
expect(result.attributes.language).toBe("en");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should handle pages with default layout when omitted", async () => {
|
|
110
|
+
const result = await extractPageAttributes(join(fixturesDir, "default-layout.tsx"));
|
|
111
|
+
|
|
112
|
+
expect(result.path).toBe("/home");
|
|
113
|
+
expect(result.layout).toBe("default");
|
|
114
|
+
expect(result.attributes.label).toBe("Home");
|
|
115
|
+
expect(result.attributes.title).toBe("Home Page");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should handle dynamic routes with params", async () => {
|
|
119
|
+
const result = await extractPageAttributes(join(fixturesDir, "dynamic-route.tsx"));
|
|
120
|
+
|
|
121
|
+
expect(result.path).toBe("/posts/:id");
|
|
122
|
+
expect(result.layout).toBe("default");
|
|
123
|
+
expect(result.attributes.label).toBe("Post Detail");
|
|
124
|
+
expect(result.attributes.title).toBe(null);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should handle nested objects in attributes", async () => {
|
|
128
|
+
const result = await extractPageAttributes(join(fixturesDir, "with-nested-objects.tsx"));
|
|
129
|
+
|
|
130
|
+
expect(result.path).toBe("/advanced");
|
|
131
|
+
expect(result.attributes.label).toBe("Advanced Page");
|
|
132
|
+
expect(result.attributes.title).toBe("Advanced Features");
|
|
133
|
+
expect(result.attributes.tags).toEqual(["admin", "advanced"]);
|
|
134
|
+
expect(result.attributes.additionalTags).toEqual({
|
|
135
|
+
headTags: "<meta name='custom' content='value' />",
|
|
136
|
+
bodyTags: "<script>console.log('test')</script>",
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should handle comments in attributes", async () => {
|
|
141
|
+
const result = await extractPageAttributes(join(fixturesDir, "with-comments.tsx"));
|
|
142
|
+
|
|
143
|
+
expect(result.path).toBe("/services");
|
|
144
|
+
expect(result.attributes.label).toBe("Services");
|
|
145
|
+
expect(result.attributes.title).toBe("Our Services");
|
|
146
|
+
expect(result.attributes.description).toBe("Check out our services");
|
|
147
|
+
expect(result.attributes.tags).toEqual(["navbar", "featured"]);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("Error Cases - Fail Fast", () => {
|
|
152
|
+
it("should throw error when path is missing", async () => {
|
|
153
|
+
await expect(extractPageAttributes(join(fixturesDir, "missing-path.tsx"))).rejects.toThrow(
|
|
154
|
+
/Page attributes not found or invalid/,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should throw error when attributes export is missing", async () => {
|
|
159
|
+
await expect(extractPageAttributes(join(fixturesDir, "missing-attributes.tsx"))).rejects.toThrow(
|
|
160
|
+
/Page attributes not found or invalid/,
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("Array Handling", () => {
|
|
166
|
+
it("should extract array values correctly", async () => {
|
|
167
|
+
const result = await extractPageAttributes(join(fixturesDir, "valid-full.tsx"));
|
|
168
|
+
|
|
169
|
+
expect(Array.isArray(result.attributes.tags)).toBe(true);
|
|
170
|
+
expect(result.attributes.tags).toHaveLength(2);
|
|
171
|
+
expect(result.attributes.tags).toContain("navbar");
|
|
172
|
+
expect(result.attributes.tags).toContain("important");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("Layout Assignment", () => {
|
|
177
|
+
it("should use explicit layout when specified", async () => {
|
|
178
|
+
const result = await extractPageAttributes(join(fixturesDir, "valid-full.tsx"));
|
|
179
|
+
expect(result.layout).toBe("sidebar");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should default to 'default' layout when not specified", async () => {
|
|
183
|
+
const result = await extractPageAttributes(join(fixturesDir, "valid-minimal.tsx"));
|
|
184
|
+
expect(result.layout).toBe("default");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("Path Formats", () => {
|
|
189
|
+
it("should handle root path", async () => {
|
|
190
|
+
const result = await extractPageAttributes(join(fixturesDir, "valid-minimal.tsx"));
|
|
191
|
+
expect(result.path).toMatch(/^\//);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should handle nested paths", async () => {
|
|
195
|
+
const result = await extractPageAttributes(join(fixturesDir, "valid-full.tsx"));
|
|
196
|
+
expect(result.path).toBe("/contact");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should handle dynamic parameters", async () => {
|
|
200
|
+
const result = await extractPageAttributes(join(fixturesDir, "dynamic-route.tsx"));
|
|
201
|
+
expect(result.path).toContain(":id");
|
|
202
|
+
expect(result.path).toBe("/posts/:id");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("Edge Cases", () => {
|
|
207
|
+
it("should preserve string values exactly", async () => {
|
|
208
|
+
const result = await extractPageAttributes(join(fixturesDir, "valid-full.tsx"));
|
|
209
|
+
expect(typeof result.attributes.description).toBe("string");
|
|
210
|
+
expect(result.attributes.description).toBe("Get in touch with our team");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should preserve boolean values", async () => {
|
|
214
|
+
const result = await extractPageAttributes(join(fixturesDir, "valid-full.tsx"));
|
|
215
|
+
expect(typeof result.attributes.robotsIndexing).toBe("boolean");
|
|
216
|
+
expect(result.attributes.robotsIndexing).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import MagicString from "magic-string";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import type { EditableEntry } from "./vite-plugin-upstart-attrs";
|
|
5
|
+
|
|
6
|
+
export interface EditableRegistry {
|
|
7
|
+
version: number;
|
|
8
|
+
generatedAt: string;
|
|
9
|
+
elements: Record<string, EditableEntry>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface EditResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class UpstartEditorAPI {
|
|
18
|
+
private registry: EditableRegistry | null = null;
|
|
19
|
+
private projectRoot: string;
|
|
20
|
+
private registryPath: string;
|
|
21
|
+
|
|
22
|
+
constructor(projectRoot: string, registryPath: string) {
|
|
23
|
+
this.projectRoot = projectRoot;
|
|
24
|
+
this.registryPath = registryPath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load the registry from disk
|
|
29
|
+
*/
|
|
30
|
+
async loadRegistry(): Promise<void> {
|
|
31
|
+
const content = await fs.readFile(this.registryPath, "utf-8");
|
|
32
|
+
this.registry = JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the current registry (for testing/debugging)
|
|
37
|
+
*/
|
|
38
|
+
getRegistry(): EditableRegistry | null {
|
|
39
|
+
return this.registry;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set the registry directly (for testing)
|
|
44
|
+
*/
|
|
45
|
+
setRegistry(registry: EditableRegistry): void {
|
|
46
|
+
this.registry = registry;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Edit the text content of an element
|
|
51
|
+
*/
|
|
52
|
+
async editText(id: string, newContent: string): Promise<EditResult> {
|
|
53
|
+
if (!this.registry) {
|
|
54
|
+
try {
|
|
55
|
+
await this.loadRegistry();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { success: false, error: `Failed to load registry: ${err}` };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const entry = this.registry!.elements[id];
|
|
62
|
+
if (!entry) {
|
|
63
|
+
return { success: false, error: `Element ${id} not found in registry` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (entry.type !== "text") {
|
|
67
|
+
return { success: false, error: `Element ${id} is not a text element (type: ${entry.type})` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return this.applyEdit(id, entry, newContent);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Edit the className of an element
|
|
75
|
+
*/
|
|
76
|
+
async editClassName(id: string, newClassName: string): Promise<EditResult> {
|
|
77
|
+
if (!this.registry) {
|
|
78
|
+
try {
|
|
79
|
+
await this.loadRegistry();
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return { success: false, error: `Failed to load registry: ${err}` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const entry = this.registry!.elements[id];
|
|
86
|
+
if (!entry) {
|
|
87
|
+
return { success: false, error: `Element ${id} not found in registry` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (entry.type !== "className") {
|
|
91
|
+
return { success: false, error: `Element ${id} is not a className element (type: ${entry.type})` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return this.applyEdit(id, entry, newClassName);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Apply an edit to a source file
|
|
99
|
+
*/
|
|
100
|
+
private async applyEdit(
|
|
101
|
+
id: string,
|
|
102
|
+
entry: EditableEntry,
|
|
103
|
+
newContent: string
|
|
104
|
+
): Promise<EditResult> {
|
|
105
|
+
const filePath = path.join(this.projectRoot, entry.file);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const code = await fs.readFile(filePath, "utf-8");
|
|
109
|
+
|
|
110
|
+
// Verify content at expected location
|
|
111
|
+
const currentContent = code.slice(entry.startOffset, entry.endOffset);
|
|
112
|
+
|
|
113
|
+
let actualStart = entry.startOffset;
|
|
114
|
+
let actualEnd = entry.endOffset;
|
|
115
|
+
|
|
116
|
+
if (currentContent !== entry.originalContent) {
|
|
117
|
+
// Content has shifted - try to find it by searching
|
|
118
|
+
const searchIndex = code.indexOf(entry.originalContent);
|
|
119
|
+
if (searchIndex === -1) {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
error: `Original content "${entry.originalContent}" not found in file ${entry.file}. The file may have been modified.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
actualStart = searchIndex;
|
|
126
|
+
actualEnd = searchIndex + entry.originalContent.length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Apply the edit using MagicString
|
|
130
|
+
const s = new MagicString(code);
|
|
131
|
+
s.overwrite(actualStart, actualEnd, newContent);
|
|
132
|
+
|
|
133
|
+
// Write the modified file
|
|
134
|
+
await fs.writeFile(filePath, s.toString());
|
|
135
|
+
|
|
136
|
+
// Calculate the length difference for offset adjustments
|
|
137
|
+
const lengthDiff = newContent.length - entry.originalContent.length;
|
|
138
|
+
|
|
139
|
+
// Update the registry entry
|
|
140
|
+
entry.startOffset = actualStart;
|
|
141
|
+
entry.endOffset = actualStart + newContent.length;
|
|
142
|
+
entry.originalContent = newContent;
|
|
143
|
+
|
|
144
|
+
// Shift all subsequent entries in the same file
|
|
145
|
+
for (const [otherId, otherEntry] of Object.entries(this.registry!.elements)) {
|
|
146
|
+
if (otherId !== id && otherEntry.file === entry.file && otherEntry.startOffset > actualStart) {
|
|
147
|
+
otherEntry.startOffset += lengthDiff;
|
|
148
|
+
otherEntry.endOffset += lengthDiff;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Save the updated registry
|
|
153
|
+
await fs.writeFile(
|
|
154
|
+
this.registryPath,
|
|
155
|
+
JSON.stringify(this.registry, null, 2)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return { success: true };
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return { success: false, error: String(err) };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get element info by ID
|
|
166
|
+
*/
|
|
167
|
+
getElement(id: string): EditableEntry | undefined {
|
|
168
|
+
return this.registry?.elements[id];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get all elements of a specific type
|
|
173
|
+
*/
|
|
174
|
+
getElementsByType(type: "text" | "className"): Record<string, EditableEntry> {
|
|
175
|
+
if (!this.registry) {
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const result: Record<string, EditableEntry> = {};
|
|
180
|
+
for (const [id, entry] of Object.entries(this.registry.elements)) {
|
|
181
|
+
if (entry.type === type) {
|
|
182
|
+
result[id] = entry;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get all elements in a specific file
|
|
190
|
+
*/
|
|
191
|
+
getElementsByFile(file: string): Record<string, EditableEntry> {
|
|
192
|
+
if (!this.registry) {
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result: Record<string, EditableEntry> = {};
|
|
197
|
+
for (const [id, entry] of Object.entries(this.registry.elements)) {
|
|
198
|
+
if (entry.file === file) {
|
|
199
|
+
result[id] = entry;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
}
|