@upstart.gg/vite-plugins 0.1.29 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/upstart-editor-api.d.ts +13 -1
- package/dist/upstart-editor-api.d.ts.map +1 -1
- package/dist/upstart-editor-api.js +59 -1
- package/dist/upstart-editor-api.js.map +1 -1
- package/dist/vite-plugin-upstart-attrs.d.ts +12 -7
- package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-attrs.js +195 -3
- package/dist/vite-plugin-upstart-attrs.js.map +1 -1
- package/dist/vite-plugin-upstart-branding/plugin.d.ts +3 -3
- package/dist/vite-plugin-upstart-branding/plugin.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-branding/plugin.js.map +1 -1
- package/dist/vite-plugin-upstart-branding/runtime.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/plugin.d.ts +3 -3
- package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/plugin.js +4 -1
- package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +27 -16
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js +14 -2
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
- package/dist/{src/vite-plugin-upstart-editor → vite-plugin-upstart-editor}/runtime/state.d.ts +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/state.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/state.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +212 -28
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +16 -3
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/utils.js.map +1 -1
- package/dist/vite-plugin-upstart-theme.d.ts +3 -3
- package/dist/vite-plugin-upstart-theme.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-theme.js +2 -2
- package/dist/vite-plugin-upstart-theme.js.map +1 -1
- package/package.json +7 -7
- package/src/tests/vite-plugin-upstart-attrs.test.ts +298 -37
- package/src/upstart-editor-api.ts +71 -0
- package/src/vite-plugin-upstart-attrs.ts +293 -5
- package/src/vite-plugin-upstart-editor/plugin.ts +11 -1
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +35 -21
- package/src/vite-plugin-upstart-editor/runtime/index.ts +21 -1
- package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +260 -41
- package/src/vite-plugin-upstart-editor/runtime/types.ts +17 -4
- package/src/vite-plugin-upstart-theme.ts +4 -1
- package/dist/src/vite-plugin-upstart-editor/runtime/state.d.ts.map +0 -1
- package/dist/src/vite-plugin-upstart-editor/runtime/state.js.map +0 -1
- /package/dist/{src/vite-plugin-upstart-editor → vite-plugin-upstart-editor}/runtime/state.js +0 -0
|
@@ -78,7 +78,7 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
describe("Static Text Detection", () => {
|
|
81
|
-
test("should mark leaf elements with static text as
|
|
81
|
+
test("should mark leaf elements with static text as directly editable (direct mode)", () => {
|
|
82
82
|
const code = `
|
|
83
83
|
export default function App() {
|
|
84
84
|
return <div>Hello World</div>;
|
|
@@ -87,10 +87,12 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
87
87
|
|
|
88
88
|
const result = transform(code);
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
// Text-only leaf elements are editable directly in the TSX source
|
|
91
|
+
expect(result).toContain('data-upstart-editable-text="true"');
|
|
92
|
+
expect(result).toContain('data-upstart-editable-text-mode="direct"');
|
|
91
93
|
});
|
|
92
94
|
|
|
93
|
-
test("should mark span with static text as
|
|
95
|
+
test("should mark span with static text as directly editable (direct mode)", () => {
|
|
94
96
|
const code = `
|
|
95
97
|
export default function App() {
|
|
96
98
|
return <span>Click here</span>;
|
|
@@ -99,10 +101,11 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
99
101
|
|
|
100
102
|
const result = transform(code);
|
|
101
103
|
|
|
102
|
-
expect(result).toContain('data-upstart-editable-text="
|
|
104
|
+
expect(result).toContain('data-upstart-editable-text="true"');
|
|
105
|
+
expect(result).toContain('data-upstart-editable-text-mode="direct"');
|
|
103
106
|
});
|
|
104
107
|
|
|
105
|
-
test("should mark PascalCase component with static text as
|
|
108
|
+
test("should mark PascalCase component with static text as directly editable AND track component", () => {
|
|
106
109
|
const code = `
|
|
107
110
|
export default function App() {
|
|
108
111
|
return <Button>Submit</Button>;
|
|
@@ -111,12 +114,12 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
111
114
|
|
|
112
115
|
const result = transform(code);
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
expect(result).toContain('data-upstart-editable-text="
|
|
117
|
+
expect(result).toContain('data-upstart-editable-text="true"');
|
|
118
|
+
expect(result).toContain('data-upstart-editable-text-mode="direct"');
|
|
116
119
|
expect(result).toContain('data-upstart-component="Button"');
|
|
117
120
|
});
|
|
118
121
|
|
|
119
|
-
test("should
|
|
122
|
+
test("should mark outer div with inline child as almost-leaf (rich-panel) and inner span as direct", () => {
|
|
120
123
|
const code = `
|
|
121
124
|
export default function App() {
|
|
122
125
|
return <div><span>nested</span></div>;
|
|
@@ -125,10 +128,13 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
125
128
|
|
|
126
129
|
const result = transform(code);
|
|
127
130
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
// <span> is an inline formatting tag → outer div is "almost-leaf" → rich-panel
|
|
132
|
+
expect(result).not.toContain('data-upstart-editable-text="false"');
|
|
133
|
+
// Both elements get "true": outer div (rich-panel) + inner span (direct)
|
|
134
|
+
const trueMatches = result?.match(/data-upstart-editable-text="true"/g);
|
|
135
|
+
expect(trueMatches?.length).toBe(2);
|
|
136
|
+
expect(result).toContain('data-upstart-editable-text-mode="rich-panel"');
|
|
137
|
+
expect(result).toContain('data-upstart-editable-text-mode="direct"');
|
|
132
138
|
});
|
|
133
139
|
|
|
134
140
|
test("should mark elements with dynamic text expressions as non-editable", () => {
|
|
@@ -141,12 +147,12 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
141
147
|
|
|
142
148
|
const result = transform(code);
|
|
143
149
|
|
|
144
|
-
//
|
|
150
|
+
// Identifier expression = text-producing but not statically editable → "false"
|
|
145
151
|
expect(result).toContain('data-upstart-editable-text="false"');
|
|
146
152
|
expect(result).toContain("data-upstart-hash");
|
|
147
153
|
});
|
|
148
154
|
|
|
149
|
-
test("should mark
|
|
155
|
+
test("should mark mixed-content outer as almost-leaf (rich-panel) and text-only child as direct", () => {
|
|
150
156
|
const code = `
|
|
151
157
|
export default function App() {
|
|
152
158
|
return <div>Text <b>bold</b></div>;
|
|
@@ -155,9 +161,13 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
155
161
|
|
|
156
162
|
const result = transform(code);
|
|
157
163
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
// <div> has JSXText "Text " + <b> (inline tag) → almost-leaf → rich-panel
|
|
165
|
+
expect(result).not.toContain('data-upstart-editable-text="false"');
|
|
166
|
+
// Both outer div (rich-panel) + inner <b> (direct) get "true"
|
|
167
|
+
const trueMatches = result?.match(/data-upstart-editable-text="true"/g);
|
|
168
|
+
expect(trueMatches?.length).toBe(2);
|
|
169
|
+
expect(result).toContain('data-upstart-editable-text-mode="rich-panel"');
|
|
170
|
+
expect(result).toContain('data-upstart-editable-text-mode="direct"');
|
|
161
171
|
});
|
|
162
172
|
|
|
163
173
|
test("should NOT mark empty elements as editable", () => {
|
|
@@ -190,7 +200,7 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
190
200
|
expect(result).toContain("data-upstart-hash");
|
|
191
201
|
});
|
|
192
202
|
|
|
193
|
-
test("should
|
|
203
|
+
test("should mark multiple text leaf elements as directly editable", () => {
|
|
194
204
|
const code = `
|
|
195
205
|
export default function App() {
|
|
196
206
|
return (
|
|
@@ -204,9 +214,10 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
204
214
|
|
|
205
215
|
const result = transform(code);
|
|
206
216
|
|
|
207
|
-
// Both h1 and p
|
|
208
|
-
const
|
|
209
|
-
expect(
|
|
217
|
+
// Both h1 and p are text leaves → direct mode
|
|
218
|
+
const trueMatches = result?.match(/data-upstart-editable-text="true"/g);
|
|
219
|
+
expect(trueMatches?.length).toBe(2);
|
|
220
|
+
expect(result).not.toContain('data-upstart-editable-text="false"');
|
|
210
221
|
});
|
|
211
222
|
});
|
|
212
223
|
|
|
@@ -283,7 +294,7 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
283
294
|
expect(result).not.toContain("data-upstart-editable-text");
|
|
284
295
|
});
|
|
285
296
|
|
|
286
|
-
test("Trans
|
|
297
|
+
test("should NOT be editable when Trans is mixed with a non-i18n variable", () => {
|
|
287
298
|
const code = `
|
|
288
299
|
import { Trans } from "react-i18next";
|
|
289
300
|
export default function App() {
|
|
@@ -291,9 +302,10 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
291
302
|
}
|
|
292
303
|
`;
|
|
293
304
|
const result = transform(code);
|
|
294
|
-
// Trans
|
|
295
|
-
|
|
296
|
-
expect(result).not.toContain('data-upstart-editable-text="
|
|
305
|
+
// {prefix} is a text-producing Identifier alongside Trans → mixed → NOT editable
|
|
306
|
+
// Saving would incorrectly include the rendered prefix in the translation value
|
|
307
|
+
expect(result).not.toContain('data-upstart-editable-text="true"');
|
|
308
|
+
expect(result).not.toContain("data-upstart-i18n");
|
|
297
309
|
});
|
|
298
310
|
});
|
|
299
311
|
|
|
@@ -774,7 +786,7 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
774
786
|
clearRegistry();
|
|
775
787
|
});
|
|
776
788
|
|
|
777
|
-
test("should add data-upstart-id to text leaf elements", () => {
|
|
789
|
+
test("should add data-upstart-id to text leaf elements (direct mode)", () => {
|
|
778
790
|
const code = `
|
|
779
791
|
export default function App() {
|
|
780
792
|
return <div>Hello World</div>;
|
|
@@ -782,12 +794,13 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
782
794
|
`;
|
|
783
795
|
const result = transform(code);
|
|
784
796
|
|
|
785
|
-
//
|
|
786
|
-
expect(result).toContain('data-upstart-editable-text="
|
|
797
|
+
// Text leaf elements are directly editable (direct mode) and get an ID
|
|
798
|
+
expect(result).toContain('data-upstart-editable-text="true"');
|
|
799
|
+
expect(result).toContain('data-upstart-editable-text-mode="direct"');
|
|
787
800
|
expect(result).toMatch(/data-upstart-id="test\.tsx:\d+"/);
|
|
788
801
|
});
|
|
789
802
|
|
|
790
|
-
test("should generate unique IDs for multiple text elements", () => {
|
|
803
|
+
test("should generate unique IDs for multiple text leaf elements", () => {
|
|
791
804
|
const code = `
|
|
792
805
|
export default function App() {
|
|
793
806
|
return (
|
|
@@ -800,9 +813,10 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
800
813
|
`;
|
|
801
814
|
const result = transform(code);
|
|
802
815
|
|
|
803
|
-
//
|
|
816
|
+
// Each span is a text leaf → direct mode with a unique ID
|
|
817
|
+
// Note: the outer div is an "almost-leaf" (span is inline) → rich-panel, gets its own ID too
|
|
804
818
|
const ids = result?.match(/data-upstart-id="test\.tsx:\d+"/g);
|
|
805
|
-
expect(ids?.length).
|
|
819
|
+
expect(ids?.length).toBeGreaterThanOrEqual(2);
|
|
806
820
|
expect(ids?.[0]).not.toBe(ids?.[1]);
|
|
807
821
|
});
|
|
808
822
|
|
|
@@ -1008,14 +1022,18 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
1008
1022
|
const registry = getRegistry();
|
|
1009
1023
|
const entries = Object.values(registry);
|
|
1010
1024
|
|
|
1011
|
-
//
|
|
1025
|
+
// 2 text (spans) + 2 className (spans); the outer div is NOT almost-leaf because
|
|
1026
|
+
// its <span> children have attributes (className), which disqualifies them as
|
|
1027
|
+
// simple inline formatting wrappers.
|
|
1012
1028
|
expect(entries.length).toBe(4);
|
|
1013
1029
|
|
|
1014
1030
|
const textEntries = entries.filter((e) => e.type === "text");
|
|
1015
1031
|
const classEntries = entries.filter((e) => e.type === "className");
|
|
1032
|
+
const richTextEntries = entries.filter((e) => e.type === "rich-text");
|
|
1016
1033
|
|
|
1017
1034
|
expect(textEntries.length).toBe(2);
|
|
1018
1035
|
expect(classEntries.length).toBe(2);
|
|
1036
|
+
expect(richTextEntries.length).toBe(0);
|
|
1019
1037
|
});
|
|
1020
1038
|
});
|
|
1021
1039
|
|
|
@@ -1271,7 +1289,7 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
1271
1289
|
expect(result).toContain('data-upstart-editable-text="true"');
|
|
1272
1290
|
});
|
|
1273
1291
|
|
|
1274
|
-
test("should
|
|
1292
|
+
test("should NOT be editable when Trans is mixed with static text", () => {
|
|
1275
1293
|
const code = `
|
|
1276
1294
|
import { Trans } from "react-i18next";
|
|
1277
1295
|
export default function App() {
|
|
@@ -1279,10 +1297,10 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
1279
1297
|
}
|
|
1280
1298
|
`;
|
|
1281
1299
|
const result = transform(code);
|
|
1282
|
-
//
|
|
1283
|
-
|
|
1284
|
-
expect(
|
|
1285
|
-
expect(result).toContain(
|
|
1300
|
+
// "Prefix: " is non-whitespace JSXText alongside Trans → mixed → NOT editable
|
|
1301
|
+
// Saving would incorrectly include "Prefix: " in the translation value
|
|
1302
|
+
expect(result).not.toContain('data-upstart-editable-text="true"');
|
|
1303
|
+
expect(result).not.toContain("data-upstart-i18n");
|
|
1286
1304
|
});
|
|
1287
1305
|
|
|
1288
1306
|
test("should promote <Trans> from inside && expression to parent", () => {
|
|
@@ -1346,6 +1364,90 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
1346
1364
|
const result = transform(code);
|
|
1347
1365
|
expect(result).toContain('data-upstart-i18n="translation:nested"');
|
|
1348
1366
|
});
|
|
1367
|
+
|
|
1368
|
+
describe("Mixed i18n + non-i18n content detection", () => {
|
|
1369
|
+
test("should NOT be editable when JSXText static content precedes Trans", () => {
|
|
1370
|
+
const code = `
|
|
1371
|
+
import { Trans } from "react-i18next";
|
|
1372
|
+
export default function App() {
|
|
1373
|
+
return (
|
|
1374
|
+
<p>
|
|
1375
|
+
© {new Date().getFullYear()} John Doe.{" "}
|
|
1376
|
+
<Trans i18nKey="footer.rights" />
|
|
1377
|
+
</p>
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
`;
|
|
1381
|
+
const result = transform(code);
|
|
1382
|
+
// "© ... John Doe." is non-whitespace JSXText alongside Trans
|
|
1383
|
+
expect(result).not.toContain('data-upstart-editable-text="true"');
|
|
1384
|
+
expect(result).not.toContain("data-upstart-i18n");
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
test("should NOT be editable when a text variable is alongside Trans", () => {
|
|
1388
|
+
const code = `
|
|
1389
|
+
import { Trans } from "react-i18next";
|
|
1390
|
+
export default function App() {
|
|
1391
|
+
return <p>{userName}: <Trans i18nKey="greeting" /></p>;
|
|
1392
|
+
}
|
|
1393
|
+
`;
|
|
1394
|
+
const result = transform(code);
|
|
1395
|
+
// {userName} is an Identifier (text-producing) alongside Trans
|
|
1396
|
+
expect(result).not.toContain('data-upstart-editable-text="true"');
|
|
1397
|
+
expect(result).not.toContain("data-upstart-i18n");
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
test("should NOT be editable when a member expression is alongside Trans", () => {
|
|
1401
|
+
const code = `
|
|
1402
|
+
import { Trans } from "react-i18next";
|
|
1403
|
+
export default function App() {
|
|
1404
|
+
return <span>{user.name} — <Trans i18nKey="role" /></span>;
|
|
1405
|
+
}
|
|
1406
|
+
`;
|
|
1407
|
+
const result = transform(code);
|
|
1408
|
+
// {user.name} is a MemberExpression (text-producing) alongside Trans
|
|
1409
|
+
expect(result).not.toContain('data-upstart-editable-text="true"');
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
test("should still be editable when Trans is inside && guard (boolean, not text)", () => {
|
|
1413
|
+
const code = `
|
|
1414
|
+
import { Trans } from "react-i18next";
|
|
1415
|
+
export default function App() {
|
|
1416
|
+
return <p>{showBanner && <Trans i18nKey="banner.text" />}</p>;
|
|
1417
|
+
}
|
|
1418
|
+
`;
|
|
1419
|
+
const result = transform(code);
|
|
1420
|
+
// showBanner is a boolean guard, not text content — still pure i18n
|
|
1421
|
+
expect(result).toContain('data-upstart-editable-text="true"');
|
|
1422
|
+
expect(result).toContain('data-upstart-i18n="translation:banner.text"');
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
test("should still be editable when Trans is in a ternary between two i18n values", () => {
|
|
1426
|
+
const code = `
|
|
1427
|
+
import { Trans } from "react-i18next";
|
|
1428
|
+
export default function App() {
|
|
1429
|
+
return <p>{isAdmin ? <Trans i18nKey="role.admin" /> : <Trans i18nKey="role.user" />}</p>;
|
|
1430
|
+
}
|
|
1431
|
+
`;
|
|
1432
|
+
const result = transform(code);
|
|
1433
|
+
// Both branches are i18n — no non-i18n text produced
|
|
1434
|
+
expect(result).toContain('data-upstart-editable-text="true"');
|
|
1435
|
+
expect(result).toContain('data-upstart-i18n="translation:role.admin,translation:role.user"');
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
test("should still be editable when only whitespace {' '} separates Trans from nothing", () => {
|
|
1439
|
+
const code = `
|
|
1440
|
+
import { Trans } from "react-i18next";
|
|
1441
|
+
export default function App() {
|
|
1442
|
+
return <span>{" "}<Trans i18nKey="label" /></span>;
|
|
1443
|
+
}
|
|
1444
|
+
`;
|
|
1445
|
+
const result = transform(code);
|
|
1446
|
+
// {" "} is whitespace-only — harmless
|
|
1447
|
+
expect(result).toContain('data-upstart-editable-text="true"');
|
|
1448
|
+
expect(result).toContain('data-upstart-i18n="translation:label"');
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
1349
1451
|
});
|
|
1350
1452
|
|
|
1351
1453
|
describe("useTranslation t() Function Detection", () => {
|
|
@@ -1503,4 +1605,163 @@ describe("upstart-editor-vite-plugin", () => {
|
|
|
1503
1605
|
expect(result).toContain('data-upstart-i18n="app:greeting"');
|
|
1504
1606
|
});
|
|
1505
1607
|
});
|
|
1608
|
+
|
|
1609
|
+
describe("Trans values prop — template variable detection", () => {
|
|
1610
|
+
test("should emit data-i18n-values for shorthand values prop (single key)", () => {
|
|
1611
|
+
const code = `
|
|
1612
|
+
import { Trans } from "react-i18next";
|
|
1613
|
+
export default function App() {
|
|
1614
|
+
const year = 2026;
|
|
1615
|
+
return <p><Trans i18nKey="footer.copyright" values={{ year }} /></p>;
|
|
1616
|
+
}
|
|
1617
|
+
`;
|
|
1618
|
+
const result = transform(code);
|
|
1619
|
+
expect(result).toContain('data-i18n-values="year"');
|
|
1620
|
+
expect(result).toContain('data-upstart-i18n="translation:footer.copyright"');
|
|
1621
|
+
expect(result).toContain('data-upstart-editable-text="true"');
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
test("should emit data-i18n-values for explicit key-value pair", () => {
|
|
1625
|
+
const code = `
|
|
1626
|
+
import { Trans } from "react-i18next";
|
|
1627
|
+
export default function App() {
|
|
1628
|
+
return <span><Trans i18nKey="greeting" values={{ name: userName }} /></span>;
|
|
1629
|
+
}
|
|
1630
|
+
`;
|
|
1631
|
+
const result = transform(code);
|
|
1632
|
+
expect(result).toContain('data-i18n-values="name"');
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
test("should emit data-i18n-values for multiple keys (shorthand)", () => {
|
|
1636
|
+
const code = `
|
|
1637
|
+
import { Trans } from "react-i18next";
|
|
1638
|
+
export default function App() {
|
|
1639
|
+
const year = 2026;
|
|
1640
|
+
const month = "January";
|
|
1641
|
+
return <p><Trans i18nKey="date.label" values={{ year, month }} /></p>;
|
|
1642
|
+
}
|
|
1643
|
+
`;
|
|
1644
|
+
const result = transform(code);
|
|
1645
|
+
expect(result).toContain('data-i18n-values="year,month"');
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
test("should emit data-i18n-values for multiple explicit key-value pairs", () => {
|
|
1649
|
+
const code = `
|
|
1650
|
+
import { Trans } from "react-i18next";
|
|
1651
|
+
export default function App() {
|
|
1652
|
+
return <p><Trans i18nKey="user.info" values={{ firstName: user.first, lastName: user.last }} /></p>;
|
|
1653
|
+
}
|
|
1654
|
+
`;
|
|
1655
|
+
const result = transform(code);
|
|
1656
|
+
expect(result).toContain('data-i18n-values="firstName,lastName"');
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
test("should NOT emit data-i18n-values when values prop is absent", () => {
|
|
1660
|
+
const code = `
|
|
1661
|
+
import { Trans } from "react-i18next";
|
|
1662
|
+
export default function App() {
|
|
1663
|
+
return <h3><Trans i18nKey="features.title" /></h3>;
|
|
1664
|
+
}
|
|
1665
|
+
`;
|
|
1666
|
+
const result = transform(code);
|
|
1667
|
+
expect(result).not.toContain("data-i18n-values");
|
|
1668
|
+
});
|
|
1669
|
+
|
|
1670
|
+
test("should NOT emit data-i18n-values when values prop is empty object", () => {
|
|
1671
|
+
const code = `
|
|
1672
|
+
import { Trans } from "react-i18next";
|
|
1673
|
+
export default function App() {
|
|
1674
|
+
return <p><Trans i18nKey="simple" values={{}} /></p>;
|
|
1675
|
+
}
|
|
1676
|
+
`;
|
|
1677
|
+
const result = transform(code);
|
|
1678
|
+
expect(result).not.toContain("data-i18n-values");
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
test("should NOT crash and NOT emit data-i18n-values when values uses spread", () => {
|
|
1682
|
+
const code = `
|
|
1683
|
+
import { Trans } from "react-i18next";
|
|
1684
|
+
export default function App() {
|
|
1685
|
+
return <p><Trans i18nKey="msg" values={{ ...extraVars }} /></p>;
|
|
1686
|
+
}
|
|
1687
|
+
`;
|
|
1688
|
+
expect(() => transform(code)).not.toThrow();
|
|
1689
|
+
const result = transform(code);
|
|
1690
|
+
expect(result).not.toContain("data-i18n-values");
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
test("should emit data-i18n-values with custom namespace", () => {
|
|
1694
|
+
const code = `
|
|
1695
|
+
import { Trans } from "react-i18next";
|
|
1696
|
+
export default function App() {
|
|
1697
|
+
const count = 5;
|
|
1698
|
+
return <span><Trans i18nKey="items.count" ns="dashboard" values={{ count }} /></span>;
|
|
1699
|
+
}
|
|
1700
|
+
`;
|
|
1701
|
+
const result = transform(code);
|
|
1702
|
+
expect(result).toContain('data-i18n-values="count"');
|
|
1703
|
+
expect(result).toContain('data-upstart-i18n="dashboard:items.count"');
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
test("should deduplicate value keys across multiple Trans components in same parent", () => {
|
|
1707
|
+
const code = `
|
|
1708
|
+
import { Trans } from "react-i18next";
|
|
1709
|
+
export default function App() {
|
|
1710
|
+
const year = 2026;
|
|
1711
|
+
return (
|
|
1712
|
+
<div>
|
|
1713
|
+
{flag
|
|
1714
|
+
? <Trans i18nKey="a" values={{ year }} />
|
|
1715
|
+
: <Trans i18nKey="b" values={{ year }} />}
|
|
1716
|
+
</div>
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
`;
|
|
1720
|
+
const result = transform(code);
|
|
1721
|
+
// "year" appears in both Trans but should only be emitted once
|
|
1722
|
+
const matches = result?.match(/data-i18n-values="year"/g);
|
|
1723
|
+
expect(matches?.length).toBe(1);
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
test("should merge value keys from multiple Trans with different keys", () => {
|
|
1727
|
+
const code = `
|
|
1728
|
+
import { Trans } from "react-i18next";
|
|
1729
|
+
export default function App() {
|
|
1730
|
+
const year = 2026;
|
|
1731
|
+
const name = "Alice";
|
|
1732
|
+
return (
|
|
1733
|
+
<div>
|
|
1734
|
+
{flag
|
|
1735
|
+
? <Trans i18nKey="a" values={{ year }} />
|
|
1736
|
+
: <Trans i18nKey="b" values={{ name }} />}
|
|
1737
|
+
</div>
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
`;
|
|
1741
|
+
const result = transform(code);
|
|
1742
|
+
expect(result).toContain("data-i18n-values=");
|
|
1743
|
+
const valuesAttr = result?.match(/data-i18n-values="([^"]+)"/)?.[1];
|
|
1744
|
+
const keys = valuesAttr?.split(",") ?? [];
|
|
1745
|
+
expect(keys).toContain("year");
|
|
1746
|
+
expect(keys).toContain("name");
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
test("data-i18n-values should be on parent element, not on Trans", () => {
|
|
1750
|
+
const code = `
|
|
1751
|
+
import { Trans } from "react-i18next";
|
|
1752
|
+
export default function App() {
|
|
1753
|
+
const year = 2026;
|
|
1754
|
+
return <footer><Trans i18nKey="copyright" values={{ year }} /></footer>;
|
|
1755
|
+
}
|
|
1756
|
+
`;
|
|
1757
|
+
const result = transform(code);
|
|
1758
|
+
// data-i18n-values must appear inside the <footer ...> opening tag, not inside <Trans>
|
|
1759
|
+
// Rough check: values attr and i18n attr should be on the same tag
|
|
1760
|
+
expect(result).toContain('data-i18n-values="year"');
|
|
1761
|
+
expect(result).toContain('data-upstart-i18n="translation:copyright"');
|
|
1762
|
+
// Trans should have no injected attrs (no hash)
|
|
1763
|
+
const hashMatches = result?.match(/data-upstart-hash="[a-f0-9]+"/g);
|
|
1764
|
+
expect(hashMatches?.length).toBe(1);
|
|
1765
|
+
});
|
|
1766
|
+
});
|
|
1506
1767
|
});
|
|
@@ -17,6 +17,14 @@ export const payloadEditText = z.object({
|
|
|
17
17
|
|
|
18
18
|
export type PayloadEditText = z.infer<typeof payloadEditText>;
|
|
19
19
|
|
|
20
|
+
export const payloadEditTextDirect = z.object({
|
|
21
|
+
action: z.literal("editTextDirect"),
|
|
22
|
+
id: z.string().min(1),
|
|
23
|
+
content: z.string(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type PayloadEditTextDirect = z.infer<typeof payloadEditTextDirect>;
|
|
27
|
+
|
|
20
28
|
export const payloadEditClassName = z.object({
|
|
21
29
|
action: z.literal("editClassName"),
|
|
22
30
|
id: z.string().min(1),
|
|
@@ -133,6 +141,36 @@ export class UpstartEditorAPI {
|
|
|
133
141
|
return { success: true, filePath };
|
|
134
142
|
}
|
|
135
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Edit a plain-text or rich-text JSX node directly in the source TSX file.
|
|
146
|
+
* For rich-text entries, `content` should be the inner JSX/HTML of the element's children.
|
|
147
|
+
*/
|
|
148
|
+
async editTextDirect(params: PayloadEditTextDirect): Promise<EditResult> {
|
|
149
|
+
const parsed = payloadEditTextDirect.safeParse(params);
|
|
150
|
+
if (!parsed.success) {
|
|
151
|
+
return { success: false, error: `Invalid payload: ${parsed.error.message}` };
|
|
152
|
+
}
|
|
153
|
+
const { id, content } = parsed.data;
|
|
154
|
+
if (!this.registry) {
|
|
155
|
+
try {
|
|
156
|
+
await this.loadRegistry();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return { success: false, error: `Failed to load registry: ${err}` };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const entry = this.registry!.elements[id];
|
|
162
|
+
if (!entry) {
|
|
163
|
+
return { success: false, error: `Element ${id} not found in registry` };
|
|
164
|
+
}
|
|
165
|
+
if (entry.type !== "text" && entry.type !== "rich-text" && entry.type !== "mixed-text") {
|
|
166
|
+
return { success: false, error: `Element ${id} is not a text element (type: ${entry.type})` };
|
|
167
|
+
}
|
|
168
|
+
if (entry.type === "mixed-text") {
|
|
169
|
+
return this.applyMixedTextEdit(id, entry, content);
|
|
170
|
+
}
|
|
171
|
+
return this.applyEdit(id, entry, content);
|
|
172
|
+
}
|
|
173
|
+
|
|
136
174
|
/**
|
|
137
175
|
* Edit the className of an element
|
|
138
176
|
*/
|
|
@@ -222,6 +260,39 @@ export class UpstartEditorAPI {
|
|
|
222
260
|
}
|
|
223
261
|
}
|
|
224
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Reconstruct JSX children from a user-edited template and the original expression segments,
|
|
265
|
+
* then write the result back to the TSX source file.
|
|
266
|
+
*
|
|
267
|
+
* `template` uses `{{N}}` placeholders for expressions, e.g.:
|
|
268
|
+
* "© {{0}} Alex's Kitchen. All rights reserved."
|
|
269
|
+
* The server replaces each placeholder with the original expression source from the registry.
|
|
270
|
+
*/
|
|
271
|
+
private async applyMixedTextEdit(id: string, entry: EditableEntry, template: string): Promise<EditResult> {
|
|
272
|
+
const exprSegments = (entry.segments ?? []).filter((s) => s.type === "expr");
|
|
273
|
+
|
|
274
|
+
// Split template by {{N}} placeholders — odd indices are expression indices
|
|
275
|
+
const parts = template.split(/\{\{(\d+)\}\}/);
|
|
276
|
+
|
|
277
|
+
// Reconstruct inner JSX: text parts interleaved with original expression sources
|
|
278
|
+
let inner = "";
|
|
279
|
+
for (let i = 0; i < parts.length; i++) {
|
|
280
|
+
if (i % 2 === 0) {
|
|
281
|
+
inner += parts[i];
|
|
282
|
+
} else {
|
|
283
|
+
const exprSeg = exprSegments[parseInt(parts[i])];
|
|
284
|
+
inner += exprSeg ? exprSeg.raw : `{{${parts[i]}}}`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Preserve leading/trailing JSX whitespace (indentation) from the original source
|
|
289
|
+
const leadingWS = entry.originalContent.match(/^(\s*)/)?.[1] ?? "";
|
|
290
|
+
const trailingWS = entry.originalContent.match(/(\s*)$/)?.[1] ?? "";
|
|
291
|
+
const newContent = leadingWS + inner + trailingWS;
|
|
292
|
+
|
|
293
|
+
return this.applyEdit(id, entry, newContent);
|
|
294
|
+
}
|
|
295
|
+
|
|
225
296
|
/**
|
|
226
297
|
* Get element info by ID
|
|
227
298
|
*/
|