@upstart.gg/vite-plugins 0.0.38 → 0.0.40

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 (64) hide show
  1. package/dist/upstart-editor-api.d.ts +79 -0
  2. package/dist/upstart-editor-api.d.ts.map +1 -0
  3. package/dist/upstart-editor-api.js +208 -0
  4. package/dist/upstart-editor-api.js.map +1 -0
  5. package/dist/vite-plugin-upstart-attrs.d.ts +3 -3
  6. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
  7. package/dist/vite-plugin-upstart-attrs.js +227 -25
  8. package/dist/vite-plugin-upstart-attrs.js.map +1 -1
  9. package/dist/vite-plugin-upstart-branding/plugin.d.ts +17 -0
  10. package/dist/vite-plugin-upstart-branding/plugin.d.ts.map +1 -0
  11. package/dist/vite-plugin-upstart-branding/plugin.js +41 -0
  12. package/dist/vite-plugin-upstart-branding/plugin.js.map +1 -0
  13. package/dist/vite-plugin-upstart-branding/runtime.d.ts +10 -0
  14. package/dist/vite-plugin-upstart-branding/runtime.d.ts.map +1 -0
  15. package/dist/vite-plugin-upstart-branding/runtime.js +118 -0
  16. package/dist/vite-plugin-upstart-branding/runtime.js.map +1 -0
  17. package/dist/vite-plugin-upstart-branding/types.d.ts +14 -0
  18. package/dist/vite-plugin-upstart-branding/types.d.ts.map +1 -0
  19. package/dist/vite-plugin-upstart-branding/types.js +1 -0
  20. package/dist/vite-plugin-upstart-editor/plugin.d.ts +3 -3
  21. package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -1
  22. package/dist/vite-plugin-upstart-editor/plugin.js +3 -16
  23. package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -1
  24. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +25 -11
  25. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  26. package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts +5 -0
  27. package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts.map +1 -0
  28. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js +16 -0
  29. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -0
  30. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +1 -1
  31. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
  32. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +2 -1
  33. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
  34. package/dist/vite-plugin-upstart-editor/runtime/index.js +42 -7
  35. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  36. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts +6 -1
  37. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
  38. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +423 -129
  39. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
  40. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +18 -10
  41. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  42. package/dist/vite-plugin-upstart-theme.d.ts +3 -3
  43. package/dist/vite-plugin-upstart-theme.d.ts.map +1 -1
  44. package/dist/vite-plugin-upstart-theme.js +1 -3
  45. package/dist/vite-plugin-upstart-theme.js.map +1 -1
  46. package/package.json +12 -4
  47. package/src/tests/upstart-editor-api.test.ts +98 -174
  48. package/src/tests/vite-plugin-upstart-attrs.test.ts +408 -105
  49. package/src/tests/vite-plugin-upstart-branding.test.ts +90 -0
  50. package/src/tests/vite-plugin-upstart-editor.test.ts +1 -2
  51. package/src/upstart-editor-api.ts +90 -29
  52. package/src/vite-plugin-upstart-attrs.ts +376 -38
  53. package/src/vite-plugin-upstart-branding/plugin.ts +59 -0
  54. package/src/vite-plugin-upstart-branding/runtime.ts +128 -0
  55. package/src/vite-plugin-upstart-branding/types.ts +10 -0
  56. package/src/vite-plugin-upstart-editor/plugin.ts +4 -19
  57. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +25 -12
  58. package/src/vite-plugin-upstart-editor/runtime/error-handler.ts +12 -0
  59. package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +1 -1
  60. package/src/vite-plugin-upstart-editor/runtime/index.ts +39 -5
  61. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +518 -141
  62. package/src/vite-plugin-upstart-editor/runtime/types.ts +18 -4
  63. package/src/vite-plugin-upstart-theme.ts +0 -3
  64. package/src/vite-plugin-upstart-editor/PLAN.md +0 -1391
@@ -0,0 +1,79 @@
1
+ import { EditableEntry } from "./vite-plugin-upstart-attrs.js";
2
+ import z from "zod";
3
+
4
+ //#region src/upstart-editor-api.d.ts
5
+ declare const payloadEditText: z.ZodObject<{
6
+ action: z.ZodLiteral<"editText">;
7
+ language: z.ZodString;
8
+ namespace: z.ZodString;
9
+ key: z.ZodString;
10
+ content: z.ZodString;
11
+ }, z.core.$strip>;
12
+ type PayloadEditText = z.infer<typeof payloadEditText>;
13
+ declare const payloadEditClassName: z.ZodObject<{
14
+ action: z.ZodLiteral<"editClassName">;
15
+ id: z.ZodString;
16
+ className: z.ZodString;
17
+ }, z.core.$strip>;
18
+ type PayloadEditClassName = z.infer<typeof payloadEditClassName>;
19
+ interface EditableRegistry {
20
+ version: number;
21
+ generatedAt: string;
22
+ elements: Record<string, EditableEntry>;
23
+ }
24
+ type EditResult = {
25
+ success: true;
26
+ error?: never;
27
+ filePath: string;
28
+ } | {
29
+ success: false;
30
+ error: string;
31
+ filePath?: never;
32
+ };
33
+ declare class UpstartEditorAPI {
34
+ private registry;
35
+ private projectRoot;
36
+ private registryPath;
37
+ constructor(projectRoot: string, registryPath: string);
38
+ /**
39
+ * Load the registry from disk
40
+ */
41
+ loadRegistry(): Promise<void>;
42
+ /**
43
+ * Get the current registry (for testing/debugging)
44
+ */
45
+ getRegistry(): EditableRegistry | null;
46
+ /**
47
+ * Set the registry directly (for testing)
48
+ */
49
+ setRegistry(registry: EditableRegistry): void;
50
+ /**
51
+ * Edit a translation value in an i18next locale file.
52
+ * Auto-detects flat keys (e.g. "nav.home" as literal key) vs nested keys (e.g. nav -> home).
53
+ * Only updates existing keys — returns an error if the key is not found.
54
+ */
55
+ editText(params: PayloadEditText): Promise<EditResult>;
56
+ /**
57
+ * Edit the className of an element
58
+ */
59
+ editClassName(params: PayloadEditClassName): Promise<EditResult>;
60
+ /**
61
+ * Apply an edit to a source file
62
+ */
63
+ private applyEdit;
64
+ /**
65
+ * Get element info by ID
66
+ */
67
+ getElement(id: string): EditableEntry | undefined;
68
+ /**
69
+ * Get all elements of a specific type
70
+ */
71
+ getElementsByType(type: "text" | "className"): Record<string, EditableEntry>;
72
+ /**
73
+ * Get all elements in a specific file
74
+ */
75
+ getElementsByFile(file: string): Record<string, EditableEntry>;
76
+ }
77
+ //#endregion
78
+ export { EditResult, EditableRegistry, PayloadEditClassName, PayloadEditText, UpstartEditorAPI, payloadEditClassName, payloadEditText };
79
+ //# sourceMappingURL=upstart-editor-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstart-editor-api.d.ts","names":[],"sources":["../src/upstart-editor-api.ts"],"sourcesContent":[],"mappings":";;;;cAMa,iBAAe,CAAA,CAAA;;EAAf,QAAA,aASX;;;;;KAEU,eAAA,GAAkB,CAAA,CAAE,aAAa;cAEhC,sBAAoB,CAAA,CAAA;EAbL,MAAA,cAAA,CAAA,eAAA,CAAA;EAAA,EAAA,aAAA;EAWhB,SAAA,aAAe;AAE3B,CAAA,eAAa,CAAA;KAMD,oBAAA,GAAuB,CAAA,CAAE,aAAa;UAEjC,gBAAA;;;EARgB,QAAA,EAWrB,MAXqB,CAAA,MAAA,EAWN,aAXM,CAAA;;AAMrB,KAQA,UAAA,GARA;EAEK,OAAA,EAAA,IAAA;EAML,KAAA,CAAA,EAAA,KAAU;EAYT,QAAA,EAAA,MAAA;CAaW,GAAA;EAQP,OAAA,EAAA,KAAA;EAOO,KAAA,EAAA,MAAA;EASC,QAAA,CAAA,EAAA,KAAA;CAA0B;AAAR,cArC9B,gBAAA,CAqC8B;EAwDb,QAAA,QAAA;EAA+B,QAAA,WAAA;EAAR,QAAA,YAAA;EAyF3B,WAAA,CAAA,WAAA,EAAA,MAAA,EAAA,YAAA,EAAA,MAAA;EAOsC;;;EAiB7B,YAAA,CAAA,CAAA,EAjMX,OAiMW,CAAA,IAAA,CAAA;EAAM;;;iBAzLxB;;;;wBAOO;;;;;;mBASC,kBAAkB,QAAQ;;;;wBAwDrB,uBAAuB,QAAQ;;;;;;;;0BAyFnC;;;;iDAOuB,eAAe;;;;mCAiB7B,eAAe"}
@@ -0,0 +1,208 @@
1
+ import MagicString from "magic-string";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import z from "zod";
5
+
6
+ //#region src/upstart-editor-api.ts
7
+ const payloadEditText = z.object({
8
+ action: z.literal("editText"),
9
+ language: z.string().length(2).regex(/^[a-z]{2}$/),
10
+ namespace: z.string().regex(/^[a-z0-9_-]+$/),
11
+ key: z.string().regex(/^[a-zA-Z0-9_.-]+$/),
12
+ content: z.string()
13
+ });
14
+ const payloadEditClassName = z.object({
15
+ action: z.literal("editClassName"),
16
+ id: z.string().min(1),
17
+ className: z.string()
18
+ });
19
+ var UpstartEditorAPI = class {
20
+ registry = null;
21
+ projectRoot;
22
+ registryPath;
23
+ constructor(projectRoot, registryPath) {
24
+ this.projectRoot = projectRoot;
25
+ this.registryPath = registryPath;
26
+ }
27
+ /**
28
+ * Load the registry from disk
29
+ */
30
+ async loadRegistry() {
31
+ const content = await fs.readFile(this.registryPath, "utf-8");
32
+ this.registry = JSON.parse(content);
33
+ }
34
+ /**
35
+ * Get the current registry (for testing/debugging)
36
+ */
37
+ getRegistry() {
38
+ return this.registry;
39
+ }
40
+ /**
41
+ * Set the registry directly (for testing)
42
+ */
43
+ setRegistry(registry) {
44
+ this.registry = registry;
45
+ }
46
+ /**
47
+ * Edit a translation value in an i18next locale file.
48
+ * Auto-detects flat keys (e.g. "nav.home" as literal key) vs nested keys (e.g. nav -> home).
49
+ * Only updates existing keys — returns an error if the key is not found.
50
+ */
51
+ async editText(params) {
52
+ const parsed = payloadEditText.safeParse(params);
53
+ if (!parsed.success) return {
54
+ success: false,
55
+ error: `Invalid payload: ${parsed.error.message}`
56
+ };
57
+ const { language, namespace, key, content: newContent } = parsed.data;
58
+ const filePath = path.join(this.projectRoot, "app", "locales", language, `${namespace}.json`);
59
+ let raw;
60
+ try {
61
+ raw = await fs.readFile(filePath, "utf-8");
62
+ } catch (err) {
63
+ return {
64
+ success: false,
65
+ error: `Failed to read locale file: ${filePath}`
66
+ };
67
+ }
68
+ let data;
69
+ try {
70
+ data = JSON.parse(raw);
71
+ } catch (err) {
72
+ return {
73
+ success: false,
74
+ error: `Failed to parse locale file: ${filePath}`
75
+ };
76
+ }
77
+ if (key in data && typeof data[key] === "string") data[key] = newContent;
78
+ else {
79
+ const parts = key.split(".");
80
+ let current = data;
81
+ for (let i = 0; i < parts.length - 1; i++) {
82
+ const part = parts[i];
83
+ if (current[part] == null || typeof current[part] !== "object") return {
84
+ success: false,
85
+ error: `Key "${key}" not found in locale file ${filePath}`
86
+ };
87
+ current = current[part];
88
+ }
89
+ const leafKey = parts[parts.length - 1];
90
+ if (!(leafKey in current) || typeof current[leafKey] !== "string") return {
91
+ success: false,
92
+ error: `Key "${key}" not found in locale file ${filePath}`
93
+ };
94
+ current[leafKey] = newContent;
95
+ }
96
+ try {
97
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
98
+ } catch (err) {
99
+ return {
100
+ success: false,
101
+ error: `Failed to write locale file: ${filePath}`
102
+ };
103
+ }
104
+ return {
105
+ success: true,
106
+ filePath
107
+ };
108
+ }
109
+ /**
110
+ * Edit the className of an element
111
+ */
112
+ async editClassName(params) {
113
+ const parsed = payloadEditClassName.safeParse(params);
114
+ if (!parsed.success) return {
115
+ success: false,
116
+ error: `Invalid payload: ${parsed.error.message}`
117
+ };
118
+ const { id, className: newClassName } = parsed.data;
119
+ if (!this.registry) try {
120
+ await this.loadRegistry();
121
+ } catch (err) {
122
+ return {
123
+ success: false,
124
+ error: `Failed to load registry: ${err}`
125
+ };
126
+ }
127
+ const entry = this.registry.elements[id];
128
+ if (!entry) return {
129
+ success: false,
130
+ error: `Element ${id} not found in registry`
131
+ };
132
+ if (entry.type !== "className") return {
133
+ success: false,
134
+ error: `Element ${id} is not a className element (type: ${entry.type})`
135
+ };
136
+ return this.applyEdit(id, entry, newClassName);
137
+ }
138
+ /**
139
+ * Apply an edit to a source file
140
+ */
141
+ async applyEdit(id, entry, newContent) {
142
+ const filePath = path.join(this.projectRoot, entry.file);
143
+ try {
144
+ const code = await fs.readFile(filePath, "utf-8");
145
+ const currentContent = code.slice(entry.startOffset, entry.endOffset);
146
+ let actualStart = entry.startOffset;
147
+ let actualEnd = entry.endOffset;
148
+ if (currentContent !== entry.originalContent) {
149
+ const searchIndex = code.indexOf(entry.originalContent);
150
+ if (searchIndex === -1) return {
151
+ success: false,
152
+ error: `Original content "${entry.originalContent}" not found in file ${entry.file}. The file may have been modified.`
153
+ };
154
+ actualStart = searchIndex;
155
+ actualEnd = searchIndex + entry.originalContent.length;
156
+ }
157
+ const s = new MagicString(code);
158
+ s.overwrite(actualStart, actualEnd, newContent);
159
+ await fs.writeFile(filePath, s.toString());
160
+ const lengthDiff = newContent.length - entry.originalContent.length;
161
+ entry.startOffset = actualStart;
162
+ entry.endOffset = actualStart + newContent.length;
163
+ entry.originalContent = newContent;
164
+ for (const [otherId, otherEntry] of Object.entries(this.registry.elements)) if (otherId !== id && otherEntry.file === entry.file && otherEntry.startOffset > actualStart) {
165
+ otherEntry.startOffset += lengthDiff;
166
+ otherEntry.endOffset += lengthDiff;
167
+ }
168
+ await fs.writeFile(this.registryPath, JSON.stringify(this.registry, null, 2));
169
+ return {
170
+ success: true,
171
+ filePath
172
+ };
173
+ } catch (err) {
174
+ return {
175
+ success: false,
176
+ error: String(err)
177
+ };
178
+ }
179
+ }
180
+ /**
181
+ * Get element info by ID
182
+ */
183
+ getElement(id) {
184
+ return this.registry?.elements[id];
185
+ }
186
+ /**
187
+ * Get all elements of a specific type
188
+ */
189
+ getElementsByType(type) {
190
+ if (!this.registry) return {};
191
+ const result = {};
192
+ for (const [id, entry] of Object.entries(this.registry.elements)) if (entry.type === type) result[id] = entry;
193
+ return result;
194
+ }
195
+ /**
196
+ * Get all elements in a specific file
197
+ */
198
+ getElementsByFile(file) {
199
+ if (!this.registry) return {};
200
+ const result = {};
201
+ for (const [id, entry] of Object.entries(this.registry.elements)) if (entry.file === file) result[id] = entry;
202
+ return result;
203
+ }
204
+ };
205
+
206
+ //#endregion
207
+ export { UpstartEditorAPI, payloadEditClassName, payloadEditText };
208
+ //# sourceMappingURL=upstart-editor-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstart-editor-api.js","names":["raw: string","data: Record<string, unknown>","current: Record<string, unknown>","result: Record<string, EditableEntry>"],"sources":["../src/upstart-editor-api.ts"],"sourcesContent":["import MagicString from \"magic-string\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport z from \"zod\";\nimport type { EditableEntry } from \"./vite-plugin-upstart-attrs\";\n\nexport const payloadEditText = z.object({\n action: z.literal(\"editText\"),\n language: z\n .string()\n .length(2)\n .regex(/^[a-z]{2}$/),\n namespace: z.string().regex(/^[a-z0-9_-]+$/),\n key: z.string().regex(/^[a-zA-Z0-9_.-]+$/),\n content: z.string(),\n});\n\nexport type PayloadEditText = z.infer<typeof payloadEditText>;\n\nexport const payloadEditClassName = z.object({\n action: z.literal(\"editClassName\"),\n id: z.string().min(1),\n className: z.string(),\n});\n\nexport type PayloadEditClassName = z.infer<typeof payloadEditClassName>;\n\nexport interface EditableRegistry {\n version: number;\n generatedAt: string;\n elements: Record<string, EditableEntry>;\n}\n\nexport type EditResult =\n | {\n success: true;\n error?: never;\n filePath: string;\n }\n | {\n success: false;\n error: string;\n filePath?: never;\n };\n\nexport class UpstartEditorAPI {\n private registry: EditableRegistry | null = null;\n private projectRoot: string;\n private registryPath: string;\n\n constructor(projectRoot: string, registryPath: string) {\n this.projectRoot = projectRoot;\n this.registryPath = registryPath;\n }\n\n /**\n * Load the registry from disk\n */\n async loadRegistry(): Promise<void> {\n const content = await fs.readFile(this.registryPath, \"utf-8\");\n this.registry = JSON.parse(content);\n }\n\n /**\n * Get the current registry (for testing/debugging)\n */\n getRegistry(): EditableRegistry | null {\n return this.registry;\n }\n\n /**\n * Set the registry directly (for testing)\n */\n setRegistry(registry: EditableRegistry): void {\n this.registry = registry;\n }\n\n /**\n * Edit a translation value in an i18next locale file.\n * Auto-detects flat keys (e.g. \"nav.home\" as literal key) vs nested keys (e.g. nav -> home).\n * Only updates existing keys — returns an error if the key is not found.\n */\n async editText(params: PayloadEditText): Promise<EditResult> {\n const parsed = payloadEditText.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { language, namespace, key, content: newContent } = parsed.data;\n const filePath = path.join(this.projectRoot, \"app\", \"locales\", language, `${namespace}.json`);\n\n let raw: string;\n try {\n raw = await fs.readFile(filePath, \"utf-8\");\n } catch (err) {\n return { success: false, error: `Failed to read locale file: ${filePath}` };\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw);\n } catch (err) {\n return { success: false, error: `Failed to parse locale file: ${filePath}` };\n }\n\n // Strategy 1: check for flat/literal key at top level\n if (key in data && typeof data[key] === \"string\") {\n data[key] = newContent;\n } else {\n // Strategy 2: nested traversal via dot notation\n const parts = key.split(\".\");\n let current: Record<string, unknown> = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (current[part] == null || typeof current[part] !== \"object\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n const leafKey = parts[parts.length - 1];\n if (!(leafKey in current) || typeof current[leafKey] !== \"string\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current[leafKey] = newContent;\n }\n\n try {\n await fs.writeFile(filePath, JSON.stringify(data, null, 2) + \"\\n\");\n } catch (err) {\n return { success: false, error: `Failed to write locale file: ${filePath}` };\n }\n\n return { success: true, filePath };\n }\n\n /**\n * Edit the className of an element\n */\n async editClassName(params: PayloadEditClassName): Promise<EditResult> {\n const parsed = payloadEditClassName.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, className: newClassName } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n\n if (entry.type !== \"className\") {\n return { success: false, error: `Element ${id} is not a className element (type: ${entry.type})` };\n }\n\n return this.applyEdit(id, entry, newClassName);\n }\n\n /**\n * Apply an edit to a source file\n */\n private async applyEdit(id: string, entry: EditableEntry, newContent: string): Promise<EditResult> {\n const filePath = path.join(this.projectRoot, entry.file);\n\n try {\n const code = await fs.readFile(filePath, \"utf-8\");\n\n // Verify content at expected location\n const currentContent = code.slice(entry.startOffset, entry.endOffset);\n\n let actualStart = entry.startOffset;\n let actualEnd = entry.endOffset;\n\n if (currentContent !== entry.originalContent) {\n // Content has shifted - try to find it by searching\n const searchIndex = code.indexOf(entry.originalContent);\n if (searchIndex === -1) {\n return {\n success: false,\n error: `Original content \"${entry.originalContent}\" not found in file ${entry.file}. The file may have been modified.`,\n };\n }\n actualStart = searchIndex;\n actualEnd = searchIndex + entry.originalContent.length;\n }\n\n // Apply the edit using MagicString\n const s = new MagicString(code);\n s.overwrite(actualStart, actualEnd, newContent);\n\n // Write the modified file\n await fs.writeFile(filePath, s.toString());\n\n // Calculate the length difference for offset adjustments\n const lengthDiff = newContent.length - entry.originalContent.length;\n\n // Update the registry entry\n entry.startOffset = actualStart;\n entry.endOffset = actualStart + newContent.length;\n entry.originalContent = newContent;\n\n // Shift all subsequent entries in the same file\n for (const [otherId, otherEntry] of Object.entries(this.registry!.elements)) {\n if (otherId !== id && otherEntry.file === entry.file && otherEntry.startOffset > actualStart) {\n otherEntry.startOffset += lengthDiff;\n otherEntry.endOffset += lengthDiff;\n }\n }\n\n // Save the updated registry\n await fs.writeFile(this.registryPath, JSON.stringify(this.registry, null, 2));\n\n return { success: true, filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Get element info by ID\n */\n getElement(id: string): EditableEntry | undefined {\n return this.registry?.elements[id];\n }\n\n /**\n * Get all elements of a specific type\n */\n getElementsByType(type: \"text\" | \"className\"): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.type === type) {\n result[id] = entry;\n }\n }\n return result;\n }\n\n /**\n * Get all elements in a specific file\n */\n getElementsByFile(file: string): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.file === file) {\n result[id] = entry;\n }\n }\n return result;\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import * as unplugin5 from "unplugin";
2
1
  import * as magic_string0 from "magic-string";
2
+ import * as unplugin0 from "unplugin";
3
3
 
4
4
  //#region src/vite-plugin-upstart-attrs.d.ts
5
5
  interface Options {
@@ -18,12 +18,12 @@ interface EditableEntry {
18
18
  }
19
19
  declare function getRegistry(): Record<string, EditableEntry>;
20
20
  declare function clearRegistry(): void;
21
- declare const upstartEditor: unplugin5.UnpluginInstance<Options, boolean>;
21
+ declare const upstartEditor: unplugin0.UnpluginInstance<Options, boolean>;
22
22
  declare function transformWithOxc(code: string, filePath: string): {
23
23
  code: string;
24
24
  map: magic_string0.SourceMap;
25
25
  } | null;
26
- declare const _default: (options: Options) => unplugin5.VitePlugin<any> | unplugin5.VitePlugin<any>[];
26
+ declare const _default: (options: Options) => unplugin0.VitePlugin<any> | unplugin0.VitePlugin<any>[];
27
27
  //#endregion
28
28
  export { EditableEntry, clearRegistry, _default as default, getRegistry, transformWithOxc, upstartEditor };
29
29
  //# sourceMappingURL=vite-plugin-upstart-attrs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin-upstart-attrs.d.ts","names":[],"sources":["../src/vite-plugin-upstart-attrs.ts"],"sourcesContent":[],"mappings":";;;;UAgBU,OAAA;;;;AAAA,UAuCO,aAAA,CAvCA;EAuCA,IAAA,EAAA,MAAA;EAoBD,IAAA,EAAA,MAAA,GAAW,WAAmB;EAK9B,WAAA,EAAA,MAAa;EAWhB,SAAA,EAAA,MAkEX;EAEc,eAAA,EAAA,MAAgB;EA+L/B,OAAA,EAAA;;;;iBAnRe,WAAA,CAAA,GAAe,eAAe;iBAK9B,aAAA,CAAA;cAWH,eAAa,SAAA,CAAA,iBAAA;iBAoEV,gBAAA;;OAAgB,aAAA,CAAA;;cA+L/B"}
1
+ {"version":3,"file":"vite-plugin-upstart-attrs.d.ts","names":[],"sources":["../src/vite-plugin-upstart-attrs.ts"],"sourcesContent":[],"mappings":";;;;UAgBU,OAAA;;;;AAAA,UA0CO,aAAA,CA1CA;EA0CA,IAAA,EAAA,MAAA;EAoBD,IAAA,EAAA,MAAA,GAAW,WAAmB;EAK9B,WAAA,EAAA,MAAa;EAehB,SAAA,EAAA,MAkEX;EAEc,eAAA,EAAA,MAAgB;EA0P/B,OAAA,EAAA;;;;iBAlVe,WAAA,CAAA,GAAe,eAAe;iBAK9B,aAAA,CAAA;cAeH,eAAa,SAAA,CAAA,iBAAA;iBAoEV,gBAAA;;OAAgB,aAAA,CAAA;;cA0P/B"}
@@ -1,8 +1,8 @@
1
+ import MagicString from "magic-string";
2
+ import path from "node:path";
1
3
  import { createUnplugin } from "unplugin";
2
4
  import { parseSync } from "oxc-parser";
3
5
  import { walk } from "zimmerframe";
4
- import MagicString from "magic-string";
5
- import path from "node:path";
6
6
 
7
7
  //#region src/vite-plugin-upstart-attrs.ts
8
8
  function hasRange(node) {
@@ -76,11 +76,16 @@ function transformWithOxc(code, filePath) {
76
76
  filePath,
77
77
  code,
78
78
  s,
79
- loopStack: []
79
+ loopStack: [],
80
+ constants: /* @__PURE__ */ new Map(),
81
+ tFunctions: /* @__PURE__ */ new Map()
80
82
  };
81
83
  let modified = false;
82
84
  walk(ast.program, state, { _(node, { state: state$1, next }) {
83
- if (node.type === "VariableDeclaration") checkForbiddenUseTranslation(node, state$1.filePath);
85
+ if (node.type === "VariableDeclaration") {
86
+ trackUseTranslation(node, state$1);
87
+ for (const decl of node.declarations) if (decl.type === "VariableDeclarator" && decl.id?.type === "Identifier" && decl.init?.type === "Literal" && typeof decl.init.value === "string") state$1.constants.set(decl.id.name, decl.init.value);
88
+ }
84
89
  if (node.type === "CallExpression") {
85
90
  const loopInfo = detectMapCall(node, state$1.code);
86
91
  if (loopInfo) {
@@ -94,14 +99,38 @@ function transformWithOxc(code, filePath) {
94
99
  const jsxNode = node;
95
100
  const opening = jsxNode.openingElement;
96
101
  const tagName = getJSXElementName(opening);
102
+ if (tagName === "Trans") {
103
+ next();
104
+ return;
105
+ }
97
106
  const insertPos = getAttributeInsertPosition(opening, state$1.code);
98
107
  const attributes = [];
99
108
  if (hasRange(jsxNode)) {
100
109
  const hash = hashContent(state$1.code.slice(jsxNode.start, jsxNode.end));
101
- attributes.push(`data-upstart-hash="${hash}"`);
110
+ const loopIndices = state$1.loopStack.map((l) => l.indexName).filter((n) => n !== null);
111
+ if (loopIndices.length > 0) {
112
+ const suffix = loopIndices.map((n) => `\${${n}}`).join("-");
113
+ attributes.push(`data-upstart-hash={\`${hash}-${suffix}\`}`);
114
+ } else attributes.push(`data-upstart-hash="${hash}"`);
102
115
  }
103
- if (isTextLeafElement(jsxNode)) {
116
+ const transChildren = findTransInChildren(jsxNode, state$1.code, state$1.constants);
117
+ const tCallChildren = findTCallsInChildren(jsxNode, state$1.code, state$1);
118
+ const allI18nKeys = [...transChildren, ...tCallChildren];
119
+ const hasI18n = allI18nKeys.length > 0;
120
+ if (hasI18n) {
104
121
  attributes.push("data-upstart-editable-text=\"true\"");
122
+ if (allI18nKeys.some((t) => t.keyExpr || t.nsExpr)) {
123
+ const parts = allI18nKeys.map((t) => {
124
+ return `${t.nsExpr ? `\${${t.nsExpr}}` : t.namespace}:${t.keyExpr ? `\${${t.keyExpr}}` : t.key}`;
125
+ });
126
+ attributes.push(`data-upstart-i18n={\`${parts.join(",")}\`}`);
127
+ } else {
128
+ const keys = allI18nKeys.map((t) => escapeProp(t.fullKey)).join(",");
129
+ attributes.push(`data-upstart-i18n="${keys}"`);
130
+ }
131
+ }
132
+ if (isTextLeafElement(jsxNode)) {
133
+ if (!hasI18n) attributes.push("data-upstart-editable-text=\"false\"");
105
134
  const textChild = jsxNode.children.find((c) => c.type === "JSXText" && c.value?.trim());
106
135
  if (textChild && hasRange(textChild)) {
107
136
  const id = generateId(state$1.filePath, textChild);
@@ -118,12 +147,7 @@ function transformWithOxc(code, filePath) {
118
147
  });
119
148
  attributes.push(`data-upstart-id="${id}"`);
120
149
  }
121
- }
122
- const transInfo = detectTransComponent(jsxNode, state$1.code);
123
- if (transInfo) {
124
- attributes.push("data-upstart-editable-text=\"true\"");
125
- attributes.push(`data-i18n-key="${escapeProp(transInfo.fullKey)}"`);
126
- }
150
+ } else if (!hasI18n && hasVisibleTextContent(jsxNode)) attributes.push("data-upstart-editable-text=\"false\"");
127
151
  const classNameAttr = opening.attributes.find((attr) => attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === "className" && attr.value?.type === "Literal" && typeof attr.value.value === "string");
128
152
  if (classNameAttr && classNameAttr.value && hasRange(classNameAttr.value)) {
129
153
  const id = generateId(state$1.filePath, classNameAttr.value);
@@ -254,10 +278,10 @@ function extractDatasource(expr) {
254
278
  }
255
279
  function extractRecordId(expr) {
256
280
  const obj = expr.object;
257
- if (obj.type === "Identifier") return `${obj.name}.id`;
281
+ if (obj.type === "Identifier") return `${obj.name}.$id`;
258
282
  if (obj.type === "MemberExpression") {
259
283
  const datasource = extractDatasource(obj);
260
- if (datasource) return `${datasource}.id`;
284
+ if (datasource) return `${datasource}.$id`;
261
285
  }
262
286
  }
263
287
  function detectMapCall(node, code) {
@@ -278,37 +302,201 @@ function detectMapCall(node, code) {
278
302
  function escapeProp(value) {
279
303
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&apos;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
280
304
  }
281
- function checkForbiddenUseTranslation(node, filePath) {
305
+ function trackUseTranslation(node, state) {
282
306
  if (node.type !== "VariableDeclaration") return;
283
307
  const decl = node;
284
308
  for (const declarator of decl.declarations) {
285
309
  if (declarator.id?.type !== "ObjectPattern" || !declarator.init || declarator.init.type !== "CallExpression") continue;
286
310
  const callExpr = declarator.init;
287
311
  if (callExpr.callee?.type !== "Identifier" || callExpr.callee.name !== "useTranslation") continue;
312
+ const namespace = extractUseTranslationNamespace(callExpr);
288
313
  for (const prop of declarator.id.properties) {
289
314
  if (prop.type !== "Property") continue;
290
- if (prop.key?.type === "Identifier" && prop.key.name === "t") throw new Error(`[${filePath}] useTranslation hook is forbidden for translations. Use <Trans i18nKey="..." /> component instead. Only { i18n } destructuring is allowed (for language switching).`);
315
+ if (prop.key?.type !== "Identifier" || prop.key.name !== "t") continue;
316
+ const localName = prop.value?.type === "Identifier" ? prop.value.name : "t";
317
+ state.tFunctions.set(localName, namespace);
291
318
  }
292
319
  }
293
320
  }
294
- function detectTransComponent(jsxElement, code) {
321
+ function extractUseTranslationNamespace(callExpr) {
322
+ const firstArg = callExpr.arguments?.[0];
323
+ if (!firstArg) return "translation";
324
+ if (firstArg.type === "Literal" && typeof firstArg.value === "string") return firstArg.value;
325
+ if (firstArg.type === "ArrayExpression" && firstArg.elements?.length > 0) {
326
+ const first = firstArg.elements[0];
327
+ if (first?.type === "Literal" && typeof first.value === "string") return first.value;
328
+ }
329
+ return "translation";
330
+ }
331
+ function resolveJSXAttrValue(value, code, constants) {
332
+ if (!value) return null;
333
+ if (value.type === "Literal" && typeof value.value === "string") return {
334
+ value: value.value,
335
+ expr: null
336
+ };
337
+ if (value.type === "JSXExpressionContainer") {
338
+ const expression = value.expression;
339
+ if (!expression) return null;
340
+ if (expression.type === "Identifier" && constants.has(expression.name)) return {
341
+ value: constants.get(expression.name),
342
+ expr: null
343
+ };
344
+ if (hasRange(expression)) {
345
+ const src = code.slice(expression.start, expression.end);
346
+ return {
347
+ value: src,
348
+ expr: src
349
+ };
350
+ }
351
+ }
352
+ return null;
353
+ }
354
+ function detectTransComponent(jsxElement, code, constants) {
295
355
  const opening = jsxElement.openingElement;
296
356
  if (getJSXElementName(opening) !== "Trans") return null;
297
- let i18nKey = null;
298
- let namespace = "translation";
357
+ let keyResult = null;
358
+ let nsResult = null;
299
359
  for (const attr of opening.attributes) {
300
360
  if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
301
361
  const attrName = attr.name.name;
302
- if (attrName === "i18nKey" && attr.value?.type === "Literal") i18nKey = attr.value.value;
303
- if (attrName === "ns" && attr.value?.type === "Literal") namespace = attr.value.value;
362
+ if (attrName === "i18nKey") keyResult = resolveJSXAttrValue(attr.value, code, constants);
363
+ if (attrName === "ns") nsResult = resolveJSXAttrValue(attr.value, code, constants);
304
364
  }
305
- if (!i18nKey) return null;
365
+ if (!keyResult) return null;
366
+ const key = keyResult.value;
367
+ const namespace = nsResult?.value ?? "translation";
368
+ const keyExpr = keyResult.expr;
369
+ const nsExpr = nsResult?.expr ?? null;
306
370
  return {
307
- fullKey: `${namespace}:${i18nKey}`,
308
- key: i18nKey,
309
- namespace
371
+ fullKey: `${namespace}:${key}`,
372
+ key,
373
+ namespace,
374
+ keyExpr,
375
+ nsExpr
310
376
  };
311
377
  }
378
+ function findTransInChildren(jsxElement, code, constants) {
379
+ const results = [];
380
+ for (const child of jsxElement.children) {
381
+ if (child.type === "JSXElement") {
382
+ const info = detectTransComponent(child, code, constants);
383
+ if (info) results.push(info);
384
+ }
385
+ if (child.type === "JSXExpressionContainer") {
386
+ const expr = child.expression;
387
+ if (expr && expr.type !== "JSXEmptyExpression") findTransInExpression(expr, code, results, constants);
388
+ }
389
+ }
390
+ return results;
391
+ }
392
+ function findTransInExpression(expr, code, results, constants) {
393
+ if (!expr || !expr.type) return;
394
+ if (expr.type === "JSXElement") {
395
+ const info = detectTransComponent(expr, code, constants);
396
+ if (info) results.push(info);
397
+ return;
398
+ }
399
+ if (expr.type === "LogicalExpression") {
400
+ findTransInExpression(expr.left, code, results, constants);
401
+ findTransInExpression(expr.right, code, results, constants);
402
+ return;
403
+ }
404
+ if (expr.type === "ConditionalExpression") {
405
+ findTransInExpression(expr.consequent, code, results, constants);
406
+ findTransInExpression(expr.alternate, code, results, constants);
407
+ return;
408
+ }
409
+ if (expr.type === "CallExpression") {
410
+ for (const arg of expr.arguments) findTransInExpression(arg, code, results, constants);
411
+ return;
412
+ }
413
+ if (expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") {
414
+ findTransInExpression(expr.body, code, results, constants);
415
+ return;
416
+ }
417
+ if (expr.type === "BlockStatement") {
418
+ for (const stmt of expr.body) if (stmt.type === "ReturnStatement" && stmt.argument) findTransInExpression(stmt.argument, code, results, constants);
419
+ return;
420
+ }
421
+ }
422
+ function findTCallsInChildren(jsxElement, code, state) {
423
+ if (state.tFunctions.size === 0) return [];
424
+ const results = [];
425
+ for (const child of jsxElement.children) if (child.type === "JSXExpressionContainer") {
426
+ const expr = child.expression;
427
+ if (expr && expr.type !== "JSXEmptyExpression") findTCallInExpression(expr, code, results, state);
428
+ }
429
+ return results;
430
+ }
431
+ function findTCallInExpression(expr, code, results, state) {
432
+ if (!expr || !expr.type) return;
433
+ if (expr.type === "CallExpression") {
434
+ const info = detectTCall(expr, code, state);
435
+ if (info) {
436
+ results.push(info);
437
+ return;
438
+ }
439
+ for (const arg of expr.arguments) findTCallInExpression(arg, code, results, state);
440
+ return;
441
+ }
442
+ if (expr.type === "LogicalExpression") {
443
+ findTCallInExpression(expr.left, code, results, state);
444
+ findTCallInExpression(expr.right, code, results, state);
445
+ return;
446
+ }
447
+ if (expr.type === "ConditionalExpression") {
448
+ findTCallInExpression(expr.consequent, code, results, state);
449
+ findTCallInExpression(expr.alternate, code, results, state);
450
+ return;
451
+ }
452
+ if (expr.type === "ArrowFunctionExpression" || expr.type === "FunctionExpression") {
453
+ findTCallInExpression(expr.body, code, results, state);
454
+ return;
455
+ }
456
+ if (expr.type === "BlockStatement") {
457
+ for (const stmt of expr.body) if (stmt.type === "ReturnStatement" && stmt.argument) findTCallInExpression(stmt.argument, code, results, state);
458
+ return;
459
+ }
460
+ }
461
+ function detectTCall(callExpr, code, state) {
462
+ if (callExpr.callee?.type !== "Identifier") return null;
463
+ const calleeName = callExpr.callee.name;
464
+ const namespace = state.tFunctions.get(calleeName);
465
+ if (namespace === void 0) return null;
466
+ const firstArg = callExpr.arguments?.[0];
467
+ if (!firstArg) return null;
468
+ if (firstArg.type === "Literal" && typeof firstArg.value === "string") {
469
+ const key = firstArg.value;
470
+ return {
471
+ fullKey: `${namespace}:${key}`,
472
+ key,
473
+ namespace,
474
+ keyExpr: null,
475
+ nsExpr: null
476
+ };
477
+ }
478
+ if (hasRange(firstArg)) {
479
+ const exprSrc = code.slice(firstArg.start, firstArg.end);
480
+ if (firstArg.type === "Identifier" && state.constants.has(firstArg.name)) {
481
+ const key = state.constants.get(firstArg.name);
482
+ return {
483
+ fullKey: `${namespace}:${key}`,
484
+ key,
485
+ namespace,
486
+ keyExpr: null,
487
+ nsExpr: null
488
+ };
489
+ }
490
+ return {
491
+ fullKey: `${namespace}:${exprSrc}`,
492
+ key: exprSrc,
493
+ namespace,
494
+ keyExpr: exprSrc,
495
+ nsExpr: null
496
+ };
497
+ }
498
+ return null;
499
+ }
312
500
  function isTextLeafElement(jsxElement) {
313
501
  let hasText = false;
314
502
  for (const child of jsxElement.children) if (child.type === "JSXText") {
@@ -316,6 +504,20 @@ function isTextLeafElement(jsxElement) {
316
504
  } else if (child.type === "JSXElement" || child.type === "JSXFragment" || child.type === "JSXExpressionContainer" || child.type === "JSXSpreadChild") return false;
317
505
  return hasText;
318
506
  }
507
+ function hasVisibleTextContent(jsxElement) {
508
+ for (const child of jsxElement.children) {
509
+ if (child.type === "JSXText") {
510
+ if (child.value?.trim()) return true;
511
+ continue;
512
+ }
513
+ if (child.type === "JSXExpressionContainer") {
514
+ const expr = child.expression;
515
+ if (!expr || expr.type === "JSXEmptyExpression") continue;
516
+ if (expr.type === "Identifier" || expr.type === "MemberExpression" || expr.type === "Literal" || expr.type === "TemplateLiteral") return true;
517
+ }
518
+ }
519
+ return false;
520
+ }
319
521
  var vite_plugin_upstart_attrs_default = upstartEditor.vite;
320
522
 
321
523
  //#endregion