@upstart.gg/vite-plugins 0.0.39 → 0.0.41

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
@@ -66,21 +66,21 @@ describe("upstart-editor-vite-plugin", () => {
66
66
  test("should track record IDs for datasource bindings", () => {
67
67
  const code = `
68
68
  export default function App() {
69
- const user = { id: 1, name: "Alice" };
69
+ const user = { $id: 1, name: "Alice" };
70
70
  return <MyComponent name={user.name} />;
71
71
  }
72
72
  `;
73
73
 
74
74
  const result = transform(code);
75
75
 
76
- expect(result).toContain("data-upstart-record-id-name={user.id}");
76
+ expect(result).toContain("data-upstart-record-id-name={user.$id}");
77
77
  });
78
78
 
79
79
  test("should handle conditional prop expressions", () => {
80
80
  const code = `
81
81
  export default function App() {
82
82
  const isActive = true;
83
- const user = { name: "Alice" };
83
+ const user = { $id: 1, name: "Alice" };
84
84
  return <MyComponent name={isActive ? user.name : "Guest"} />;
85
85
  }
86
86
  `;
@@ -93,7 +93,7 @@ describe("upstart-editor-vite-plugin", () => {
93
93
  test("should handle logical prop expressions", () => {
94
94
  const code = `
95
95
  export default function App() {
96
- const user = { name: "Alice" };
96
+ const user = { $id: 1, name: "Alice" };
97
97
  return <MyComponent name={user && user.name} />;
98
98
  }
99
99
  `;
@@ -105,7 +105,7 @@ describe("upstart-editor-vite-plugin", () => {
105
105
  });
106
106
 
107
107
  describe("Static Text Detection", () => {
108
- test("should mark leaf elements with static text as editable", () => {
108
+ test("should mark leaf elements with static text as non-editable", () => {
109
109
  const code = `
110
110
  export default function App() {
111
111
  return <div>Hello World</div>;
@@ -114,10 +114,10 @@ describe("upstart-editor-vite-plugin", () => {
114
114
 
115
115
  const result = transform(code);
116
116
 
117
- expect(result).toContain('data-upstart-editable-text="true"');
117
+ expect(result).toContain('data-upstart-editable-text="false"');
118
118
  });
119
119
 
120
- test("should mark span with static text as editable", () => {
120
+ test("should mark span with static text as non-editable", () => {
121
121
  const code = `
122
122
  export default function App() {
123
123
  return <span>Click here</span>;
@@ -126,10 +126,10 @@ describe("upstart-editor-vite-plugin", () => {
126
126
 
127
127
  const result = transform(code);
128
128
 
129
- expect(result).toContain('data-upstart-editable-text="true"');
129
+ expect(result).toContain('data-upstart-editable-text="false"');
130
130
  });
131
131
 
132
- test("should mark PascalCase component with static text as editable AND track component", () => {
132
+ test("should mark PascalCase component with static text as non-editable AND track component", () => {
133
133
  const code = `
134
134
  export default function App() {
135
135
  return <Button>Submit</Button>;
@@ -138,12 +138,12 @@ describe("upstart-editor-vite-plugin", () => {
138
138
 
139
139
  const result = transform(code);
140
140
 
141
- // Should have both editable text AND component tracking
142
- expect(result).toContain('data-upstart-editable-text="true"');
141
+ // Should have non-editable text AND component tracking
142
+ expect(result).toContain('data-upstart-editable-text="false"');
143
143
  expect(result).toContain('data-upstart-component="Button"');
144
144
  });
145
145
 
146
- test("should NOT mark elements with nested elements", () => {
146
+ test("should NOT mark elements with nested elements (but mark the nested leaf)", () => {
147
147
  const code = `
148
148
  export default function App() {
149
149
  return <div><span>nested</span></div>;
@@ -152,15 +152,13 @@ describe("upstart-editor-vite-plugin", () => {
152
152
 
153
153
  const result = transform(code);
154
154
 
155
- // The outer div should NOT be marked (has nested element)
156
- // But the inner span SHOULD be marked (it's a leaf with text)
157
- expect(result).toContain('data-upstart-editable-text="true"');
158
- // Count occurrences - should only be 1 (for the span)
159
- const matches = result?.match(/data-upstart-editable-text="true"/g);
155
+ // The outer div should NOT be marked (no direct text content)
156
+ // The inner span SHOULD be marked as non-editable (text leaf without Trans)
157
+ const matches = result?.match(/data-upstart-editable-text="false"/g);
160
158
  expect(matches?.length).toBe(1);
161
159
  });
162
160
 
163
- test("should NOT mark elements with dynamic expressions", () => {
161
+ test("should mark elements with dynamic text expressions as non-editable", () => {
164
162
  const code = `
165
163
  export default function App() {
166
164
  const name = "World";
@@ -170,13 +168,12 @@ describe("upstart-editor-vite-plugin", () => {
170
168
 
171
169
  const result = transform(code);
172
170
 
173
- // Should NOT have editable-text (has dynamic expression)
174
- // But SHOULD have hash (all elements get hashes)
175
- expect(result).not.toContain('data-upstart-editable-text');
176
- expect(result).toContain('data-upstart-hash');
171
+ // Should have non-editable text (Identifier expression = text-producing)
172
+ expect(result).toContain('data-upstart-editable-text="false"');
173
+ expect(result).toContain("data-upstart-hash");
177
174
  });
178
175
 
179
- test("should NOT mark elements with mixed content", () => {
176
+ test("should mark elements with mixed content as non-editable", () => {
180
177
  const code = `
181
178
  export default function App() {
182
179
  return <div>Text <b>bold</b></div>;
@@ -185,10 +182,9 @@ describe("upstart-editor-vite-plugin", () => {
185
182
 
186
183
  const result = transform(code);
187
184
 
188
- // The outer div should NOT be marked (has nested element)
189
- // But the inner b SHOULD be marked
190
- const matches = result?.match(/data-upstart-editable-text="true"/g);
191
- expect(matches?.length).toBe(1);
185
+ // Both the outer div (has JSXText "Text ") and inner b (text leaf) get "false"
186
+ const matches = result?.match(/data-upstart-editable-text="false"/g);
187
+ expect(matches?.length).toBe(2);
192
188
  });
193
189
 
194
190
  test("should NOT mark empty elements as editable", () => {
@@ -202,8 +198,8 @@ describe("upstart-editor-vite-plugin", () => {
202
198
 
203
199
  // Should NOT have editable-text (empty)
204
200
  // But SHOULD have hash
205
- expect(result).not.toContain('data-upstart-editable-text');
206
- expect(result).toContain('data-upstart-hash');
201
+ expect(result).not.toContain("data-upstart-editable-text");
202
+ expect(result).toContain("data-upstart-hash");
207
203
  });
208
204
 
209
205
  test("should NOT mark whitespace-only elements as editable", () => {
@@ -217,8 +213,8 @@ describe("upstart-editor-vite-plugin", () => {
217
213
 
218
214
  // Should NOT have editable-text (whitespace only)
219
215
  // But SHOULD have hash
220
- expect(result).not.toContain('data-upstart-editable-text');
221
- expect(result).toContain('data-upstart-hash');
216
+ expect(result).not.toContain("data-upstart-editable-text");
217
+ expect(result).toContain("data-upstart-hash");
222
218
  });
223
219
 
224
220
  test("should handle multiple text leaf elements", () => {
@@ -235,12 +231,99 @@ describe("upstart-editor-vite-plugin", () => {
235
231
 
236
232
  const result = transform(code);
237
233
 
238
- // Both h1 and p should be marked
239
- const matches = result?.match(/data-upstart-editable-text="true"/g);
234
+ // Both h1 and p should be marked as non-editable
235
+ const matches = result?.match(/data-upstart-editable-text="false"/g);
240
236
  expect(matches?.length).toBe(2);
241
237
  });
242
238
  });
243
239
 
240
+ describe("Non-Editable Text Detection (expressions and edge cases)", () => {
241
+ test("should mark element with MemberExpression as non-editable", () => {
242
+ const code = `
243
+ export default function App() {
244
+ return <span>{user.name}</span>;
245
+ }
246
+ `;
247
+ const result = transform(code);
248
+ expect(result).toContain('data-upstart-editable-text="false"');
249
+ });
250
+
251
+ test("should mark element with template literal as non-editable", () => {
252
+ const code = `
253
+ export default function App() {
254
+ const name = "World";
255
+ return <div>{\`Hello \${name}\`}</div>;
256
+ }
257
+ `;
258
+ const result = transform(code);
259
+ expect(result).toContain('data-upstart-editable-text="false"');
260
+ });
261
+
262
+ test("should mark element with string literal expression as non-editable", () => {
263
+ const code = `
264
+ export default function App() {
265
+ return <div>{"Hello World"}</div>;
266
+ }
267
+ `;
268
+ const result = transform(code);
269
+ expect(result).toContain('data-upstart-editable-text="false"');
270
+ });
271
+
272
+ test("should NOT mark element containing only a function call expression", () => {
273
+ const code = `
274
+ export default function App() {
275
+ return <div>{formatDate(date)}</div>;
276
+ }
277
+ `;
278
+ const result = transform(code);
279
+ expect(result).not.toContain("data-upstart-editable-text");
280
+ });
281
+
282
+ test("should NOT mark element containing only conditional JSX", () => {
283
+ const code = `
284
+ export default function App() {
285
+ return <div>{show ? <A /> : <B />}</div>;
286
+ }
287
+ `;
288
+ const result = transform(code);
289
+ // Outer div should not get editable-text (ConditionalExpression is not text-producing)
290
+ expect(result).not.toContain('data-upstart-editable-text="false"');
291
+ });
292
+
293
+ test("should NOT mark element containing only logical expression with JSX", () => {
294
+ const code = `
295
+ export default function App() {
296
+ return <div>{show && <Child />}</div>;
297
+ }
298
+ `;
299
+ const result = transform(code);
300
+ expect(result).not.toContain('data-upstart-editable-text="false"');
301
+ });
302
+
303
+ test("should NOT mark self-closing elements", () => {
304
+ const code = `
305
+ export default function App() {
306
+ return <img src="photo.jpg" />;
307
+ }
308
+ `;
309
+ const result = transform(code);
310
+ expect(result).not.toContain("data-upstart-editable-text");
311
+ });
312
+
313
+ test("Trans should take priority over non-editable text", () => {
314
+ const code = `
315
+ import { Trans } from "react-i18next";
316
+ export default function App() {
317
+ return <div>{prefix}<Trans i18nKey="greeting" /></div>;
318
+ }
319
+ `;
320
+ const result = transform(code);
321
+ // Trans wins: should be "true", not "false"
322
+ expect(result).toContain('data-upstart-editable-text="true"');
323
+ expect(result).not.toContain('data-upstart-editable-text="false"');
324
+ });
325
+ });
326
+
244
327
  describe("Content Hash", () => {
245
328
  test("should add data-upstart-hash to elements", () => {
246
329
  const code = `
@@ -428,6 +511,27 @@ describe("upstart-editor-vite-plugin", () => {
428
511
  expect(result).toContain('data-upstart-loop-array="items"');
429
512
  });
430
513
 
514
+ test("should produce unique hashes for elements inside .map() with index", () => {
515
+ const code = `
516
+ export default function App() {
517
+ const items = ["a", "b"];
518
+ return (
519
+ <div>
520
+ {items.map((item, index) => (
521
+ <div key={index}>
522
+ <span>{item}</span>
523
+ </div>
524
+ ))}
525
+ </div>
526
+ );
527
+ }
528
+ `;
529
+ const result = transform(code);
530
+ // Elements inside the loop should have a dynamic hash with the index
531
+ expect(result).toContain("data-upstart-hash={`");
532
+ expect(result).toContain("${index}`}");
533
+ });
534
+
431
535
  test("should handle complex array expressions", () => {
432
536
  const code = `
433
537
  export default function App() {
@@ -484,7 +588,7 @@ describe("upstart-editor-vite-plugin", () => {
484
588
  const result = transform(code);
485
589
 
486
590
  // All elements should have hashes
487
- expect(result).toContain('data-upstart-hash');
591
+ expect(result).toContain("data-upstart-hash");
488
592
  // Self-closing elements should not have editable-text (no text content)
489
593
  const matches = result?.match(/data-upstart-editable-text/g);
490
594
  expect(matches).toBeNull();
@@ -556,8 +660,8 @@ describe("upstart-editor-vite-plugin", () => {
556
660
  const result = transform(code);
557
661
 
558
662
  // Should have hash but no editable-text (comment is not editable text)
559
- expect(result).toContain('data-upstart-hash');
560
- expect(result).not.toContain('data-upstart-editable-text');
663
+ expect(result).toContain("data-upstart-hash");
664
+ expect(result).not.toContain("data-upstart-editable-text");
561
665
  });
562
666
 
563
667
  test("should handle logical expression containing JSX elements", () => {
@@ -649,7 +753,6 @@ describe("upstart-editor-vite-plugin", () => {
649
753
  expect(result).toContain('data-upstart-component="ComponentA"');
650
754
  expect(result).toContain('data-upstart-component="ComponentB"');
651
755
  });
652
-
653
756
  });
654
757
 
655
758
  describe("Element ID Tracking", () => {
@@ -657,7 +760,7 @@ describe("upstart-editor-vite-plugin", () => {
657
760
  clearRegistry();
658
761
  });
659
762
 
660
- test("should add data-upstart-id to text-editable elements", () => {
763
+ test("should add data-upstart-id to text leaf elements", () => {
661
764
  const code = `
662
765
  export default function App() {
663
766
  return <div>Hello World</div>;
@@ -665,8 +768,8 @@ describe("upstart-editor-vite-plugin", () => {
665
768
  `;
666
769
  const result = transform(code);
667
770
 
668
- // Should have both editable-text AND an ID
669
- expect(result).toContain('data-upstart-editable-text="true"');
771
+ // Should have non-editable text AND an ID
772
+ expect(result).toContain('data-upstart-editable-text="false"');
670
773
  expect(result).toMatch(/data-upstart-id="test\.tsx:\d+"/);
671
774
  });
672
775
 
@@ -716,7 +819,7 @@ describe("upstart-editor-vite-plugin", () => {
716
819
  const result = transform(code);
717
820
 
718
821
  // Empty element should NOT have an ID
719
- expect(result).not.toContain('data-upstart-id');
822
+ expect(result).not.toContain("data-upstart-id");
720
823
  });
721
824
  });
722
825
 
@@ -745,7 +848,7 @@ describe("upstart-editor-vite-plugin", () => {
745
848
  `;
746
849
  const result = transform(code);
747
850
 
748
- expect(result).not.toContain('data-upstart-classname-id');
851
+ expect(result).not.toContain("data-upstart-classname-id");
749
852
  });
750
853
 
751
854
  test("should NOT add classname-id for template literal className", () => {
@@ -756,7 +859,7 @@ describe("upstart-editor-vite-plugin", () => {
756
859
  `;
757
860
  const result = transform(code);
758
861
 
759
- expect(result).not.toContain('data-upstart-classname-id');
862
+ expect(result).not.toContain("data-upstart-classname-id");
760
863
  });
761
864
 
762
865
  test("should add both text-id and classname-id to same element", () => {
@@ -805,7 +908,7 @@ describe("upstart-editor-vite-plugin", () => {
805
908
  const registry = getRegistry();
806
909
 
807
910
  const entries = Object.values(registry);
808
- const textEntry = entries.find(e => e.type === "text");
911
+ const textEntry = entries.find((e) => e.type === "text");
809
912
 
810
913
  expect(textEntry).toBeDefined();
811
914
  expect(textEntry?.originalContent).toBe("Hello World");
@@ -823,7 +926,7 @@ describe("upstart-editor-vite-plugin", () => {
823
926
  const registry = getRegistry();
824
927
 
825
928
  const entries = Object.values(registry);
826
- const classEntry = entries.find(e => e.type === "className");
929
+ const classEntry = entries.find((e) => e.type === "className");
827
930
 
828
931
  expect(classEntry).toBeDefined();
829
932
  expect(classEntry?.originalContent).toBe("px-4 py-2");
@@ -835,7 +938,7 @@ describe("upstart-editor-vite-plugin", () => {
835
938
  transform(code);
836
939
  const registry = getRegistry();
837
940
 
838
- const entry = Object.values(registry).find(e => e.type === "text");
941
+ const entry = Object.values(registry).find((e) => e.type === "text");
839
942
  const extractedText = code.slice(entry!.startOffset, entry!.endOffset);
840
943
 
841
944
  expect(extractedText).toBe("Hello");
@@ -847,7 +950,7 @@ describe("upstart-editor-vite-plugin", () => {
847
950
  transform(code);
848
951
  const registry = getRegistry();
849
952
 
850
- const entry = Object.values(registry).find(e => e.type === "className");
953
+ const entry = Object.values(registry).find((e) => e.type === "className");
851
954
  const extractedClass = code.slice(entry!.startOffset, entry!.endOffset);
852
955
 
853
956
  expect(extractedClass).toBe("my-class");
@@ -859,7 +962,7 @@ describe("upstart-editor-vite-plugin", () => {
859
962
  transform(code);
860
963
  const registry = getRegistry();
861
964
 
862
- const entry = Object.values(registry).find(e => e.type === "text");
965
+ const entry = Object.values(registry).find((e) => e.type === "text");
863
966
  expect(entry?.context.parentTag).toBe("span");
864
967
  });
865
968
 
@@ -894,8 +997,8 @@ describe("upstart-editor-vite-plugin", () => {
894
997
  // Should have 2 text entries + 2 className entries = 4 total
895
998
  expect(entries.length).toBe(4);
896
999
 
897
- const textEntries = entries.filter(e => e.type === "text");
898
- const classEntries = entries.filter(e => e.type === "className");
1000
+ const textEntries = entries.filter((e) => e.type === "text");
1001
+ const classEntries = entries.filter((e) => e.type === "className");
899
1002
 
900
1003
  expect(textEntries.length).toBe(2);
901
1004
  expect(classEntries.length).toBe(2);
@@ -956,7 +1059,7 @@ describe("upstart-editor-vite-plugin", () => {
956
1059
  });
957
1060
 
958
1061
  describe("i18n <Trans> Component Detection", () => {
959
- test("should detect <Trans> component with default namespace", () => {
1062
+ test("should promote <Trans> i18n attributes to parent element with default namespace", () => {
960
1063
  const code = `
961
1064
  import { Trans } from "react-i18next";
962
1065
  export default function App() {
@@ -964,11 +1067,12 @@ describe("upstart-editor-vite-plugin", () => {
964
1067
  }
965
1068
  `;
966
1069
  const result = transform(code);
967
- expect(result).toContain('data-i18n-key="translation:features.title"');
1070
+ // i18n attributes should be on the parent <h3>, not on <Trans>
1071
+ expect(result).toContain('data-upstart-i18n="translation:features.title"');
968
1072
  expect(result).toContain('data-upstart-editable-text="true"');
969
1073
  });
970
1074
 
971
- test("should detect <Trans> component with custom namespace", () => {
1075
+ test("should promote <Trans> with custom namespace to parent", () => {
972
1076
  const code = `
973
1077
  import { Trans } from "react-i18next";
974
1078
  export default function App() {
@@ -976,11 +1080,11 @@ describe("upstart-editor-vite-plugin", () => {
976
1080
  }
977
1081
  `;
978
1082
  const result = transform(code);
979
- expect(result).toContain('data-i18n-key="dashboard:welcome.message"');
1083
+ expect(result).toContain('data-upstart-i18n="dashboard:welcome.message"');
980
1084
  expect(result).toContain('data-upstart-editable-text="true"');
981
1085
  });
982
1086
 
983
- test("should detect standalone <Trans> component", () => {
1087
+ test("should NOT add i18n attributes to standalone <Trans> (no parent element)", () => {
984
1088
  const code = `
985
1089
  import { Trans } from "react-i18next";
986
1090
  export default function App() {
@@ -988,11 +1092,13 @@ describe("upstart-editor-vite-plugin", () => {
988
1092
  }
989
1093
  `;
990
1094
  const result = transform(code);
991
- expect(result).toContain('data-i18n-key="translation:standalone.key"');
992
- expect(result).toContain('data-upstart-editable-text="true"');
1095
+ // No parent element to promote to, so no modifications
1096
+ // transform returns null when nothing was modified
1097
+ expect(result === null || !result.includes("data-upstart-i18n")).toBe(true);
1098
+ expect(result === null || !result.includes("data-upstart-editable-text")).toBe(true);
993
1099
  });
994
1100
 
995
- test("should detect multiple <Trans> components in one file", () => {
1101
+ test("should promote multiple <Trans> components to their respective parents", () => {
996
1102
  const code = `
997
1103
  import { Trans } from "react-i18next";
998
1104
  export default function App() {
@@ -1005,13 +1111,13 @@ describe("upstart-editor-vite-plugin", () => {
1005
1111
  }
1006
1112
  `;
1007
1113
  const result = transform(code);
1008
- expect(result).toContain('data-i18n-key="translation:title"');
1009
- expect(result).toContain('data-i18n-key="translation:description"');
1114
+ expect(result).toContain('data-upstart-i18n="translation:title"');
1115
+ expect(result).toContain('data-upstart-i18n="translation:description"');
1010
1116
  const editableMatches = result?.match(/data-upstart-editable-text="true"/g);
1011
1117
  expect(editableMatches?.length).toBe(2);
1012
1118
  });
1013
1119
 
1014
- test("should detect <Trans> with values prop", () => {
1120
+ test("should promote <Trans> with values prop to parent", () => {
1015
1121
  const code = `
1016
1122
  import { Trans } from "react-i18next";
1017
1123
  export default function App() {
@@ -1020,10 +1126,10 @@ describe("upstart-editor-vite-plugin", () => {
1020
1126
  }
1021
1127
  `;
1022
1128
  const result = transform(code);
1023
- expect(result).toContain('data-i18n-key="translation:greeting"');
1129
+ expect(result).toContain('data-upstart-i18n="translation:greeting"');
1024
1130
  });
1025
1131
 
1026
- test("should detect <Trans> with components prop", () => {
1132
+ test("should promote <Trans> with components prop to parent", () => {
1027
1133
  const code = `
1028
1134
  import { Trans } from "react-i18next";
1029
1135
  export default function App() {
@@ -1038,21 +1144,21 @@ describe("upstart-editor-vite-plugin", () => {
1038
1144
  }
1039
1145
  `;
1040
1146
  const result = transform(code);
1041
- expect(result).toContain('data-i18n-key="translation:terms"');
1147
+ expect(result).toContain('data-upstart-i18n="translation:terms"');
1042
1148
  });
1043
1149
 
1044
1150
  test("should NOT detect <Trans> without i18nKey prop", () => {
1045
1151
  const code = `
1046
1152
  import { Trans } from "react-i18next";
1047
1153
  export default function App() {
1048
- return <Trans>Static text</Trans>;
1154
+ return <p><Trans>Static text</Trans></p>;
1049
1155
  }
1050
1156
  `;
1051
1157
  const result = transform(code);
1052
- expect(result).not.toContain('data-i18n-key');
1158
+ expect(result).not.toContain("data-upstart-i18n");
1053
1159
  });
1054
1160
 
1055
- test("should still add hash to <Trans> elements", () => {
1161
+ test("should NOT add hash or any attributes to <Trans> elements", () => {
1056
1162
  const code = `
1057
1163
  import { Trans } from "react-i18next";
1058
1164
  export default function App() {
@@ -1060,11 +1166,15 @@ describe("upstart-editor-vite-plugin", () => {
1060
1166
  }
1061
1167
  `;
1062
1168
  const result = transform(code);
1063
- expect(result).toMatch(/data-upstart-hash="[a-f0-9]+"/);
1064
- expect(result).toContain('data-i18n-key="translation:title"');
1169
+ // Only 1 hash (for h1), <Trans> should have no injected attributes
1170
+ const hashMatches = result?.match(/data-upstart-hash="[a-f0-9]+"/g);
1171
+ expect(hashMatches?.length).toBe(1);
1172
+ expect(result).toContain('data-upstart-i18n="translation:title"');
1173
+ // No component tracking for Trans
1174
+ expect(result).not.toContain('data-upstart-component="Trans"');
1065
1175
  });
1066
1176
 
1067
- test("should handle PascalCase component with <Trans> child", () => {
1177
+ test("should promote i18n attrs to PascalCase component parent", () => {
1068
1178
  const code = `
1069
1179
  import { Trans } from "react-i18next";
1070
1180
  export default function App() {
@@ -1072,7 +1182,7 @@ describe("upstart-editor-vite-plugin", () => {
1072
1182
  }
1073
1183
  `;
1074
1184
  const result = transform(code);
1075
- expect(result).toContain('data-i18n-key="ui:submit"');
1185
+ expect(result).toContain('data-upstart-i18n="ui:submit"');
1076
1186
  expect(result).toContain('data-upstart-editable-text="true"');
1077
1187
  expect(result).toContain('data-upstart-component="Button"');
1078
1188
  });
@@ -1094,11 +1204,60 @@ describe("upstart-editor-vite-plugin", () => {
1094
1204
  }
1095
1205
  `;
1096
1206
  const result = transform(code);
1097
- expect(result).toContain('data-i18n-key="translation:item.label"');
1207
+ // i18n attrs promoted to <span>, loop context on <Card>
1208
+ expect(result).toContain('data-upstart-i18n="translation:item.label"');
1098
1209
  expect(result).toContain('data-upstart-loop-item="item"');
1099
1210
  });
1100
1211
 
1101
- test("should NOT detect parent element with mixed content (<Trans> + text)", () => {
1212
+ test("should detect <Trans> with dynamic expression i18nKey", () => {
1213
+ const code = `
1214
+ import { Trans } from "react-i18next";
1215
+ export default function App() {
1216
+ const labelKey = "features.title";
1217
+ return <h3><Trans i18nKey={labelKey} /></h3>;
1218
+ }
1219
+ `;
1220
+ const result = transform(code);
1221
+ expect(result).toContain('data-upstart-i18n="translation:features.title"');
1222
+ expect(result).toContain('data-upstart-editable-text="true"');
1223
+ });
1224
+
1225
+ test("should detect <Trans> with dynamic expression i18nKey inside .map() loop", () => {
1226
+ const code = `
1227
+ import { Trans } from "react-i18next";
1228
+ export default function App() {
1229
+ const stats = [{ labelKey: "stat.users" }];
1230
+ return (
1231
+ <div>
1232
+ {stats.map((stat) => (
1233
+ <Card key={stat.labelKey}>
1234
+ <span><Trans i18nKey={stat.labelKey} /></span>
1235
+ </Card>
1236
+ ))}
1237
+ </div>
1238
+ );
1239
+ }
1240
+ `;
1241
+ const result = transform(code);
1242
+ expect(result).toContain("data-upstart-i18n={`translation:${stat.labelKey}`}");
1243
+ expect(result).toContain('data-upstart-editable-text="true"');
1244
+ expect(result).toContain('data-upstart-loop-item="stat"');
1245
+ });
1246
+
1247
+ test("should detect <Trans> with dynamic ns prop", () => {
1248
+ const code = `
1249
+ import { Trans } from "react-i18next";
1250
+ export default function App() {
1251
+ const ns = "dashboard";
1252
+ return <p><Trans i18nKey="welcome" ns={ns} /></p>;
1253
+ }
1254
+ `;
1255
+ const result = transform(code);
1256
+ expect(result).toContain('data-upstart-i18n="dashboard:welcome"');
1257
+ expect(result).toContain('data-upstart-editable-text="true"');
1258
+ });
1259
+
1260
+ test("should promote i18n attrs to parent even with mixed content", () => {
1102
1261
  const code = `
1103
1262
  import { Trans } from "react-i18next";
1104
1263
  export default function App() {
@@ -1106,84 +1265,228 @@ describe("upstart-editor-vite-plugin", () => {
1106
1265
  }
1107
1266
  `;
1108
1267
  const result = transform(code);
1109
- // The div should not have data-i18n-key (mixed content)
1110
- // But the Trans itself should have it
1111
- const matches = result?.match(/data-i18n-key="translation:key"/g);
1112
- expect(matches?.length).toBe(1); // Only on Trans, not on parent div
1268
+ // The parent div should have the i18n key (promoted from Trans child)
1269
+ const matches = result?.match(/data-upstart-i18n="translation:key"/g);
1270
+ expect(matches?.length).toBe(1);
1271
+ expect(result).toContain('data-upstart-editable-text="true"');
1272
+ });
1273
+
1274
+ test("should promote <Trans> from inside && expression to parent", () => {
1275
+ const code = `
1276
+ import { Trans } from "react-i18next";
1277
+ export default function App() {
1278
+ return <p>{showWelcome && <Trans i18nKey="welcome" />}</p>;
1279
+ }
1280
+ `;
1281
+ const result = transform(code);
1282
+ expect(result).toContain('data-upstart-i18n="translation:welcome"');
1283
+ expect(result).toContain('data-upstart-editable-text="true"');
1284
+ });
1285
+
1286
+ test("should promote <Trans> from inside ternary expression to parent", () => {
1287
+ const code = `
1288
+ import { Trans } from "react-i18next";
1289
+ export default function App() {
1290
+ return <div>{isLoggedIn ? <Trans i18nKey="welcome" ns="app" /> : <Trans i18nKey="login" ns="app" />}</div>;
1291
+ }
1292
+ `;
1293
+ const result = transform(code);
1294
+ // Both keys should be comma-separated on the parent
1295
+ expect(result).toContain('data-upstart-i18n="app:welcome,app:login"');
1296
+ expect(result).toContain('data-upstart-editable-text="true"');
1297
+ });
1298
+
1299
+ test("should preserve parent hash unchanged when promoting Trans attrs", () => {
1300
+ const code = `
1301
+ import { Trans } from "react-i18next";
1302
+ export default function App() {
1303
+ return <h1><Trans i18nKey="title" /></h1>;
1304
+ }
1305
+ `;
1306
+ const result1 = transform(code);
1307
+ const result2 = transform(code);
1308
+ const hash1 = result1?.match(/data-upstart-hash="([a-f0-9]+)"/)?.[1];
1309
+ const hash2 = result2?.match(/data-upstart-hash="([a-f0-9]+)"/)?.[1];
1310
+ expect(hash1).toBe(hash2);
1311
+ expect(hash1).toBeTruthy();
1312
+ });
1313
+
1314
+ test("should skip <Trans> without i18nKey even in child scan", () => {
1315
+ const code = `
1316
+ import { Trans } from "react-i18next";
1317
+ export default function App() {
1318
+ return <p><Trans>Static text</Trans></p>;
1319
+ }
1320
+ `;
1321
+ const result = transform(code);
1322
+ expect(result).not.toContain("data-upstart-i18n");
1323
+ });
1324
+
1325
+ test("should handle <Trans> inside nested logical expressions", () => {
1326
+ const code = `
1327
+ import { Trans } from "react-i18next";
1328
+ export default function App() {
1329
+ return <p>{a && b && <Trans i18nKey="nested" />}</p>;
1330
+ }
1331
+ `;
1332
+ const result = transform(code);
1333
+ expect(result).toContain('data-upstart-i18n="translation:nested"');
1113
1334
  });
1114
1335
  });
1115
1336
 
1116
- describe("Forbidden useTranslation Enforcement", () => {
1117
- test("should throw error when destructuring t from useTranslation", () => {
1337
+ describe("useTranslation t() Function Detection", () => {
1338
+ test("should detect t() with default namespace", () => {
1118
1339
  const code = `
1119
1340
  import { useTranslation } from "react-i18next";
1120
1341
  export default function App() {
1121
1342
  const { t } = useTranslation();
1122
- return <div>Hello</div>;
1343
+ return <h3>{t("features.title")}</h3>;
1123
1344
  }
1124
1345
  `;
1125
- expect(() => transform(code)).toThrow(/useTranslation hook is forbidden/);
1346
+ const result = transform(code);
1347
+ expect(result).toContain('data-upstart-i18n="translation:features.title"');
1348
+ expect(result).toContain('data-upstart-editable-text="true"');
1126
1349
  });
1127
1350
 
1128
- test("should throw error when destructuring t with alias", () => {
1351
+ test("should detect t() with custom namespace", () => {
1129
1352
  const code = `
1130
1353
  import { useTranslation } from "react-i18next";
1131
1354
  export default function App() {
1132
- const { t: translate } = useTranslation();
1133
- return <div>Hello</div>;
1355
+ const { t } = useTranslation("dashboard");
1356
+ return <p>{t("welcome.message")}</p>;
1134
1357
  }
1135
1358
  `;
1136
- expect(() => transform(code)).toThrow(/useTranslation hook is forbidden/);
1359
+ const result = transform(code);
1360
+ expect(result).toContain('data-upstart-i18n="dashboard:welcome.message"');
1137
1361
  });
1138
1362
 
1139
- test("should throw error when destructuring both t and i18n", () => {
1363
+ test("should detect t() with array namespace (uses first)", () => {
1140
1364
  const code = `
1141
1365
  import { useTranslation } from "react-i18next";
1142
1366
  export default function App() {
1143
- const { t, i18n } = useTranslation();
1144
- return <div>Hello</div>;
1367
+ const { t } = useTranslation(["dashboard", "common"]);
1368
+ return <p>{t("welcome")}</p>;
1145
1369
  }
1146
1370
  `;
1147
- expect(() => transform(code)).toThrow(/useTranslation hook is forbidden/);
1371
+ const result = transform(code);
1372
+ expect(result).toContain('data-upstart-i18n="dashboard:welcome"');
1148
1373
  });
1149
1374
 
1150
- test("should NOT throw when only destructuring i18n (allowed for language switching)", () => {
1375
+ test("should detect aliased t function", () => {
1151
1376
  const code = `
1152
1377
  import { useTranslation } from "react-i18next";
1153
1378
  export default function App() {
1154
- const { i18n } = useTranslation();
1155
- return (
1156
- <button onClick={() => i18n.changeLanguage("fr")}>
1157
- Switch to French
1158
- </button>
1159
- );
1379
+ const { t: translate } = useTranslation("ui");
1380
+ return <button>{translate("submit")}</button>;
1160
1381
  }
1161
1382
  `;
1162
- expect(() => transform(code)).not.toThrow();
1383
+ const result = transform(code);
1384
+ expect(result).toContain('data-upstart-i18n="ui:submit"');
1385
+ });
1386
+
1387
+ test("should detect t() in conditional expression", () => {
1388
+ const code = `
1389
+ import { useTranslation } from "react-i18next";
1390
+ export default function App({ isActive }) {
1391
+ const { t } = useTranslation();
1392
+ return <span>{isActive ? t("active") : t("inactive")}</span>;
1393
+ }
1394
+ `;
1395
+ const result = transform(code);
1396
+ expect(result).toContain('data-upstart-i18n="translation:active,translation:inactive"');
1397
+ });
1398
+
1399
+ test("should detect t() in logical expression", () => {
1400
+ const code = `
1401
+ import { useTranslation } from "react-i18next";
1402
+ export default function App({ showWelcome }) {
1403
+ const { t } = useTranslation();
1404
+ return <p>{showWelcome && t("welcome")}</p>;
1405
+ }
1406
+ `;
1407
+ const result = transform(code);
1408
+ expect(result).toContain('data-upstart-i18n="translation:welcome"');
1163
1409
  });
1164
1410
 
1165
- test("should include file path in error message", () => {
1411
+ test("should handle dynamic key with template expression", () => {
1412
+ const code = `
1413
+ import { useTranslation } from "react-i18next";
1414
+ export default function App({ stat }) {
1415
+ const { t } = useTranslation();
1416
+ return <span>{t(stat.labelKey)}</span>;
1417
+ }
1418
+ `;
1419
+ const result = transform(code);
1420
+ expect(result).toContain("data-upstart-i18n={`translation:${stat.labelKey}`}");
1421
+ });
1422
+
1423
+ test("should resolve constant variable as static key", () => {
1166
1424
  const code = `
1167
1425
  import { useTranslation } from "react-i18next";
1168
1426
  export default function App() {
1169
1427
  const { t } = useTranslation();
1170
- return <div>Hello</div>;
1428
+ const labelKey = "features.title";
1429
+ return <span>{t(labelKey)}</span>;
1171
1430
  }
1172
1431
  `;
1173
- expect(() => transform(code, "components/MyComponent.tsx")).toThrow(
1174
- /\[components\/MyComponent\.tsx\]/
1175
- );
1432
+ const result = transform(code);
1433
+ expect(result).toContain('data-upstart-i18n="translation:features.title"');
1176
1434
  });
1177
1435
 
1178
- test("should suggest using <Trans> in error message", () => {
1436
+ test("should combine Trans and t() keys in same parent", () => {
1179
1437
  const code = `
1180
1438
  import { useTranslation } from "react-i18next";
1439
+ import { Trans } from "react-i18next";
1181
1440
  export default function App() {
1441
+ const { t } = useTranslation("app");
1442
+ return <div>{t("prefix")}<Trans i18nKey="suffix" ns="app" /></div>;
1443
+ }
1444
+ `;
1445
+ const result = transform(code);
1446
+ expect(result).toContain('data-upstart-i18n="app:suffix,app:prefix"');
1447
+ expect(result).toContain('data-upstart-editable-text="true"');
1448
+ });
1449
+
1450
+ test("should detect t() inside .map() callback on child element", () => {
1451
+ const code = `
1452
+ import { useTranslation } from "react-i18next";
1453
+ export default function App({ items }) {
1182
1454
  const { t } = useTranslation();
1183
- return <div>Hello</div>;
1455
+ return (
1456
+ <ul>
1457
+ {items.map((item) => (
1458
+ <li key={item}>{t("item.label")}</li>
1459
+ ))}
1460
+ </ul>
1461
+ );
1184
1462
  }
1185
1463
  `;
1186
- expect(() => transform(code)).toThrow(/<Trans i18nKey="\.\.\."/);
1464
+ const result = transform(code);
1465
+ expect(result).toContain('data-upstart-i18n="translation:item.label"');
1466
+ });
1467
+
1468
+ test("should not add i18n attributes when only i18n is destructured", () => {
1469
+ const code = `
1470
+ import { useTranslation } from "react-i18next";
1471
+ export default function App() {
1472
+ const { i18n } = useTranslation();
1473
+ return <button onClick={() => i18n.changeLanguage("fr")}>Switch</button>;
1474
+ }
1475
+ `;
1476
+ const result = transform(code);
1477
+ expect(result).not.toContain("data-upstart-i18n");
1478
+ });
1479
+
1480
+ test("should track t when both t and i18n are destructured", () => {
1481
+ const code = `
1482
+ import { useTranslation } from "react-i18next";
1483
+ export default function App() {
1484
+ const { t, i18n } = useTranslation("app");
1485
+ return <div>{t("greeting")}</div>;
1486
+ }
1487
+ `;
1488
+ const result = transform(code);
1489
+ expect(result).toContain('data-upstart-i18n="app:greeting"');
1187
1490
  });
1188
1491
  });
1189
1492
  });