@runtypelabs/persona 3.27.0 → 3.29.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.27.0",
3
+ "version": "3.29.0",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -17,6 +17,11 @@
17
17
  "import": "./dist/theme-reference.js",
18
18
  "require": "./dist/theme-reference.cjs"
19
19
  },
20
+ "./codegen": {
21
+ "types": "./dist/codegen.d.ts",
22
+ "import": "./dist/codegen.js",
23
+ "require": "./dist/codegen.cjs"
24
+ },
20
25
  "./theme-editor": {
21
26
  "types": "./dist/theme-editor.d.ts",
22
27
  "import": "./dist/theme-editor.js",
@@ -99,12 +104,13 @@
99
104
  "access": "public"
100
105
  },
101
106
  "scripts": {
102
- "build": "rimraf dist && pnpm run build:styles && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:theme-ref && pnpm run build:theme-editor && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:animations",
107
+ "build": "rimraf dist && pnpm run build:styles && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:theme-ref && pnpm run build:codegen && pnpm run build:theme-editor && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:animations",
103
108
  "build:theme-editor": "tsup src/theme-editor.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
104
109
  "build:testing": "tsup src/testing.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
105
110
  "build:smart-dom-reader": "tsup src/smart-dom-reader.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
106
111
  "build:animations": "tsup src/animations/glyph-cycle.ts src/animations/wipe.ts --format esm,cjs --minify --dts --out-dir dist/animations --no-splitting",
107
112
  "build:theme-ref": "tsup src/theme-reference.ts --format esm,cjs --minify --dts",
113
+ "build:codegen": "tsup src/codegen.ts --format esm,cjs --minify --dts",
108
114
  "build:styles": "node -e \"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.copyFileSync('src/styles/widget.css','dist/widget.css');\"",
109
115
  "build:client": "tsup src/index.ts --format esm,cjs --minify --sourcemap --splitting false --dts --loader \".css=text\" && tsup src/index-global.ts --format iife --global-name AgentWidget --minify --sourcemap --splitting false --out-dir dist --loader \".css=text\" && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/index-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/index'+ext);}\"",
110
116
  "build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { generateCodeSnippet as fromSubpath } from "./codegen";
3
+ import { generateCodeSnippet as fromInternal } from "./utils/code-generators";
4
+ import { VERSION } from "./version";
5
+
6
+ // The `@runtypelabs/persona/codegen` subpath is the server/Worker-safe entry for
7
+ // snippet generation. It must expose the exact same generator as the internal
8
+ // module (and, transitively, the main barrel) — no fork, no drift.
9
+ describe("codegen subpath", () => {
10
+ const config = {
11
+ apiUrl: "https://api.example.com/chat",
12
+ clientToken: "ct_test_123",
13
+ launcher: { enabled: true, title: "Chat" },
14
+ };
15
+
16
+ it("re-exports the same generateCodeSnippet implementation", () => {
17
+ expect(fromSubpath).toBe(fromInternal);
18
+ });
19
+
20
+ it("produces identical output via the subpath for every format", () => {
21
+ const formats = [
22
+ "esm",
23
+ "script-installer",
24
+ "script-manual",
25
+ "script-advanced",
26
+ "react-component",
27
+ "react-advanced",
28
+ ] as const;
29
+ for (const format of formats) {
30
+ expect(fromSubpath(config, format)).toBe(fromInternal(config, format));
31
+ }
32
+ });
33
+
34
+ it("pins the installer CDN url to the package version (not @latest)", () => {
35
+ const code = fromSubpath(config, "script-installer");
36
+ expect(code).toContain(`@runtypelabs/persona@${VERSION}/dist/install.global.js`);
37
+ expect(code).not.toContain("@latest");
38
+ });
39
+
40
+ describe("mount target option", () => {
41
+ it("defaults to body when no target is given", () => {
42
+ expect(fromSubpath(config, "react-component")).toContain("target: 'body'");
43
+ expect(fromSubpath(config, "script-manual")).toContain("target: 'body'");
44
+ // installer omits the target key entirely (installer falls back to body)
45
+ expect(fromSubpath(config, "script-installer")).not.toContain('"target"');
46
+ });
47
+
48
+ it("emits the selector as the initAgentWidget target for code formats", () => {
49
+ const opts = { target: "#chat" };
50
+ expect(fromSubpath(config, "esm", opts)).toContain("target: '#chat'");
51
+ expect(fromSubpath(config, "react-component", opts)).toContain("target: '#chat'");
52
+ expect(fromSubpath(config, "script-manual", opts)).toContain("target: '#chat'");
53
+ });
54
+
55
+ it("serializes the selector as a top-level installer option (sibling of config)", () => {
56
+ // install.ts reads the mount point from the TOP LEVEL of data-config, not
57
+ // from inside the widget config. Parse the emitted data-config and assert
58
+ // the structure so a regression to the nested form is caught.
59
+ const code = fromSubpath(config, "script-installer", { target: "#chat" });
60
+ const json = code.match(/data-config='([^']*)'/)?.[1];
61
+ expect(json).toBeTruthy();
62
+ const parsed = JSON.parse(json!);
63
+ expect(parsed.target).toBe("#chat");
64
+ expect(parsed.config).toBeTruthy();
65
+ // target must NOT be buried inside the widget config (where it's ignored)
66
+ expect(parsed.config.target).toBeUndefined();
67
+ });
68
+
69
+ it("keeps target as a top-level sibling alongside windowKey", () => {
70
+ const code = fromSubpath(config, "script-installer", { target: "#chat", windowKey: "myWidget" });
71
+ const parsed = JSON.parse(code.match(/data-config='([^']*)'/)![1]);
72
+ expect(parsed.target).toBe("#chat");
73
+ expect(parsed.windowKey).toBe("myWidget");
74
+ expect(parsed.config).toBeTruthy();
75
+ });
76
+
77
+ it("escapes single quotes in the selector so it can't break the literal", () => {
78
+ const code = fromSubpath(config, "react-component", { target: "[data-x='y']" });
79
+ expect(code).toContain("target: '[data-x=\\'y\\']'");
80
+ });
81
+ });
82
+ });
package/src/codegen.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Pure code-snippet generation entry (`@runtypelabs/persona/codegen`).
3
+ *
4
+ * `generateCodeSnippet` is pure string-templating — it builds install snippets
5
+ * from a widget config and depends only on a type import + the `VERSION`
6
+ * constant. The main `index.ts` barrel re-exports it too, but that barrel pulls
7
+ * in the full widget runtime (`index-core` → idiomorph / marked / DOM), which is
8
+ * unacceptable for server/Worker consumers that only need snippet strings.
9
+ *
10
+ * This subpath exposes the generator on its own (mirroring `theme-reference`) so
11
+ * those consumers import it without dragging in the browser runtime. Existing
12
+ * npm consumers keep using the barrel export unchanged.
13
+ */
14
+
15
+ export { generateCodeSnippet } from "./utils/code-generators";
16
+ export type {
17
+ CodeFormat,
18
+ CodeGeneratorHooks,
19
+ CodeGeneratorOptions
20
+ } from "./utils/code-generators";
@@ -141,6 +141,16 @@ export type CodeGeneratorOptions = {
141
141
  * Only affects script formats (script-installer, script-manual, script-advanced).
142
142
  */
143
143
  windowKey?: string;
144
+
145
+ /**
146
+ * CSS selector for the mount target. When omitted, the widget mounts on
147
+ * `body` (the existing default). When provided, the generated snippet mounts
148
+ * into that element: ESM / React / manual / advanced formats emit it as the
149
+ * `target` argument to `initAgentWidget()`, and `script-installer` serializes
150
+ * it into `data-config` (the installer reads `config.target`).
151
+ * @default "body"
152
+ */
153
+ target?: string;
144
154
  };
145
155
 
146
156
  // Internal type for normalized hooks (always strings)
@@ -540,6 +550,15 @@ function appendSerializableObjectBlock(
540
550
  lines.push(`${indent}},`);
541
551
  }
542
552
 
553
+ /**
554
+ * Resolve the mount-target selector for a single-quoted JS string context.
555
+ * Defaults to `body` and escapes backslashes / single quotes so an arbitrary
556
+ * selector can't break out of the emitted `target: '...'` literal.
557
+ */
558
+ function emitTargetSelector(options?: CodeGeneratorOptions): string {
559
+ return (options?.target ?? "body").replace(/\\/g, "\\\\").replace(/'/g, "\\'");
560
+ }
561
+
543
562
  export function generateCodeSnippet(
544
563
  config: any,
545
564
  format: CodeFormat = "esm",
@@ -580,7 +599,7 @@ function generateESMCode(config: any, options?: CodeGeneratorOptions): string {
580
599
  "import { initAgentWidget, markdownPostprocessor } from '@runtypelabs/persona';",
581
600
  "",
582
601
  "initAgentWidget({",
583
- " target: 'body',",
602
+ ` target: '${emitTargetSelector(options)}',`,
584
603
  " config: {"
585
604
  ];
586
605
 
@@ -726,7 +745,7 @@ function generateReactComponentCode(config: any, options?: CodeGeneratorOptions)
726
745
  " let handle: AgentWidgetInitHandle | null = null;",
727
746
  "",
728
747
  " handle = initAgentWidget({",
729
- " target: 'body',",
748
+ ` target: '${emitTargetSelector(options)}',`,
730
749
  " config: {"
731
750
  ];
732
751
 
@@ -990,7 +1009,7 @@ function generateReactAdvancedCode(config: any, options?: CodeGeneratorOptions):
990
1009
  " };",
991
1010
  "",
992
1011
  " handle = initAgentWidget({",
993
- " target: 'body',",
1012
+ ` target: '${emitTargetSelector(options)}',`,
994
1013
  " config: {"
995
1014
  ];
996
1015
 
@@ -1356,10 +1375,20 @@ function buildSerializableConfig(config: any): Record<string, any> {
1356
1375
  function generateScriptInstallerCode(config: any, options?: CodeGeneratorOptions): string {
1357
1376
  const serializableConfig = buildSerializableConfig(config);
1358
1377
 
1359
- // When windowKey is provided, nest the widget config under `config` so the
1360
- // install script's parsedConfig.config detection picks it up alongside windowKey.
1361
- const payload = options?.windowKey
1362
- ? { config: serializableConfig, windowKey: options.windowKey }
1378
+ // `windowKey` and `target` are INSTALLER options, not widget-config fields.
1379
+ // install.ts reads them from the top level of data-config: the nested-`config`
1380
+ // branch hoists every sibling key (Object.assign(scriptConfig, parsed)), while
1381
+ // a flat payload becomes the widget config wholesale and its siblings are
1382
+ // ignored. So whenever either is set we emit the nested `{ config, ...}` form
1383
+ // with the option as a SIBLING of `config` — nesting `target` inside the
1384
+ // widget config would leave the widget mounted on `body`.
1385
+ const needsWrapper = Boolean(options?.windowKey || options?.target);
1386
+ const payload = needsWrapper
1387
+ ? {
1388
+ config: serializableConfig,
1389
+ ...(options?.windowKey ? { windowKey: options.windowKey } : {}),
1390
+ ...(options?.target ? { target: options.target } : {}),
1391
+ }
1363
1392
  : serializableConfig;
1364
1393
 
1365
1394
  // Escape single quotes in JSON for HTML attribute
@@ -1383,7 +1412,7 @@ function generateScriptManualCode(config: any, options?: CodeGeneratorOptions):
1383
1412
  "<!-- Initialize widget -->",
1384
1413
  "<script>",
1385
1414
  " var handle = window.AgentWidget.initAgentWidget({",
1386
- " target: 'body',",
1415
+ ` target: '${emitTargetSelector(options)}',`,
1387
1416
  ...(options?.windowKey ? [` windowKey: '${options.windowKey}',`] : []),
1388
1417
  " config: {"
1389
1418
  ];
@@ -1735,7 +1764,7 @@ function generateScriptAdvancedCode(config: any, options?: CodeGeneratorOptions)
1735
1764
  "",
1736
1765
  " // Initialize widget",
1737
1766
  " var handle = agentWidget.initAgentWidget({",
1738
- " target: 'body',",
1767
+ ` target: '${emitTargetSelector(options)}',`,
1739
1768
  " useShadowDom: false,",
1740
1769
  ...(options?.windowKey ? [` windowKey: '${options.windowKey}',`] : []),
1741
1770
  " config: widgetConfig",