mountly-vite-plugin 1.5.0 → 1.6.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/README.md CHANGED
@@ -89,6 +89,19 @@ Pass `shadow: true` when the host is not one you trust: every element in the
89
89
  distribution then renders into its own shadow root, and the build emits one
90
90
  stylesheet that it injects into no document.
91
91
 
92
+ Pass `peer: true` when one page loads several distributions, such as widgets
93
+ from several teams. The build leaves the framework, its mountly adapter and
94
+ mountly's runtime as bare imports (`react`, `mountly-react`, `mountly/embed`),
95
+ and the page maps them once in an import map, so every distribution shares one
96
+ React. Without it, each `embed.js` works alone on any page.
97
+
98
+ `custom-elements.json` carries each component's JSDoc as its description, and
99
+ each prop's JSDoc, its type as written (`"paid" | "overdue"`) and a JSON Schema
100
+ derived from that type. JSDoc tags add constraints: `@minimum`, `@maximum`,
101
+ `@minLength`, `@maxLength`, `@pattern` and `@format`. `@slot` tags declare the
102
+ slots a component renders, and the element fills them from the page's children.
103
+ Slots need a `shadow: true` distribution, and the build warns without one.
104
+
92
105
  See the [complete guide](https://mountly.dev/concepts/script-embeds/) and the
93
106
  [React](../../docs/examples/react-embed) and
94
107
  [mixed-framework](../../docs/examples/mixed-embed) examples.
package/dist/docs.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type ExtractOptions } from "./props.js";
2
+ /** A JSON Schema, as far as the build can derive one from a TypeScript type. */
3
+ export type PropSchema = Record<string, unknown>;
4
+ export interface ComponentDocs {
5
+ /** The component's JSDoc summary. */
6
+ description?: string;
7
+ /**
8
+ * Per prop: its JSDoc summary, its type as written (`"paid" | "overdue"`), and
9
+ * a JSON Schema derived from that type plus any `@minimum`, `@maximum`,
10
+ * `@minLength`, `@maxLength`, `@pattern` or `@format` tags.
11
+ */
12
+ props: Record<string, {
13
+ description?: string;
14
+ type?: string;
15
+ schema?: PropSchema;
16
+ }>;
17
+ /** `@slot` tags on the component: `@slot - body` or `@slot actions - the buttons`. */
18
+ slots?: Array<{
19
+ name: string;
20
+ description: string;
21
+ }>;
22
+ }
23
+ /**
24
+ * The prose a reader of the component's source would see: the JSDoc on the
25
+ * component and on each prop, and each prop's type as the author wrote it.
26
+ * The prop table says how to wire a prop; this says what it is for, which is
27
+ * what a person browsing a catalog — or a model composing from one — needs.
28
+ *
29
+ * Best effort: anything unreadable is simply absent, never an error.
30
+ */
31
+ export declare function extractDocs(code: string, options?: ExtractOptions): ComponentDocs;
package/dist/embed.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type UserConfig } from "vite";
2
- import type { MountlyWidgetFramework } from "./index.js";
2
+ import { type MountlyWidgetFramework } from "./externals.js";
3
3
  import { type PropSpec } from "./props.js";
4
4
  export interface MountlyElementEntry {
5
5
  /** Browser-compatible component source, also importable directly by your app. */
@@ -42,6 +42,17 @@ export interface MountlyElementsConfigOptions {
42
42
  * of host you ship to, not the component.
43
43
  */
44
44
  shadow?: boolean;
45
+ /**
46
+ * Leave the framework (React, Vue or Svelte), its mountly adapter and
47
+ * mountly's runtime as bare imports, for the page's import map to resolve.
48
+ *
49
+ * Off by default, because then one script tag works on any page. Turn it on
50
+ * when one page loads several distributions — widgets from several teams,
51
+ * say — so they share one copy of React instead of each bringing its own.
52
+ * The page must map every bare specifier the build leaves behind (`react`,
53
+ * `react-dom/client`, `mountly-react`, `mountly/embed`, …).
54
+ */
55
+ peer?: boolean;
45
56
  }
46
57
  /**
47
58
  * Route each `new Worker(…)` that means the platform's Worker through the
@@ -0,0 +1,11 @@
1
+ /** Which imports a build leaves for the page to provide, per framework. */
2
+ export type MountlyWidgetFramework = "react" | "vue" | "svelte";
3
+ export declare function getFrameworkPeerExternals(framework: MountlyWidgetFramework): Array<string | RegExp>;
4
+ /**
5
+ * The self-contained build's whole promise is "drop this file in a page and it
6
+ * works", so mountly's own runtime helpers are bundled in rather than
7
+ * externalised: the host then needs no import map at all. The peer build still
8
+ * externalises them, because sharing through the host's import map is the point
9
+ * of that build.
10
+ */
11
+ export declare function getSelfContainedExternals(framework: MountlyWidgetFramework): Array<string | RegExp>;
package/dist/index.d.ts CHANGED
@@ -1,16 +1,8 @@
1
1
  import type { Plugin, UserConfig } from "vite";
2
2
  export { mountlyHostPlugin, type MountlyHostPluginOptions } from "./host.js";
3
3
  export { mountlyManifestFragmentPlugin, type ManifestFragmentPluginOptions } from "./fragment.js";
4
- export type MountlyWidgetFramework = "react" | "vue" | "svelte";
5
- export declare function getFrameworkPeerExternals(framework: MountlyWidgetFramework): Array<string | RegExp>;
6
- /**
7
- * The self-contained build's whole promise is "drop this file in a page and it
8
- * works", so mountly's own runtime helpers are bundled in rather than
9
- * externalised: the host then needs no import map at all. The peer build still
10
- * externalises them, because sharing through the host's import map is the point
11
- * of that build.
12
- */
13
- export declare function getSelfContainedExternals(framework: MountlyWidgetFramework): Array<string | RegExp>;
4
+ import { getFrameworkPeerExternals, getSelfContainedExternals, type MountlyWidgetFramework } from "./externals.js";
5
+ export { getFrameworkPeerExternals, getSelfContainedExternals, type MountlyWidgetFramework };
14
6
  export interface MountlyCssAsTextOptions {
15
7
  include?: string | RegExp | (string | RegExp)[];
16
8
  }
@@ -71,4 +63,5 @@ export interface DefineMountlyWidgetConfigOptions {
71
63
  }
72
64
  export declare function defineMountlyWidgetConfig(options?: DefineMountlyWidgetConfigOptions): UserConfig[];
73
65
  export { defineElementsConfig, type MountlyElementsConfigOptions, type MountlyElementEntry, } from "./embed.js";
66
+ export { extractDocs, type ComponentDocs, type PropSchema } from "./docs.js";
74
67
  export { extractProps, type PropSpec, type PropKind } from "./props.js";
package/dist/index.js CHANGED
@@ -592,9 +592,31 @@ ${JSON.stringify({ imports }, null, 2)}
592
592
  };
593
593
  }
594
594
 
595
+ // src/externals.ts
596
+ var PLATFORM_EXTERNALS = ["mountly", /^mountly\//];
597
+ var FRAMEWORK_EXTERNALS = {
598
+ react: ["react", "react/jsx-runtime", "react-dom", "react-dom/client", "mountly-react"],
599
+ vue: ["vue", "mountly-vue"],
600
+ svelte: ["svelte", "mountly-svelte"]
601
+ };
602
+ var FRAMEWORK_BUNDLE = {
603
+ react: ["react", "react/jsx-runtime", "react-dom", "react-dom/client"],
604
+ vue: ["vue"],
605
+ svelte: ["svelte"]
606
+ };
607
+ function getFrameworkPeerExternals(framework) {
608
+ return [...PLATFORM_EXTERNALS, ...FRAMEWORK_EXTERNALS[framework]];
609
+ }
610
+ function getSelfContainedExternals(framework) {
611
+ return FRAMEWORK_EXTERNALS[framework].filter((id) => !FRAMEWORK_BUNDLE[framework].includes(id));
612
+ }
613
+
595
614
  // src/embed.ts
596
615
  import { globSync, readFileSync as readFileSync2 } from "fs";
597
616
  import { basename, dirname as dirname3, extname, relative as relative3, resolve as resolve3 } from "path";
617
+ import { parseAst as parseAst3 } from "vite";
618
+
619
+ // src/docs.ts
598
620
  import { parseAst as parseAst2 } from "vite";
599
621
 
600
622
  // src/props.ts
@@ -906,6 +928,177 @@ function extractProps(code, options = {}) {
906
928
  return takesProps(declaration) === false ? [] : null;
907
929
  }
908
930
 
931
+ // src/docs.ts
932
+ function docBlock(source, start) {
933
+ const match = /\/\*\*((?:[^*]|\*(?!\/))*)\*\/\s*$/.exec(source.slice(0, start));
934
+ if (!match) return { tags: [] };
935
+ const lines = (match[1] ?? "").split("\n").map((line) => line.replace(/^\s*\*? ?/, "").trim());
936
+ const joined = lines.join("\n").replace(/\s+@(?=[a-zA-Z])/g, "\n@");
937
+ const [prose = "", ...rest] = joined.split(/\n(?=@)/);
938
+ const tags = rest.map((tag) => {
939
+ const [, name2 = "", value = ""] = /^@(\w+)\s*([\s\S]*)$/.exec(tag.trim()) ?? [];
940
+ return [name2, value.replace(/\s+/g, " ").trim()];
941
+ });
942
+ const text = prose.trim();
943
+ return { ...text ? { text } : {}, tags };
944
+ }
945
+ var NUMERIC_TAGS = ["minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems"];
946
+ function constraints(tags) {
947
+ const out = {};
948
+ for (const [name2, value] of tags) {
949
+ if (NUMERIC_TAGS.includes(name2) && value !== "" && Number.isFinite(Number(value))) {
950
+ out[name2] = Number(value);
951
+ }
952
+ if (name2 === "pattern" || name2 === "format") out[name2] = value;
953
+ }
954
+ return out;
955
+ }
956
+ function schemaOf(type, types, source, depth = 0) {
957
+ if (!type || depth > 6) return {};
958
+ switch (type.type) {
959
+ case "TSStringKeyword":
960
+ return { type: "string" };
961
+ case "TSNumberKeyword":
962
+ return { type: "number" };
963
+ case "TSBooleanKeyword":
964
+ return { type: "boolean" };
965
+ case "TSNullKeyword":
966
+ return { type: "null" };
967
+ case "TSLiteralType": {
968
+ const value = type.literal?.value;
969
+ return value === void 0 ? {} : { const: value };
970
+ }
971
+ case "TSArrayType":
972
+ return { type: "array", items: schemaOf(type.elementType, types, source, depth + 1) };
973
+ case "TSParenthesizedType":
974
+ return schemaOf(type.typeAnnotation, types, source, depth);
975
+ case "TSUnionType": {
976
+ const parts = type.types.filter(
977
+ (t) => t.type !== "TSUndefinedKeyword" && t.type !== "TSNullKeyword"
978
+ );
979
+ if (parts.length === 1) return schemaOf(parts[0], types, source, depth);
980
+ const values = parts.map((t) => t.literal?.value);
981
+ if (parts.every((t) => t.type === "TSLiteralType") && values.every((v) => v !== void 0)) {
982
+ const kinds = new Set(values.map((v) => typeof v));
983
+ return kinds.size === 1 ? { type: [...kinds][0], enum: values } : { enum: values };
984
+ }
985
+ return { anyOf: parts.map((t) => schemaOf(t, types, source, depth + 1)) };
986
+ }
987
+ case "TSTypeLiteral":
988
+ case "TSInterfaceDeclaration":
989
+ case "TSIntersectionType":
990
+ return objectSchema(membersOf(type, types), types, source, depth);
991
+ case "TSTypeReference": {
992
+ const name2 = type.typeName?.name;
993
+ const args = type.typeArguments?.params ?? [];
994
+ if ((name2 === "Array" || name2 === "ReadonlyArray") && args[0]) {
995
+ return { type: "array", items: schemaOf(args[0], types, source, depth + 1) };
996
+ }
997
+ if (name2 === "Record" && args[1]) {
998
+ return {
999
+ type: "object",
1000
+ additionalProperties: schemaOf(args[1], types, source, depth + 1)
1001
+ };
1002
+ }
1003
+ const local = types.get(name2);
1004
+ if (!local) return {};
1005
+ return local.type === "TSInterfaceDeclaration" ? objectSchema(membersOf(local, types), types, source, depth) : schemaOf(local, types, source, depth + 1);
1006
+ }
1007
+ default:
1008
+ return {};
1009
+ }
1010
+ }
1011
+ function objectSchema(members, types, source, depth) {
1012
+ if (!members) return { type: "object" };
1013
+ const properties = {};
1014
+ const required = [];
1015
+ for (const member2 of members) {
1016
+ if (member2.type !== "TSPropertySignature") continue;
1017
+ const name2 = keyOf(member2);
1018
+ if (!name2) continue;
1019
+ const doc = docBlock(source, member2.start);
1020
+ properties[name2] = {
1021
+ ...schemaOf(member2.typeAnnotation?.typeAnnotation, types, source, depth + 1),
1022
+ ...constraints(doc.tags),
1023
+ ...doc.text ? { description: doc.text } : {}
1024
+ };
1025
+ if (!member2.optional) required.push(name2);
1026
+ }
1027
+ return {
1028
+ type: "object",
1029
+ properties,
1030
+ ...required.length ? { required } : {},
1031
+ additionalProperties: false
1032
+ };
1033
+ }
1034
+ function statementOf(body, node) {
1035
+ return body.find((s) => s.start <= node.start && node.start < s.end);
1036
+ }
1037
+ function htmlCommentBeforeScript(code) {
1038
+ const match = /<!--((?:(?!-->)[\s\S])*?)-->\s*<script\b/.exec(code);
1039
+ const text = match?.[1]?.trim();
1040
+ return text || void 0;
1041
+ }
1042
+ function extractDocs(code, options = {}) {
1043
+ try {
1044
+ const file = options.file ?? "component.tsx";
1045
+ const sfc = /\.(vue|svelte)$/.exec(file)?.[1];
1046
+ const source = sfc ? scriptOf(code) : code;
1047
+ const body = parseAst2(source, { lang: sfc ? "ts" : "tsx" }).body;
1048
+ const types = localTypes(body);
1049
+ let declared;
1050
+ let description;
1051
+ let component = { tags: [] };
1052
+ if (sfc === "vue") {
1053
+ const call = macroCall(body, "defineProps");
1054
+ declared = call?.typeArguments?.params?.[0];
1055
+ const statement = call && statementOf(body, call);
1056
+ component = statement ? docBlock(source, statement.start) : component;
1057
+ description = component.text ?? htmlCommentBeforeScript(code);
1058
+ } else if (sfc === "svelte") {
1059
+ const declarator = svelteProps(body);
1060
+ declared = declarator?.id?.typeAnnotation?.typeAnnotation;
1061
+ const statement = declarator && statementOf(body, declarator);
1062
+ component = statement ? docBlock(source, statement.start) : component;
1063
+ description = component.text ?? htmlCommentBeforeScript(code);
1064
+ } else {
1065
+ const declaration = findExport(body, options.exportName ?? "default");
1066
+ if (declaration) {
1067
+ const statement = statementOf(body, declaration);
1068
+ const before = statement ? docBlock(source, statement.start) : { tags: [] };
1069
+ component = before.text || before.tags.length ? before : docBlock(source, declaration.start);
1070
+ description = component.text;
1071
+ declared = propsType(declaration);
1072
+ }
1073
+ }
1074
+ const props = {};
1075
+ for (const member2 of membersOf(declared, types) ?? []) {
1076
+ const name2 = keyOf(member2);
1077
+ if (!name2) continue;
1078
+ const annotation = member2.typeAnnotation?.typeAnnotation;
1079
+ const doc = docBlock(source, member2.start);
1080
+ const type = annotation ? source.slice(annotation.start, annotation.end) : void 0;
1081
+ const schema = annotation ? { ...schemaOf(annotation, types, source), ...constraints(doc.tags) } : void 0;
1082
+ props[name2] = {
1083
+ ...doc.text ? { description: doc.text } : {},
1084
+ ...type ? { type } : {},
1085
+ ...schema && Object.keys(schema).length ? { schema } : {}
1086
+ };
1087
+ }
1088
+ const slots = component.tags.filter(([tag]) => tag === "slot").map(([, value]) => {
1089
+ const [, name2 = "", text = ""] = /^(?:([A-Za-z][\w-]*)\s*)?-?\s*([\s\S]*)$/.exec(value) ?? [];
1090
+ return { name: name2, description: text.trim() };
1091
+ });
1092
+ return {
1093
+ ...description ? { description } : {},
1094
+ props,
1095
+ ...slots.length ? { slots } : {}
1096
+ };
1097
+ } catch {
1098
+ return { props: {} };
1099
+ }
1100
+ }
1101
+
909
1102
  // src/embed.ts
910
1103
  function frameworkFor(file) {
911
1104
  if (file.endsWith(".vue")) return "vue";
@@ -1043,36 +1236,53 @@ function manifest(elements) {
1043
1236
  {
1044
1237
  schemaVersion: "1.0.0",
1045
1238
  readme: "",
1046
- modules: elements.map(({ tag, entry, props }) => ({
1047
- kind: "javascript-module",
1048
- path: entry.component,
1049
- declarations: [
1050
- {
1051
- kind: "class",
1052
- name: name(tag),
1053
- customElement: true,
1054
- tagName: tag,
1055
- members: props.filter((spec) => spec.kind !== "event").map((spec) => ({
1056
- kind: "field",
1057
- name: spec.name,
1058
- type: { text: TS_TYPE[spec.kind] ?? "unknown" }
1059
- })),
1060
- attributes: props.filter((spec) => spec.attribute).map((spec) => ({
1061
- name: spec.attribute,
1062
- fieldName: spec.name,
1063
- type: { text: TS_TYPE[spec.kind] ?? "unknown" }
1064
- })),
1065
- events: props.filter((spec) => spec.kind === "event").map((spec) => ({ name: spec.event, type: { text: "CustomEvent" } }))
1066
- }
1067
- ],
1068
- exports: [
1069
- {
1070
- kind: "custom-element-definition",
1071
- name: tag,
1072
- declaration: { name: name(tag), module: entry.component }
1073
- }
1074
- ]
1075
- }))
1239
+ modules: elements.map(({ tag, entry, props, docs }) => {
1240
+ const cemType = (spec) => ({
1241
+ text: docs.props[spec.name]?.type ?? TS_TYPE[spec.kind] ?? "unknown"
1242
+ });
1243
+ const described = (spec) => {
1244
+ const { description, schema } = docs.props[spec.name] ?? {};
1245
+ return { ...description ? { description } : {}, ...schema ? { schema } : {} };
1246
+ };
1247
+ return {
1248
+ kind: "javascript-module",
1249
+ path: entry.component,
1250
+ declarations: [
1251
+ {
1252
+ kind: "class",
1253
+ name: name(tag),
1254
+ ...docs.description ? { description: docs.description } : {},
1255
+ ...docs.slots ? { slots: docs.slots } : {},
1256
+ customElement: true,
1257
+ tagName: tag,
1258
+ members: props.filter((spec) => spec.kind !== "event").map((spec) => ({
1259
+ kind: "field",
1260
+ name: spec.name,
1261
+ type: cemType(spec),
1262
+ ...described(spec)
1263
+ })),
1264
+ attributes: props.filter((spec) => spec.attribute).map((spec) => ({
1265
+ name: spec.attribute,
1266
+ fieldName: spec.name,
1267
+ type: cemType(spec),
1268
+ ...described(spec)
1269
+ })),
1270
+ events: props.filter((spec) => spec.kind === "event").map((spec) => ({
1271
+ name: spec.event,
1272
+ type: { text: "CustomEvent" },
1273
+ ...described(spec)
1274
+ }))
1275
+ }
1276
+ ],
1277
+ exports: [
1278
+ {
1279
+ kind: "custom-element-definition",
1280
+ name: tag,
1281
+ declaration: { name: name(tag), module: entry.component }
1282
+ }
1283
+ ]
1284
+ };
1285
+ })
1076
1286
  },
1077
1287
  null,
1078
1288
  2
@@ -1234,7 +1444,7 @@ function shimWorkers(code) {
1234
1444
  walk(child, node, outside ? shadowed : hidden);
1235
1445
  }
1236
1446
  };
1237
- walk(parseAst2(code, { lang: "js" }), void 0, false);
1447
+ walk(parseAst3(code, { lang: "js" }), void 0, false);
1238
1448
  if (!callees.length) return null;
1239
1449
  let out = code;
1240
1450
  for (const { start, end } of callees.sort((a, b) => b.start - a.start)) {
@@ -1242,18 +1452,48 @@ function shimWorkers(code) {
1242
1452
  }
1243
1453
  return WORKER_SHIM + out;
1244
1454
  }
1245
- function readPropTable(entry, tag, root) {
1246
- const file = resolve3(root, entry.component);
1247
- const props = extractProps(readFileSync2(file, "utf8"), {
1248
- file,
1249
- exportName: entry.exportName
1250
- });
1455
+ var NATIVE_BUBBLING = /* @__PURE__ */ new Set([
1456
+ "click",
1457
+ "dblclick",
1458
+ "input",
1459
+ "change",
1460
+ "submit",
1461
+ "reset",
1462
+ "select",
1463
+ "keydown",
1464
+ "keyup",
1465
+ "focusin",
1466
+ "focusout",
1467
+ "pointerdown",
1468
+ "pointerup",
1469
+ "mousedown",
1470
+ "mouseup",
1471
+ "contextmenu",
1472
+ "wheel",
1473
+ "beforeinput",
1474
+ "invalid"
1475
+ ]);
1476
+ function warnNativeEvents(elements, warn) {
1477
+ for (const { tag, props } of elements) {
1478
+ for (const spec of props) {
1479
+ if (spec.kind === "event" && spec.event && NATIVE_BUBBLING.has(spec.event)) {
1480
+ warn(
1481
+ `[mountly] <${tag}> ${spec.name} dispatches "${spec.event}", which is also a native event bubbling out of the component. Listeners will get both. Rename the prop (e.g. ${spec.name}Item) so its event is unambiguous.`
1482
+ );
1483
+ }
1484
+ }
1485
+ }
1486
+ }
1487
+ function readElementSource(entry, tag, root) {
1488
+ const source = readFileSync2(resolve3(root, entry.component), "utf8");
1489
+ const opts = { file: entry.component, exportName: entry.exportName };
1490
+ const props = entry.props ?? extractProps(source, opts);
1251
1491
  if (!props) {
1252
1492
  throw new Error(
1253
1493
  `[mountly] cannot read the props of ${entry.component} for <${tag}>. Declare the props type in that file, or pass \`props\` on the element entry.`
1254
1494
  );
1255
1495
  }
1256
- return props;
1496
+ return { props, docs: extractDocs(source, opts) };
1257
1497
  }
1258
1498
  function defineElementsConfig(options) {
1259
1499
  const entryId = "virtual:mountly-embed";
@@ -1269,12 +1509,25 @@ function defineElementsConfig(options) {
1269
1509
  configResolved(config) {
1270
1510
  root = config.root;
1271
1511
  assertSingleCompiler(config.plugins);
1272
- elements = normalize(options, root).map(([tag, entry]) => ({
1273
- tag,
1274
- entry,
1275
- props: entry.props ?? readPropTable(entry, tag, root),
1276
- framework: entry.framework ?? frameworkFor(entry.component)
1277
- }));
1512
+ elements = normalize(options, root).map(([tag, entry]) => {
1513
+ const { props, docs } = readElementSource(entry, tag, root);
1514
+ return {
1515
+ tag,
1516
+ entry,
1517
+ props,
1518
+ framework: entry.framework ?? frameworkFor(entry.component),
1519
+ docs
1520
+ };
1521
+ });
1522
+ const warn = config.logger?.warn.bind(config.logger) ?? console.warn;
1523
+ warnNativeEvents(elements, warn);
1524
+ if (!shadow) {
1525
+ for (const { tag } of elements.filter((element) => element.docs.slots)) {
1526
+ warn(
1527
+ `[mountly] <${tag}> declares @slot, but slots project only in a shadow root. Build the distribution with \`shadow: true\`, or the page's children render after the component instead of inside it.`
1528
+ );
1529
+ }
1530
+ }
1278
1531
  },
1279
1532
  resolveId(id) {
1280
1533
  if (id === entryId || id.startsWith(widgetPrefix)) return `\0${id}`;
@@ -1282,10 +1535,11 @@ function defineElementsConfig(options) {
1282
1535
  load(id) {
1283
1536
  if (id === `\0${entryId}`) {
1284
1537
  const definitions = elements.map(
1285
- ({ tag, entry, props }) => `${JSON.stringify(tag)}: {
1538
+ ({ tag, entry, props, docs }) => `${JSON.stringify(tag)}: {
1286
1539
  load: () => import(${JSON.stringify(widgetPrefix + tag)}),
1287
1540
  props: ${JSON.stringify(props)},
1288
- trigger: ${JSON.stringify(entry.trigger ?? "connected")}
1541
+ trigger: ${JSON.stringify(entry.trigger ?? "connected")}${docs.slots ? `,
1542
+ slots: ${JSON.stringify(docs.slots.map((slot) => slot.name))}` : ""}
1289
1543
  }`
1290
1544
  );
1291
1545
  return `import { defineElements } from "mountly/embed";
@@ -1358,6 +1612,18 @@ export default createWidget(component[${JSON.stringify(element.entry.exportName
1358
1612
  // stylesheet nothing injects, which each element adopts for itself.
1359
1613
  cssCodeSplit: !shadow,
1360
1614
  rollupOptions: {
1615
+ ...options.peer ? {
1616
+ external: [
1617
+ ...new Set(
1618
+ [...frameworks].flatMap((framework) => [
1619
+ ...getFrameworkPeerExternals(framework),
1620
+ // Compiled Svelte imports `svelte/internal/client`; bundling it
1621
+ // beside an external `svelte` would mean two runtimes.
1622
+ ...framework === "svelte" ? [/^svelte\//] : []
1623
+ ])
1624
+ )
1625
+ ]
1626
+ } : {},
1361
1627
  input: entryId,
1362
1628
  output: { entryFileNames: "embed.js", chunkFileNames: "assets/[name]-[hash].js" }
1363
1629
  }
@@ -1366,23 +1632,6 @@ export default createWidget(component[${JSON.stringify(element.entry.exportName
1366
1632
  }
1367
1633
 
1368
1634
  // src/index.ts
1369
- var PLATFORM_EXTERNALS = ["mountly", /^mountly\//];
1370
- var FRAMEWORK_EXTERNALS = {
1371
- react: ["react", "react/jsx-runtime", "react-dom", "react-dom/client", "mountly-react"],
1372
- vue: ["vue", "mountly-vue"],
1373
- svelte: ["svelte", "mountly-svelte"]
1374
- };
1375
- var FRAMEWORK_BUNDLE = {
1376
- react: ["react", "react/jsx-runtime", "react-dom", "react-dom/client"],
1377
- vue: ["vue"],
1378
- svelte: ["svelte"]
1379
- };
1380
- function getFrameworkPeerExternals(framework) {
1381
- return [...PLATFORM_EXTERNALS, ...FRAMEWORK_EXTERNALS[framework]];
1382
- }
1383
- function getSelfContainedExternals(framework) {
1384
- return FRAMEWORK_EXTERNALS[framework].filter((id) => !FRAMEWORK_BUNDLE[framework].includes(id));
1385
- }
1386
1635
  function matchesFilter(id, include) {
1387
1636
  return include.some(
1388
1637
  (pattern) => typeof pattern === "string" ? id.includes(pattern) : pattern.test(id)
@@ -1556,6 +1805,7 @@ function defineMountlyWidgetConfig(options = {}) {
1556
1805
  export {
1557
1806
  defineElementsConfig,
1558
1807
  defineMountlyWidgetConfig,
1808
+ extractDocs,
1559
1809
  extractProps,
1560
1810
  getFrameworkPeerExternals,
1561
1811
  getSelfContainedExternals,
package/dist/props.d.ts CHANGED
@@ -20,6 +20,26 @@ export interface PropSpec {
20
20
  export declare function kebab(name: string): string;
21
21
  /** `onViewDetails` → `view-details`. */
22
22
  export declare function eventName(prop: string): string;
23
+ export type Node = Record<string, any>;
24
+ /** Local `interface`/`type` declarations, so a `Props` reference can be resolved. */
25
+ export declare function localTypes(body: Node[]): Map<string, Node>;
26
+ /** The members of a type literal, an interface (with its bases), or a reference. */
27
+ export declare function membersOf(type: Node | undefined, types: Map<string, Node>, seen?: number): Node[] | null;
28
+ /** Find the declaration a build entry points at: `default`, or a named export. */
29
+ export declare function findExport(body: Node[], exportName: string): Node | null;
30
+ export declare function propsType(declaration: Node): Node | undefined;
31
+ /** The `<script>` halves of an SFC, which is where all the types live. */
32
+ export declare function scriptOf(code: string): string;
33
+ /** `label` or `'aria-label'` — both name a prop. */
34
+ export declare function keyOf(node: Node): string | null;
35
+ /**
36
+ * `defineProps<T>()` / `defineEmits<T>()`, wherever it sits — including inside
37
+ * `withDefaults(defineProps<T>(), { … })`, which is how a Vue component with
38
+ * default values is normally written.
39
+ */
40
+ export declare function macroCall(body: Node[], macro: string): Node | undefined;
41
+ /** Svelte 5: `let { … }: Props = $props()`. */
42
+ export declare function svelteProps(body: Node[]): Node | null;
23
43
  export interface ExtractOptions {
24
44
  /** Path or filename. Its extension selects the component dialect. */
25
45
  file?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mountly-vite-plugin",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Vite lib build plugin for mountly vertical widgets (dual ESM output)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {