@upstart.gg/vite-plugins 0.1.41 → 0.1.43

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 (37) hide show
  1. package/dist/upstart-editor-api.d.ts +59 -1
  2. package/dist/upstart-editor-api.d.ts.map +1 -1
  3. package/dist/upstart-editor-api.js +273 -4
  4. package/dist/upstart-editor-api.js.map +1 -1
  5. package/dist/vite-plugin-upstart-attrs.d.ts +2 -1
  6. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
  7. package/dist/vite-plugin-upstart-attrs.js +91 -4
  8. package/dist/vite-plugin-upstart-attrs.js.map +1 -1
  9. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js +342 -0
  10. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js.map +1 -0
  11. package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -1
  12. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +29 -2
  13. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  14. package/dist/vite-plugin-upstart-editor/runtime/form-guard.js +80 -0
  15. package/dist/vite-plugin-upstart-editor/runtime/form-guard.js.map +1 -0
  16. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts.map +1 -1
  17. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +2 -1
  18. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
  19. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
  20. package/dist/vite-plugin-upstart-editor/runtime/index.js +26 -4
  21. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  22. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
  23. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +40 -36
  24. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
  25. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +20 -4
  26. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  27. package/package.json +3 -3
  28. package/src/tests/vite-plugin-upstart-attrs.test.ts +412 -0
  29. package/src/upstart-editor-api.ts +314 -5
  30. package/src/vite-plugin-upstart-attrs.ts +154 -4
  31. package/src/vite-plugin-upstart-editor/runtime/array-controls.ts +478 -0
  32. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +43 -0
  33. package/src/vite-plugin-upstart-editor/runtime/form-guard.ts +121 -0
  34. package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +6 -1
  35. package/src/vite-plugin-upstart-editor/runtime/index.ts +20 -4
  36. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +49 -58
  37. package/src/vite-plugin-upstart-editor/runtime/types.ts +31 -4
@@ -1,5 +1,9 @@
1
1
  import { describe, test, expect, beforeEach } from "vitest";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import os from "os";
2
5
  import { transformWithOxc, getRegistry, clearRegistry } from "../vite-plugin-upstart-attrs";
6
+ import { UpstartEditorAPI, type EditableRegistry } from "../upstart-editor-api";
3
7
 
4
8
  // Helper to run transformation
5
9
  function transform(code: string, filePath = "test.tsx"): string | null {
@@ -7,6 +11,29 @@ function transform(code: string, filePath = "test.tsx"): string | null {
7
11
  return result ? result.code : null;
8
12
  }
9
13
 
14
+ // Write the current registry + source to a temp project and return a ready API.
15
+ // Call transform(source, file) first so the registry is populated.
16
+ async function setupEditorApi(source: string, file = "app/routes/about.tsx") {
17
+ const registry = getRegistry();
18
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "upstart-api-"));
19
+ const filePath = path.join(tempDir, file);
20
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
21
+ await fs.writeFile(filePath, source);
22
+ const registryPath = path.join(tempDir, "registry.json");
23
+ const editorRegistry: EditableRegistry = { version: 1, generatedAt: "test", elements: registry };
24
+ await fs.writeFile(registryPath, JSON.stringify(editorRegistry));
25
+ const api = new UpstartEditorAPI(tempDir, registryPath);
26
+ api.setRegistry(editorRegistry);
27
+ return { api, filePath, tempDir, registry };
28
+ }
29
+
30
+ // Extract the data-upstart-array-id value emitted for a mapped item.
31
+ function arrayIdOf(transformed: string): string {
32
+ const m = transformed.match(/data-upstart-array-id="([^"]+)"/);
33
+ if (!m) throw new Error("no data-upstart-array-id in output");
34
+ return m[1];
35
+ }
36
+
10
37
  describe("upstart-editor-vite-plugin", () => {
11
38
  describe("JSX Element Attribute Injection", () => {
12
39
  test("should inject data attributes into PascalCase components", () => {
@@ -309,6 +336,391 @@ describe("upstart-editor-vite-plugin", () => {
309
336
  });
310
337
  });
311
338
 
339
+ describe("Inline array-literal map items", () => {
340
+ beforeEach(() => clearRegistry());
341
+
342
+ test("makes <span>{item}</span> over an inline string-literal array editable", () => {
343
+ const code = `
344
+ export default function App() {
345
+ return (
346
+ <div>
347
+ {["TypeScript", "React", "Node.js"].map((tech) => (
348
+ <span key={tech}>{tech}</span>
349
+ ))}
350
+ </div>
351
+ );
352
+ }
353
+ `;
354
+ const result = transform(code, "about.tsx")!;
355
+
356
+ expect(result).toContain('data-upstart-editable-text="true"');
357
+ expect(result).toContain('data-upstart-editable-text-mode="plain"');
358
+ // Auto-injected loop index used to address the right element at runtime
359
+ expect(result).toContain("(tech, __i)");
360
+ // Indexed id expression: one id per array element, picked by the loop index
361
+ expect(result).toMatch(
362
+ /data-upstart-id=\{\["about\.tsx:\d+","about\.tsx:\d+","about\.tsx:\d+"\]\[__i\]\}/,
363
+ );
364
+ });
365
+
366
+ test("registers one text entry per array element, pointing at the literal content", () => {
367
+ const code = `export default () => <div>{["TypeScript", "React"].map((t) => <span>{t}</span>)}</div>;`;
368
+ transform(code, "about.tsx");
369
+
370
+ const entries = Object.values(getRegistry()).filter((e) => e.type === "text");
371
+ expect(entries).toHaveLength(2);
372
+ expect(entries.map((e) => e.originalContent).sort()).toEqual(["React", "TypeScript"]);
373
+ // Offsets point at the inner content (quotes excluded)
374
+ for (const e of entries) {
375
+ expect(code.slice(e.startOffset, e.endOffset)).toBe(e.originalContent);
376
+ }
377
+ });
378
+
379
+ test("supports number literals in the array", () => {
380
+ const code = `export default () => <div>{[2020, 2021].map((y) => <span>{y}</span>)}</div>;`;
381
+ transform(code, "about.tsx");
382
+ const entries = Object.values(getRegistry()).filter((e) => e.type === "text");
383
+ expect(entries.map((e) => e.originalContent).sort()).toEqual(["2020", "2021"]);
384
+ });
385
+
386
+ test("does NOT make items editable when the array is a variable (not inline)", () => {
387
+ const code = `
388
+ export default function App({ items }) {
389
+ return <div>{items.map((tech) => <span>{tech}</span>)}</div>;
390
+ }
391
+ `;
392
+ const result = transform(code, "about.tsx")!;
393
+ expect(result).toContain('data-upstart-editable-text="false"');
394
+ expect(result).not.toContain('data-upstart-editable-text="true"');
395
+ });
396
+
397
+ test("does NOT make items editable when the item is transformed", () => {
398
+ const code = `export default () => <div>{["a", "b"].map((t) => <span>{t.toUpperCase()}</span>)}</div>;`;
399
+ const result = transform(code, "about.tsx")!;
400
+ // {t.toUpperCase()} is a CallExpression → no in-place source to edit
401
+ expect(result).not.toContain('data-upstart-editable-text="true"');
402
+ });
403
+
404
+ test("round-trip: editing one rendered item rewrites the matching source literal", async () => {
405
+ const source = `export default () => <div>{["TypeScript", "React", "Node.js"].map((tech) => <span>{tech}</span>)}</div>;\n`;
406
+ transform(source, "app/routes/about.tsx");
407
+
408
+ // The middle element ("React") — locate its registry id.
409
+ const registry = getRegistry();
410
+ const reactId = Object.entries(registry).find(([, e]) => e.originalContent === "React")![0];
411
+
412
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "upstart-arr-"));
413
+ try {
414
+ const filePath = path.join(tempDir, "app", "routes", "about.tsx");
415
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
416
+ await fs.writeFile(filePath, source);
417
+
418
+ const registryPath = path.join(tempDir, "registry.json");
419
+ const editorRegistry: EditableRegistry = {
420
+ version: 1,
421
+ generatedAt: "test",
422
+ elements: registry,
423
+ };
424
+ await fs.writeFile(registryPath, JSON.stringify(editorRegistry));
425
+
426
+ const api = new UpstartEditorAPI(tempDir, registryPath);
427
+ api.setRegistry(editorRegistry);
428
+
429
+ const res = await api.editTextDirect({ action: "editTextDirect", id: reactId, content: "Vue" });
430
+ expect(res.success).toBe(true);
431
+
432
+ const updated = await fs.readFile(filePath, "utf-8");
433
+ // Only the middle literal changed; siblings untouched.
434
+ expect(updated).toContain(`["TypeScript", "Vue", "Node.js"]`);
435
+ } finally {
436
+ await fs.rm(tempDir, { recursive: true, force: true });
437
+ }
438
+ });
439
+
440
+ test("captures the source quote char in the registry entry", () => {
441
+ transform(`export default () => <div>{["A"].map((t) => <span>{t}</span>)}</div>;`, "about.tsx");
442
+ const entry = Object.values(getRegistry()).find((e) => e.type === "text")!;
443
+ expect(entry.quote).toBe('"');
444
+ });
445
+
446
+ test("round-trip: escapes quotes and backslashes typed into a string-literal item", async () => {
447
+ const source = `export default () => <div>{["React", "Vue"].map((t) => <span>{t}</span>)}</div>;\n`;
448
+ transform(source, "app/routes/about.tsx");
449
+ const registry = getRegistry();
450
+ const reactId = Object.entries(registry).find(([, e]) => e.originalContent === "React")![0];
451
+
452
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "upstart-esc-"));
453
+ try {
454
+ const filePath = path.join(tempDir, "app", "routes", "about.tsx");
455
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
456
+ await fs.writeFile(filePath, source);
457
+
458
+ const registryPath = path.join(tempDir, "registry.json");
459
+ const editorRegistry: EditableRegistry = { version: 1, generatedAt: "test", elements: registry };
460
+ await fs.writeFile(registryPath, JSON.stringify(editorRegistry));
461
+
462
+ const api = new UpstartEditorAPI(tempDir, registryPath);
463
+ api.setRegistry(editorRegistry);
464
+
465
+ const res = await api.editTextDirect({
466
+ action: "editTextDirect",
467
+ id: reactId,
468
+ content: 'a "quote" and a \\ back',
469
+ });
470
+ expect(res.success).toBe(true);
471
+
472
+ const updated = await fs.readFile(filePath, "utf-8");
473
+ // The written literal must be a valid double-quoted string.
474
+ expect(updated).toContain('["a \\"quote\\" and a \\\\ back", "Vue"]');
475
+ } finally {
476
+ await fs.rm(tempDir, { recursive: true, force: true });
477
+ }
478
+ });
479
+
480
+ test("escapes for the actual quote style (single quotes)", async () => {
481
+ const source = `export default () => <div>{['Alex'].map((t) => <span>{t}</span>)}</div>;\n`;
482
+ transform(source, "app/routes/about.tsx");
483
+ const registry = getRegistry();
484
+ const id = Object.entries(registry).find(([, e]) => e.originalContent === "Alex")![0];
485
+
486
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "upstart-esc2-"));
487
+ try {
488
+ const filePath = path.join(tempDir, "app", "routes", "about.tsx");
489
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
490
+ await fs.writeFile(filePath, source);
491
+ const registryPath = path.join(tempDir, "registry.json");
492
+ const editorRegistry: EditableRegistry = { version: 1, generatedAt: "test", elements: registry };
493
+ await fs.writeFile(registryPath, JSON.stringify(editorRegistry));
494
+ const api = new UpstartEditorAPI(tempDir, registryPath);
495
+ api.setRegistry(editorRegistry);
496
+
497
+ await api.editTextDirect({ action: "editTextDirect", id, content: "O'Brien" });
498
+ const updated = await fs.readFile(filePath, "utf-8");
499
+ expect(updated).toContain("['O\\'Brien']");
500
+ } finally {
501
+ await fs.rm(tempDir, { recursive: true, force: true });
502
+ }
503
+ });
504
+
505
+ test("round-trip: gives a value to an empty-string item (zero-length range)", async () => {
506
+ const source = `export default () => <div>{["", "React"].map((t) => <span>{t}</span>)}</div>;\n`;
507
+ transform(source, "app/routes/about.tsx");
508
+ const registry = getRegistry();
509
+ const emptyId = Object.entries(registry).find(
510
+ ([, e]) => e.type === "text" && e.originalContent === "",
511
+ )![0];
512
+
513
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "upstart-empty-"));
514
+ try {
515
+ const filePath = path.join(tempDir, "app", "routes", "about.tsx");
516
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
517
+ await fs.writeFile(filePath, source);
518
+ const registryPath = path.join(tempDir, "registry.json");
519
+ const editorRegistry: EditableRegistry = { version: 1, generatedAt: "test", elements: registry };
520
+ await fs.writeFile(registryPath, JSON.stringify(editorRegistry));
521
+ const api = new UpstartEditorAPI(tempDir, registryPath);
522
+ api.setRegistry(editorRegistry);
523
+
524
+ const res = await api.editTextDirect({ action: "editTextDirect", id: emptyId, content: "Vue" });
525
+ expect(res.success).toBe(true);
526
+ const updated = await fs.readFile(filePath, "utf-8");
527
+ expect(updated).toContain(`["Vue", "React"]`);
528
+ } finally {
529
+ await fs.rm(tempDir, { recursive: true, force: true });
530
+ }
531
+ });
532
+
533
+ test("round-trip: clearing an item to an empty string, then re-filling it", async () => {
534
+ const source = `export default () => <div>{["React", "Vue"].map((t) => <span>{t}</span>)}</div>;\n`;
535
+ transform(source, "app/routes/about.tsx");
536
+ const registry = getRegistry();
537
+ const id = Object.entries(registry).find(([, e]) => e.originalContent === "React")![0];
538
+
539
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "upstart-clear-"));
540
+ try {
541
+ const filePath = path.join(tempDir, "app", "routes", "about.tsx");
542
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
543
+ await fs.writeFile(filePath, source);
544
+ const registryPath = path.join(tempDir, "registry.json");
545
+ const editorRegistry: EditableRegistry = { version: 1, generatedAt: "test", elements: registry };
546
+ await fs.writeFile(registryPath, JSON.stringify(editorRegistry));
547
+ const api = new UpstartEditorAPI(tempDir, registryPath);
548
+ api.setRegistry(editorRegistry);
549
+
550
+ // Clear to empty (overwrite → zero-length), then re-fill (zero-length → insert).
551
+ expect((await api.editTextDirect({ action: "editTextDirect", id, content: "" })).success).toBe(true);
552
+ expect(await fs.readFile(filePath, "utf-8")).toContain(`["", "Vue"]`);
553
+
554
+ expect((await api.editTextDirect({ action: "editTextDirect", id, content: "Svelte" })).success).toBe(
555
+ true,
556
+ );
557
+ expect(await fs.readFile(filePath, "utf-8")).toContain(`["Svelte", "Vue"]`);
558
+ } finally {
559
+ await fs.rm(tempDir, { recursive: true, force: true });
560
+ }
561
+ });
562
+ });
563
+
564
+ describe("Inline array-literal add/delete (×/+ controls)", () => {
565
+ beforeEach(() => clearRegistry());
566
+
567
+ test("emits array-id and array-index on each mapped item", () => {
568
+ const result = transform(
569
+ `export default () => <div>{["A", "B"].map((t) => <span>{t}</span>)}</div>;`,
570
+ "about.tsx",
571
+ )!;
572
+ expect(result).toMatch(/data-upstart-array-id="about\.tsx:\d+"/);
573
+ expect(result).toContain("data-upstart-array-index={__i}");
574
+ });
575
+
576
+ test("add: appends a new element preserving the multi-line separator", async () => {
577
+ const source = `export default () => (\n <div>\n {[\n "TypeScript",\n "React",\n ].map((t) => (\n <span>{t}</span>\n ))}\n </div>\n);\n`;
578
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
579
+ const { api, filePath, tempDir } = await setupEditorApi(source);
580
+ try {
581
+ const res = await api.arrayItemAdd({ action: "arrayItemAdd", arrayId, content: "New item" });
582
+ expect(res.success).toBe(true);
583
+ const updated = await fs.readFile(filePath, "utf-8");
584
+ expect(updated).toContain(`"React",\n "New item",`);
585
+ } finally {
586
+ await fs.rm(tempDir, { recursive: true, force: true });
587
+ }
588
+ });
589
+
590
+ test("add: single-element array falls back to a ', ' separator", async () => {
591
+ const source = `export default () => <div>{["Only"].map((t) => <span>{t}</span>)}</div>;\n`;
592
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
593
+ const { api, filePath, tempDir } = await setupEditorApi(source);
594
+ try {
595
+ await api.arrayItemAdd({ action: "arrayItemAdd", arrayId, content: "Second" });
596
+ expect(await fs.readFile(filePath, "utf-8")).toContain(`["Only", "Second"]`);
597
+ } finally {
598
+ await fs.rm(tempDir, { recursive: true, force: true });
599
+ }
600
+ });
601
+
602
+ test("add: matches the existing quote style and escapes content", async () => {
603
+ const source = `export default () => <div>{['Alex'].map((t) => <span>{t}</span>)}</div>;\n`;
604
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
605
+ const { api, filePath, tempDir } = await setupEditorApi(source);
606
+ try {
607
+ await api.arrayItemAdd({ action: "arrayItemAdd", arrayId, content: "O'Brien" });
608
+ expect(await fs.readFile(filePath, "utf-8")).toContain("['Alex', 'O\\'Brien']");
609
+ } finally {
610
+ await fs.rm(tempDir, { recursive: true, force: true });
611
+ }
612
+ });
613
+
614
+ test("delete: removes a middle element cleanly", async () => {
615
+ const source = `export default () => <div>{["A", "B", "C"].map((t) => <span>{t}</span>)}</div>;\n`;
616
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
617
+ const { api, filePath, tempDir } = await setupEditorApi(source);
618
+ try {
619
+ const res = await api.arrayItemDelete({ action: "arrayItemDelete", arrayId, index: 1 });
620
+ expect(res.success).toBe(true);
621
+ expect(await fs.readFile(filePath, "utf-8")).toContain(`["A", "C"]`);
622
+ } finally {
623
+ await fs.rm(tempDir, { recursive: true, force: true });
624
+ }
625
+ });
626
+
627
+ test("delete: removes the first element cleanly", async () => {
628
+ const source = `export default () => <div>{["A", "B", "C"].map((t) => <span>{t}</span>)}</div>;\n`;
629
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
630
+ const { api, filePath, tempDir } = await setupEditorApi(source);
631
+ try {
632
+ await api.arrayItemDelete({ action: "arrayItemDelete", arrayId, index: 0 });
633
+ expect(await fs.readFile(filePath, "utf-8")).toContain(`["B", "C"]`);
634
+ } finally {
635
+ await fs.rm(tempDir, { recursive: true, force: true });
636
+ }
637
+ });
638
+
639
+ test("delete: refuses to remove the last remaining item", async () => {
640
+ const source = `export default () => <div>{["Only"].map((t) => <span>{t}</span>)}</div>;\n`;
641
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
642
+ const { api, filePath, tempDir } = await setupEditorApi(source);
643
+ try {
644
+ const res = await api.arrayItemDelete({ action: "arrayItemDelete", arrayId, index: 0 });
645
+ expect(res.success).toBe(false);
646
+ expect(await fs.readFile(filePath, "utf-8")).toContain(`["Only"]`);
647
+ } finally {
648
+ await fs.rm(tempDir, { recursive: true, force: true });
649
+ }
650
+ });
651
+
652
+ test("delete: out-of-range index returns an error", async () => {
653
+ const source = `export default () => <div>{["A", "B"].map((t) => <span>{t}</span>)}</div>;\n`;
654
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
655
+ const { api, tempDir } = await setupEditorApi(source);
656
+ try {
657
+ const res = await api.arrayItemDelete({ action: "arrayItemDelete", arrayId, index: 5 });
658
+ expect(res.success).toBe(false);
659
+ } finally {
660
+ await fs.rm(tempDir, { recursive: true, force: true });
661
+ }
662
+ });
663
+ });
664
+
665
+ describe("Inline array-literal arraySet (batched apply)", () => {
666
+ beforeEach(() => clearRegistry());
667
+
668
+ test("replaces the whole single-line array, preserving quote + escaping", async () => {
669
+ const source = `export default () => <div>{["A", "B", "C"].map((t) => <span>{t}</span>)}</div>;\n`;
670
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
671
+ const { api, filePath, tempDir } = await setupEditorApi(source);
672
+ try {
673
+ // Net effect of a session: delete "B", keep "A"/"C", add two new items.
674
+ const res = await api.arraySet({
675
+ action: "arraySet",
676
+ arrayId,
677
+ items: ["A", "C", "New item", 'has "quote"'],
678
+ });
679
+ expect(res.success).toBe(true);
680
+ expect(await fs.readFile(filePath, "utf-8")).toContain(`["A", "C", "New item", "has \\"quote\\""]`);
681
+ } finally {
682
+ await fs.rm(tempDir, { recursive: true, force: true });
683
+ }
684
+ });
685
+
686
+ test("preserves multi-line indentation and trailing comma", async () => {
687
+ const source = `export default () => (\n <div>\n {[\n "TypeScript",\n "React",\n ].map((t) => (\n <span>{t}</span>\n ))}\n </div>\n);\n`;
688
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
689
+ const { api, filePath, tempDir } = await setupEditorApi(source);
690
+ try {
691
+ await api.arraySet({ action: "arraySet", arrayId, items: ["TypeScript", "Go", "Rust"] });
692
+ const updated = await fs.readFile(filePath, "utf-8");
693
+ expect(updated).toContain(`[\n "TypeScript",\n "Go",\n "Rust",\n ].map`);
694
+ } finally {
695
+ await fs.rm(tempDir, { recursive: true, force: true });
696
+ }
697
+ });
698
+
699
+ test("matches single-quote style", async () => {
700
+ const source = `export default () => <div>{['A'].map((t) => <span>{t}</span>)}</div>;\n`;
701
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
702
+ const { api, filePath, tempDir } = await setupEditorApi(source);
703
+ try {
704
+ await api.arraySet({ action: "arraySet", arrayId, items: ["A", "B"] });
705
+ expect(await fs.readFile(filePath, "utf-8")).toContain(`['A', 'B']`);
706
+ } finally {
707
+ await fs.rm(tempDir, { recursive: true, force: true });
708
+ }
709
+ });
710
+
711
+ test("rejects an empty item list", async () => {
712
+ const source = `export default () => <div>{["A"].map((t) => <span>{t}</span>)}</div>;\n`;
713
+ const arrayId = arrayIdOf(transform(source, "app/routes/about.tsx")!);
714
+ const { api, tempDir } = await setupEditorApi(source);
715
+ try {
716
+ const res = await api.arraySet({ action: "arraySet", arrayId, items: [] });
717
+ expect(res.success).toBe(false);
718
+ } finally {
719
+ await fs.rm(tempDir, { recursive: true, force: true });
720
+ }
721
+ });
722
+ });
723
+
312
724
  describe("Content Hash", () => {
313
725
  test("should add data-upstart-hash to elements", () => {
314
726
  const code = `