@upstart.gg/vite-plugins 0.0.37
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 +323 -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-theme.d.ts +22 -0
- package/dist/vite-plugin-upstart-theme.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-theme.js +179 -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 +1189 -0
- package/src/tests/vite-plugin-upstart-editor.test.ts +81 -0
- package/src/upstart-editor-api.ts +204 -0
- package/src/vite-plugin-upstart-attrs.ts +708 -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 +314 -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,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
|
+
}
|