@assistant-ui/x-generative-compiler 0.0.5 → 0.0.6

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.
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import * as nodePath from "node:path";
4
- import { describe, it, expect } from "vitest";
4
+ import { describe, it, expect, vi } from "vitest";
5
5
  import {
6
6
  compileGenerative,
7
7
  isGenerativeModule,
@@ -86,6 +86,19 @@ function createMergeFixture(childSource: string): string {
86
86
  return nodePath.join(appRoot, "src", "toolkit.tsx");
87
87
  }
88
88
 
89
+ function createMultiMergeFixture(files: Record<string, string>): string {
90
+ const appRoot = mkdtempSync(
91
+ nodePath.join(tmpdir(), "aui-generative-merge-many-"),
92
+ );
93
+ const srcRoot = nodePath.join(appRoot, "src");
94
+ for (const [relativePath, source] of Object.entries(files)) {
95
+ const file = nodePath.join(srcRoot, relativePath);
96
+ mkdirSync(nodePath.dirname(file), { recursive: true });
97
+ writeFileSync(file, source);
98
+ }
99
+ return nodePath.join(srcRoot, "toolkit.tsx");
100
+ }
101
+
89
102
  /**
90
103
  * Like {@link createMergeFixture}, but also writes a (JSONC) `tsconfig.json`
91
104
  * with a `@/*` path alias so alias resolution can be exercised. The child lives
@@ -407,6 +420,41 @@ export default defineToolkit({
407
420
  expect(client.trimStart().startsWith('"use client"')).toBe(true);
408
421
  });
409
422
 
423
+ it("splits an unstable_interactableTool entry: client keeps render, server drops it", () => {
424
+ const src = `"use generative";
425
+ import { z } from "zod";
426
+ import { Notepad } from "@/ui/notepad";
427
+ import { defineToolkit, unstable_interactableTool } from "@assistant-ui/react";
428
+ export default defineToolkit({
429
+ notepad: unstable_interactableTool({
430
+ description: "A notepad.",
431
+ stateSchema: z.object({ content: z.string() }),
432
+ render: (props) => <Notepad {...props} />,
433
+ }),
434
+ });`;
435
+ const server = compileGenerative(src, { target: "server" }).code;
436
+ expect(server).toContain("unstable_interactableTool({");
437
+ expect(server).toContain('description: "A notepad."');
438
+ expect(server).toContain("z.object");
439
+ expect(server).not.toMatch(/render\s*:/);
440
+ expect(server).not.toContain("@/ui/notepad");
441
+ expect(server).not.toContain("server-only");
442
+ const client = compileGenerative(src, { target: "client" }).code;
443
+ expect(client.trimStart().startsWith('"use client"')).toBe(true);
444
+ expect(client).toContain("<Notepad");
445
+ expect(client).toContain('import { Notepad } from "@/ui/notepad"');
446
+ });
447
+
448
+ it("rejects unstable_interactableTool not imported from an assistant-ui package", () => {
449
+ const src = `"use generative";
450
+ import { defineToolkit } from "@assistant-ui/react";
451
+ import { unstable_interactableTool } from "@/my-tools";
452
+ export default defineToolkit({ notepad: unstable_interactableTool({ render: () => null }) });`;
453
+ expect(() => compileGenerative(src, { target: "server" })).toThrow(
454
+ /inline object literal/,
455
+ );
456
+ });
457
+
410
458
  it("routes provider tool config", () => {
411
459
  const src = `"use generative";
412
460
  import { defineToolkit, providerTool } from "@assistant-ui/react";
@@ -559,6 +607,291 @@ export default defineToolkit({
559
607
  expect(client).toContain('from "./tools/weather"');
560
608
  });
561
609
 
610
+ it("allows unique tool names across more than two imported generative spreads", () => {
611
+ const filename = createMultiMergeFixture({
612
+ "tools/weather.tsx": `"use generative";
613
+ import { defineToolkit } from "@assistant-ui/react";
614
+ export default defineToolkit({
615
+ get_weather: { execute: async () => 1, render: () => null },
616
+ });`,
617
+ "tools/database.tsx": `"use generative";
618
+ import { defineToolkit } from "@assistant-ui/react";
619
+ export default defineToolkit({
620
+ query_db: { execute: async () => 1, render: () => null },
621
+ });`,
622
+ "tools/calendar.tsx": `"use generative";
623
+ import { defineToolkit } from "@assistant-ui/react";
624
+ export default defineToolkit({
625
+ create_event: { execute: async () => 1, render: () => null },
626
+ });`,
627
+ "tools/email.tsx": `"use generative";
628
+ import { defineToolkit } from "@assistant-ui/react";
629
+ export default defineToolkit({
630
+ send_email: { execute: async () => 1, render: () => null },
631
+ });`,
632
+ });
633
+ const src = `"use generative";
634
+ import { defineToolkit } from "@assistant-ui/react";
635
+ import weatherTools from "./tools/weather";
636
+ import databaseTools from "./tools/database";
637
+ import calendarTools from "./tools/calendar";
638
+ import emailTools from "./tools/email";
639
+ export default defineToolkit({
640
+ ...weatherTools,
641
+ ...databaseTools,
642
+ ...calendarTools,
643
+ ...emailTools,
644
+ });`;
645
+
646
+ const client = compileGenerative(src, { target: "client", filename }).code;
647
+ expect(client).toContain("...weatherTools");
648
+ expect(client).toContain("...databaseTools");
649
+ expect(client).toContain("...calendarTools");
650
+ expect(client).toContain("...emailTools");
651
+ });
652
+
653
+ it("warns about duplicate tool names across more than two imported generative spreads", () => {
654
+ const filename = createMultiMergeFixture({
655
+ "tools/weather.tsx": `"use generative";
656
+ import { defineToolkit } from "@assistant-ui/react";
657
+ export default defineToolkit({
658
+ get_weather: { execute: async () => 1, render: () => null },
659
+ search: { execute: async () => 1, render: () => null },
660
+ });`,
661
+ "tools/database.tsx": `"use generative";
662
+ import { defineToolkit } from "@assistant-ui/react";
663
+ export default defineToolkit({
664
+ query_db: { execute: async () => 1, render: () => null },
665
+ });`,
666
+ "tools/calendar.tsx": `"use generative";
667
+ import { defineToolkit } from "@assistant-ui/react";
668
+ export default defineToolkit({
669
+ search: { execute: async () => 1, render: () => null },
670
+ });`,
671
+ "tools/email.tsx": `"use generative";
672
+ import { defineToolkit } from "@assistant-ui/react";
673
+ export default defineToolkit({
674
+ send_email: { execute: async () => 1, render: () => null },
675
+ });`,
676
+ });
677
+ const src = `"use generative";
678
+ import { defineToolkit } from "@assistant-ui/react";
679
+ import weatherTools from "./tools/weather";
680
+ import databaseTools from "./tools/database";
681
+ import calendarTools from "./tools/calendar";
682
+ import emailTools from "./tools/email";
683
+ export default defineToolkit({
684
+ ...weatherTools,
685
+ ...databaseTools,
686
+ ...calendarTools,
687
+ ...emailTools,
688
+ });`;
689
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
690
+
691
+ try {
692
+ const client = compileGenerative(src, {
693
+ target: "client",
694
+ filename,
695
+ }).code;
696
+
697
+ expect(client).toContain("...weatherTools");
698
+ expect(client).toContain("...databaseTools");
699
+ expect(client).toContain("...calendarTools");
700
+ expect(client).toContain("...emailTools");
701
+ expect(warn).toHaveBeenCalledWith(
702
+ expect.stringContaining("[assistant-ui/use-generative]"),
703
+ );
704
+ expect(warn).toHaveBeenCalledWith(
705
+ expect.stringContaining(
706
+ 'Duplicate tool name "search" while composing toolkits. ' +
707
+ "JavaScript object spread keeps the last definition.",
708
+ ),
709
+ );
710
+ } finally {
711
+ warn.mockRestore();
712
+ }
713
+ });
714
+
715
+ it("does not reuse imported toolkit names across compile calls", () => {
716
+ const filename = createMultiMergeFixture({
717
+ "tools/weather.tsx": `"use generative";
718
+ import { defineToolkit } from "@assistant-ui/react";
719
+ export default defineToolkit({
720
+ get_weather: { execute: async () => 1, render: () => null },
721
+ });`,
722
+ "tools/calendar.tsx": `"use generative";
723
+ import { defineToolkit } from "@assistant-ui/react";
724
+ export default defineToolkit({
725
+ search: { execute: async () => 1, render: () => null },
726
+ });`,
727
+ });
728
+ const src = `"use generative";
729
+ import { defineToolkit } from "@assistant-ui/react";
730
+ import weatherTools from "./tools/weather";
731
+ import calendarTools from "./tools/calendar";
732
+ export default defineToolkit({
733
+ ...weatherTools,
734
+ ...calendarTools,
735
+ });`;
736
+
737
+ compileGenerative(src, { target: "client", filename });
738
+ writeFileSync(
739
+ nodePath.join(nodePath.dirname(filename), "tools", "weather.tsx"),
740
+ `"use generative";
741
+ import { defineToolkit } from "@assistant-ui/react";
742
+ export default defineToolkit({
743
+ search: { execute: async () => 1, render: () => null },
744
+ });`,
745
+ );
746
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
747
+
748
+ try {
749
+ compileGenerative(src, { target: "client", filename });
750
+
751
+ expect(warn).toHaveBeenCalledWith(
752
+ expect.stringContaining(
753
+ 'Duplicate tool name "search" while composing toolkits. ' +
754
+ "JavaScript object spread keeps the last definition.",
755
+ ),
756
+ );
757
+ } finally {
758
+ warn.mockRestore();
759
+ }
760
+ });
761
+
762
+ it("warns about duplicate tool names between local toolkit spreads", () => {
763
+ const src = `"use generative";
764
+ import { defineToolkit } from "@assistant-ui/react";
765
+ const weatherTools = defineToolkit({
766
+ get_weather: { execute: async () => 1, render: () => null },
767
+ search: { execute: async () => 1, render: () => null },
768
+ });
769
+ const databaseTools = defineToolkit({
770
+ query_db: { execute: async () => 1, render: () => null },
771
+ });
772
+ const calendarTools = defineToolkit({
773
+ search: { execute: async () => 1, render: () => null },
774
+ });
775
+ const emailTools = defineToolkit({
776
+ send_email: { execute: async () => 1, render: () => null },
777
+ });
778
+ export default defineToolkit({
779
+ ...weatherTools,
780
+ ...databaseTools,
781
+ ...calendarTools,
782
+ ...emailTools,
783
+ });`;
784
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
785
+
786
+ try {
787
+ const client = compileGenerative(src, { target: "client" }).code;
788
+
789
+ expect(client).toContain("...weatherTools");
790
+ expect(client).toContain("...databaseTools");
791
+ expect(client).toContain("...calendarTools");
792
+ expect(client).toContain("...emailTools");
793
+ expect(warn).toHaveBeenCalledWith(
794
+ expect.stringContaining(
795
+ '[assistant-ui/use-generative] Duplicate tool name "search" while composing toolkits. ' +
796
+ "JavaScript object spread keeps the last definition.",
797
+ ),
798
+ );
799
+ } finally {
800
+ warn.mockRestore();
801
+ }
802
+ });
803
+
804
+ it("warns once across server and client compiles", () => {
805
+ const src = `"use generative";
806
+ import { defineToolkit } from "@assistant-ui/react";
807
+ const weatherTools = defineToolkit({
808
+ search: { execute: async () => 1, render: () => null },
809
+ });
810
+ const calendarTools = defineToolkit({
811
+ search: { execute: async () => 1, render: () => null },
812
+ });
813
+ export default defineToolkit({
814
+ ...weatherTools,
815
+ ...calendarTools,
816
+ });`;
817
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
818
+
819
+ try {
820
+ compileGenerative(src, { target: "server" });
821
+ compileGenerative(src, { target: "client" });
822
+
823
+ expect(warn).toHaveBeenCalledTimes(1);
824
+ expect(warn).toHaveBeenCalledWith(
825
+ expect.stringContaining(
826
+ 'Duplicate tool name "search" while composing toolkits. ' +
827
+ "JavaScript object spread keeps the last definition.",
828
+ ),
829
+ );
830
+ } finally {
831
+ warn.mockRestore();
832
+ }
833
+ });
834
+
835
+ it("warns about duplicate names through nested local toolkit spreads", () => {
836
+ const src = `"use generative";
837
+ import { defineToolkit } from "@assistant-ui/react";
838
+ const baseTools = defineToolkit({
839
+ search: { execute: async () => 1, render: () => null },
840
+ });
841
+ const weatherTools = defineToolkit({
842
+ ...baseTools,
843
+ get_weather: { execute: async () => 1, render: () => null },
844
+ });
845
+ const calendarTools = defineToolkit({
846
+ search: { execute: async () => 1, render: () => null },
847
+ });
848
+ export default defineToolkit({
849
+ ...weatherTools,
850
+ ...calendarTools,
851
+ });`;
852
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
853
+
854
+ try {
855
+ compileGenerative(src, { target: "client" });
856
+
857
+ expect(warn).toHaveBeenCalledWith(
858
+ expect.stringContaining(
859
+ 'Duplicate tool name "search" while composing toolkits. ' +
860
+ "JavaScript object spread keeps the last definition.",
861
+ ),
862
+ );
863
+ } finally {
864
+ warn.mockRestore();
865
+ }
866
+ });
867
+
868
+ it("does not warn again when spreading a fragment with an internal duplicate", () => {
869
+ const src = `"use generative";
870
+ import { defineToolkit } from "@assistant-ui/react";
871
+ const baseTools = defineToolkit({
872
+ search: { execute: async () => 1, render: () => null },
873
+ search: { execute: async () => 2, render: () => null },
874
+ });
875
+ export default defineToolkit({
876
+ ...baseTools,
877
+ });`;
878
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
879
+
880
+ try {
881
+ compileGenerative(src, { target: "client" });
882
+
883
+ expect(warn).toHaveBeenCalledTimes(1);
884
+ expect(warn).toHaveBeenCalledWith(
885
+ expect.stringContaining(
886
+ 'Duplicate tool name "search" while composing toolkits. ' +
887
+ "JavaScript object spread keeps the last definition.",
888
+ ),
889
+ );
890
+ } finally {
891
+ warn.mockRestore();
892
+ }
893
+ });
894
+
562
895
  it("rejects spreading the default import of a non-generative module", () => {
563
896
  const filename = createMergeFixture(
564
897
  `export default { get_weather: { execute: async () => 1 } };\n`,
@@ -1068,17 +1401,34 @@ export default defineToolkit({
1068
1401
  import { defineToolkit, providerTool } from "@assistant-ui/react";
1069
1402
  export default defineToolkit({
1070
1403
  web_search: {
1071
- render: () => null,
1404
+ providerId: "openai.web_search_preview",
1072
1405
  execute: providerTool({
1073
1406
  providerId: "openai.web_search_preview",
1074
1407
  args: {},
1075
- render: "duplicate",
1076
1408
  }),
1077
1409
  },
1078
1410
  });`;
1079
1411
 
1080
1412
  expect(() => compileGenerative(src, { target: "server" })).toThrow(
1081
- /cannot duplicate tool properties/,
1413
+ /`providerTool\(\.\.\.\)` config for "web_search" duplicates "providerId"/,
1414
+ );
1415
+ });
1416
+
1417
+ it("rejects duplicate providerTool config properties with the duplicated key", () => {
1418
+ const src = `"use generative";
1419
+ import { defineToolkit, providerTool } from "@assistant-ui/react";
1420
+ export default defineToolkit({
1421
+ web_search: {
1422
+ execute: providerTool({
1423
+ providerId: "openai.web_search_preview",
1424
+ providerId: "openai.web_search_preview_2",
1425
+ args: {},
1426
+ }),
1427
+ },
1428
+ });`;
1429
+
1430
+ expect(() => compileGenerative(src, { target: "server" })).toThrow(
1431
+ /`providerTool\(\.\.\.\)` config for "web_search" duplicates "providerId"/,
1082
1432
  );
1083
1433
  });
1084
1434
 
@@ -1097,6 +1447,42 @@ export default defineToolkit({
1097
1447
  true,
1098
1448
  );
1099
1449
  expect(isGenerativeModule(`// a comment\n"use generative";\n`)).toBe(true);
1450
+ expect(isGenerativeModule(`"use generative"\nexport default {};`)).toBe(
1451
+ true,
1452
+ );
1453
+ expect(
1454
+ isGenerativeModule(`"use generative" // comment\nexport default {};`),
1455
+ ).toBe(true);
1456
+ expect(
1457
+ isGenerativeModule(`"use generative" /* comment */;\nexport default {};`),
1458
+ ).toBe(true);
1100
1459
  expect(isGenerativeModule(`export default {};`)).toBe(false);
1460
+ expect(
1461
+ isGenerativeModule(`"use generative" + suffix;\nexport default {};`),
1462
+ ).toBe(false);
1463
+ expect(
1464
+ isGenerativeModule(`"use generative"\n+ suffix;\nexport default {};`),
1465
+ ).toBe(false);
1466
+ expect(
1467
+ isGenerativeModule(`"use generative".toString();\nexport default {};`),
1468
+ ).toBe(false);
1469
+ expect(
1470
+ isGenerativeModule(`"use generative"\n.toString();\nexport default {};`),
1471
+ ).toBe(false);
1472
+ expect(
1473
+ isGenerativeModule(
1474
+ `"use generative" // comment\n+ suffix;\nexport default {};`,
1475
+ ),
1476
+ ).toBe(false);
1477
+ expect(
1478
+ isGenerativeModule(
1479
+ `"use generative" /* no line break */ + suffix;\nexport default {};`,
1480
+ ),
1481
+ ).toBe(false);
1482
+ expect(
1483
+ isGenerativeModule(
1484
+ `"use generative" /* first line\nsecond line */ + suffix;\nexport default {};`,
1485
+ ),
1486
+ ).toBe(false);
1101
1487
  });
1102
1488
  });