@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.
Files changed (63) hide show
  1. package/dist/vite-plugin-upstart-attrs.d.ts +29 -0
  2. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -0
  3. package/dist/vite-plugin-upstart-attrs.js +286 -0
  4. package/dist/vite-plugin-upstart-attrs.js.map +1 -0
  5. package/dist/vite-plugin-upstart-editor/plugin.d.ts +15 -0
  6. package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -0
  7. package/dist/vite-plugin-upstart-editor/plugin.js +55 -0
  8. package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -0
  9. package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts +12 -0
  10. package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -0
  11. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +57 -0
  12. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -0
  13. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts +12 -0
  14. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts.map +1 -0
  15. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +91 -0
  16. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -0
  17. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +22 -0
  18. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -0
  19. package/dist/vite-plugin-upstart-editor/runtime/index.js +62 -0
  20. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -0
  21. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts +15 -0
  22. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -0
  23. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +292 -0
  24. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -0
  25. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +126 -0
  26. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -0
  27. package/dist/vite-plugin-upstart-editor/runtime/types.js +1 -0
  28. package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts +15 -0
  29. package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts.map +1 -0
  30. package/dist/vite-plugin-upstart-editor/runtime/utils.js +26 -0
  31. package/dist/vite-plugin-upstart-editor/runtime/utils.js.map +1 -0
  32. package/dist/vite-plugin-upstart-routes.d.ts +20 -0
  33. package/dist/vite-plugin-upstart-routes.d.ts.map +1 -0
  34. package/dist/vite-plugin-upstart-routes.js +143 -0
  35. package/dist/vite-plugin-upstart-routes.js.map +1 -0
  36. package/dist/vite-plugin-upstart-theme.d.ts +22 -0
  37. package/dist/vite-plugin-upstart-theme.d.ts.map +1 -0
  38. package/dist/vite-plugin-upstart-theme.js +180 -0
  39. package/dist/vite-plugin-upstart-theme.js.map +1 -0
  40. package/package.json +63 -0
  41. package/src/tests/fixtures/routes/default-layout.tsx +10 -0
  42. package/src/tests/fixtures/routes/dynamic-route.tsx +10 -0
  43. package/src/tests/fixtures/routes/missing-attributes.tsx +8 -0
  44. package/src/tests/fixtures/routes/missing-path.tsx +9 -0
  45. package/src/tests/fixtures/routes/valid-full.tsx +15 -0
  46. package/src/tests/fixtures/routes/valid-minimal.tsx +10 -0
  47. package/src/tests/fixtures/routes/with-comments.tsx +12 -0
  48. package/src/tests/fixtures/routes/with-nested-objects.tsx +15 -0
  49. package/src/tests/upstart-editor-api.test.ts +367 -0
  50. package/src/tests/vite-plugin-upstart-attrs.test.ts +957 -0
  51. package/src/tests/vite-plugin-upstart-editor.test.ts +81 -0
  52. package/src/tests/vite-plugin-upstart-routes.test.ts +220 -0
  53. package/src/upstart-editor-api.ts +204 -0
  54. package/src/vite-plugin-upstart-attrs.ts +616 -0
  55. package/src/vite-plugin-upstart-editor/PLAN.md +1391 -0
  56. package/src/vite-plugin-upstart-editor/plugin.ts +73 -0
  57. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +80 -0
  58. package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +135 -0
  59. package/src/vite-plugin-upstart-editor/runtime/index.ts +90 -0
  60. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +401 -0
  61. package/src/vite-plugin-upstart-editor/runtime/types.ts +120 -0
  62. package/src/vite-plugin-upstart-editor/runtime/utils.ts +34 -0
  63. 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
+ }