mountly-vite-plugin 1.5.0 → 1.7.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
- import { basename, dirname as dirname3, extname, relative as relative3, resolve as resolve3 } from "path";
616
+ import { basename, dirname as dirname4, extname, relative as relative3, resolve as resolve3 } from "path";
617
+ import { parseAst as parseAst4 } 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,454 @@ 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
+
1102
+ // src/types.ts
1103
+ import { dirname as dirname3, resolve as resolvePath } from "path";
1104
+ import { parseAst as parseAst3 } from "vite";
1105
+ function parseModule(file, code) {
1106
+ const sfc = /\.(vue|svelte)$/.exec(file)?.[1];
1107
+ const source = sfc ? scriptOf(code) : code;
1108
+ const body = parseAst3(source, { lang: sfc ? "ts" : "tsx" }).body;
1109
+ const mod = {
1110
+ file,
1111
+ source,
1112
+ body,
1113
+ types: /* @__PURE__ */ new Map(),
1114
+ imports: /* @__PURE__ */ new Map(),
1115
+ exports: /* @__PURE__ */ new Map(),
1116
+ opaque: /* @__PURE__ */ new Set()
1117
+ };
1118
+ for (const raw of body) {
1119
+ const node = raw.declaration ?? raw;
1120
+ if (node.type === "TSInterfaceDeclaration" || node.type === "TSTypeAliasDeclaration") {
1121
+ if (mod.types.has(node.id.name) || mod.opaque.has(node.id.name)) {
1122
+ mod.types.delete(node.id.name);
1123
+ mod.opaque.add(node.id.name);
1124
+ } else {
1125
+ mod.types.set(node.id.name, node);
1126
+ }
1127
+ } else if (node.type === "ImportDeclaration" || raw.source && raw.specifiers) {
1128
+ for (const s of node.specifiers ?? []) {
1129
+ if (s.type !== "ImportSpecifier" && s.type !== "ExportSpecifier") {
1130
+ mod.opaque.add(s.local.name);
1131
+ continue;
1132
+ }
1133
+ const name2 = s.imported ?? s.local;
1134
+ const local = s.type === "ExportSpecifier" ? s.exported : s.local;
1135
+ mod.imports.set(local.name ?? local.value, {
1136
+ from: node.source.value,
1137
+ name: name2.name ?? name2.value
1138
+ });
1139
+ }
1140
+ } else if (raw.type === "ExportNamedDeclaration" && !raw.declaration) {
1141
+ for (const s of raw.specifiers) {
1142
+ mod.exports.set(s.exported.name ?? s.exported.value, s.local.name ?? s.local.value);
1143
+ }
1144
+ } else if (node.id?.name) {
1145
+ mod.opaque.add(node.id.name);
1146
+ }
1147
+ }
1148
+ const params = (node) => {
1149
+ if (Array.isArray(node)) return node.forEach(params);
1150
+ if (!node || typeof node !== "object") return;
1151
+ const n = node;
1152
+ if (n.type === "TSTypeParameter") mod.opaque.add(n.name.name);
1153
+ for (const key in n) params(n[key]);
1154
+ };
1155
+ params(body);
1156
+ for (const [, list = ""] of code.matchAll(/<script\b[^>]*\bgenerics?="([^"]*)"/g)) {
1157
+ for (const part of list.split(",")) mod.opaque.add(part.trim().split(/\s/)[0] ?? "");
1158
+ }
1159
+ return mod;
1160
+ }
1161
+ function rewrite(source, node, edits) {
1162
+ let out = source.slice(node.start, node.end);
1163
+ for (const [start, end, text] of [...edits].sort((a, b) => b[0] - a[0])) {
1164
+ out = out.slice(0, start - node.start) + text + out.slice(end - node.start);
1165
+ }
1166
+ return out;
1167
+ }
1168
+ function introduced(node) {
1169
+ const names = (node.typeParameters?.params ?? []).map((p) => p.name.name);
1170
+ if (node.type === "TSMappedType" && node.key?.name) names.push(node.key.name);
1171
+ if (node.type === "TSConditionalType") {
1172
+ const infer = (n) => {
1173
+ if (Array.isArray(n)) return n.forEach(infer);
1174
+ if (!n || typeof n !== "object") return;
1175
+ if (n.type === "TSInferType") names.push(n.typeParameter.name.name);
1176
+ for (const key in n) infer(n[key]);
1177
+ };
1178
+ infer(node.extendsType);
1179
+ }
1180
+ return names;
1181
+ }
1182
+ function tupleElement(node) {
1183
+ if (!node || node.type === "TSRestType") return void 0;
1184
+ if (node.type === "TSNamedTupleMember") {
1185
+ if (node.elementType?.type === "TSRestType") return void 0;
1186
+ const inner = node.elementType?.type === "TSOptionalType" ? node.elementType.typeAnnotation : node.elementType;
1187
+ return { type: inner, optional: Boolean(node.optional) || inner !== node.elementType };
1188
+ }
1189
+ if (node.type === "TSOptionalType") return { type: node.typeAnnotation, optional: true };
1190
+ return { type: node, optional: false };
1191
+ }
1192
+ var UTILITY_TYPES = /* @__PURE__ */ new Set([
1193
+ "Array",
1194
+ "ArrayLike",
1195
+ "AsyncIterable",
1196
+ "Awaited",
1197
+ "Capitalize",
1198
+ "ConstructorParameters",
1199
+ "Exclude",
1200
+ "Extract",
1201
+ "InstanceType",
1202
+ "Iterable",
1203
+ "Lowercase",
1204
+ "NoInfer",
1205
+ "NonNullable",
1206
+ "Omit",
1207
+ "Parameters",
1208
+ "Partial",
1209
+ "Pick",
1210
+ "PromiseLike",
1211
+ "PropertyKey",
1212
+ "Readonly",
1213
+ "ReadonlyArray",
1214
+ "ReadonlyMap",
1215
+ "ReadonlySet",
1216
+ "Record",
1217
+ "Required",
1218
+ "ReturnType",
1219
+ "Uncapitalize",
1220
+ "Uppercase"
1221
+ ]);
1222
+ var NODE_ONLY = /* @__PURE__ */ new Set(["Buffer", "process", "global", "require", "module", "exports"]);
1223
+ function isGlobal(name2) {
1224
+ if (UTILITY_TYPES.has(name2)) return true;
1225
+ if (NODE_ONLY.has(name2)) return false;
1226
+ if (/^(?:HTML|SVG)\w*Element$|^\w*Event(?:Init|Target)?$|^(?:Element|Node|Document|Window|ShadowRoot|DOMRect|FileList|DataTransfer|CSSStyleDeclaration|NodeList)$/.test(
1227
+ name2
1228
+ )) {
1229
+ return true;
1230
+ }
1231
+ return name2 in globalThis;
1232
+ }
1233
+ function unwrap(type) {
1234
+ if (type?.type === "TSParenthesizedType") return unwrap(type.typeAnnotation);
1235
+ if (type?.type !== "TSUnionType") return type;
1236
+ const real = type.types.filter(
1237
+ (t) => t.type !== "TSUndefinedKeyword" && t.type !== "TSNullKeyword"
1238
+ );
1239
+ return real.length === 1 ? unwrap(real[0]) : type;
1240
+ }
1241
+ var CANDIDATES = ["", ".ts", ".tsx", ".d.ts", "/index.ts", "/index.tsx"];
1242
+ function embedTypes(code, options) {
1243
+ const result = { props: {}, events: {}, declarations: [] };
1244
+ const named = /* @__PURE__ */ new Map();
1245
+ const taken = /* @__PURE__ */ new Set();
1246
+ const modules = /* @__PURE__ */ new Map();
1247
+ const load = (file) => {
1248
+ if (!modules.has(file)) {
1249
+ let mod = null;
1250
+ try {
1251
+ const text = options.load?.(file);
1252
+ if (text !== void 0) mod = parseModule(file, text);
1253
+ } catch {
1254
+ }
1255
+ modules.set(file, mod);
1256
+ }
1257
+ return modules.get(file) ?? null;
1258
+ };
1259
+ const resolve4 = (mod, name2, depth = 0) => {
1260
+ const decl = mod.types.get(name2);
1261
+ if (decl) return { decl, mod };
1262
+ if (mod.opaque.has(name2) || depth > 8) return null;
1263
+ const imp = mod.imports.get(name2);
1264
+ if (!imp) return isGlobal(name2) ? void 0 : null;
1265
+ if (!imp.from.startsWith(".")) return null;
1266
+ const base = resolvePath(dirname3(mod.file), imp.from.replace(/\.[cm]?js$/, ""));
1267
+ for (const ext of CANDIDATES) {
1268
+ const other = load(base + ext);
1269
+ if (other) return resolve4(other, other.exports.get(imp.name) ?? imp.name, depth + 1) ?? null;
1270
+ }
1271
+ return null;
1272
+ };
1273
+ const emit = (decl, mod) => {
1274
+ const existing = named.get(decl);
1275
+ if (existing) return existing;
1276
+ let as = `${options.scope}_${decl.id.name}`;
1277
+ for (let n = 2; taken.has(as); n++) as = `${options.scope}_${decl.id.name}${n}`;
1278
+ named.set(decl, as);
1279
+ taken.add(as);
1280
+ const edits = [[decl.id.start, decl.id.end, as]];
1281
+ if (!walk(decl, mod, /* @__PURE__ */ new Set(), edits)) return void 0;
1282
+ result.declarations.push(`export ${rewrite(mod.source, decl, edits)}`);
1283
+ return as;
1284
+ };
1285
+ const walk = (node, mod, scope, edits) => {
1286
+ if (Array.isArray(node)) return node.every((n2) => walk(n2, mod, scope, edits));
1287
+ if (!node || typeof node !== "object") return true;
1288
+ const n = node;
1289
+ if (n.type === "TSTypeQuery" || n.type === "TSImportType" || n.type === "TSQualifiedName" || n.type === "MemberExpression" || n.computed && n.key?.type !== "Literal") {
1290
+ return false;
1291
+ }
1292
+ const own = introduced(n);
1293
+ const inner = own.length ? /* @__PURE__ */ new Set([...scope, ...own]) : scope;
1294
+ const ref = n.type === "TSTypeReference" ? n.typeName : n.type === "TSInterfaceHeritage" ? n.expression : null;
1295
+ if (ref?.type === "Identifier" && !inner.has(ref.name)) {
1296
+ const target = resolve4(mod, ref.name);
1297
+ if (target === null) return false;
1298
+ if (target) {
1299
+ const as = emit(target.decl, target.mod);
1300
+ if (!as) return false;
1301
+ edits.push([ref.start, ref.end, as]);
1302
+ }
1303
+ }
1304
+ for (const key in n) if (!walk(n[key], mod, inner, edits)) return false;
1305
+ return true;
1306
+ };
1307
+ const typed = (node, mod) => {
1308
+ if (!node) return void 0;
1309
+ const mark = result.declarations.length;
1310
+ const before = new Map(named);
1311
+ const edits = [];
1312
+ if (walk(node, mod, /* @__PURE__ */ new Set(), edits)) return rewrite(mod.source, node, edits);
1313
+ result.declarations.length = mark;
1314
+ named.clear();
1315
+ taken.clear();
1316
+ for (const [decl, as] of before) {
1317
+ named.set(decl, as);
1318
+ taken.add(as);
1319
+ }
1320
+ return void 0;
1321
+ };
1322
+ const detail = (params, mod, skip = 0) => {
1323
+ const param = params?.[skip];
1324
+ if (!param || param.type === "RestElement") return void 0;
1325
+ const text = typed(param.typeAnnotation?.typeAnnotation, mod);
1326
+ return text && param.optional ? `${text} | null` : text;
1327
+ };
1328
+ try {
1329
+ const file = options.file ?? "component.tsx";
1330
+ const mod = parseModule(file, code);
1331
+ modules.set(file, mod);
1332
+ const aliases = localTypes(mod.body);
1333
+ const sfc = /\.(vue|svelte)$/.exec(file)?.[1];
1334
+ let declared;
1335
+ if (sfc === "vue") declared = macroCall(mod.body, "defineProps")?.typeArguments?.params?.[0];
1336
+ else if (sfc === "svelte") declared = svelteProps(mod.body)?.id?.typeAnnotation?.typeAnnotation;
1337
+ else {
1338
+ const declaration = findExport(mod.body, options.exportName ?? "default");
1339
+ declared = declaration ? propsType(declaration) : void 0;
1340
+ }
1341
+ for (const member2 of membersOf(declared, aliases) ?? []) {
1342
+ const name2 = keyOf(member2);
1343
+ if (!name2) continue;
1344
+ if (/^on[A-Z]/.test(name2)) {
1345
+ const fn = member2.type === "TSMethodSignature" ? member2 : unwrap(member2.typeAnnotation?.typeAnnotation);
1346
+ if (fn?.type !== "TSFunctionType" && fn?.type !== "TSMethodSignature") continue;
1347
+ if (fn.typeParameters) continue;
1348
+ const text2 = detail(fn.params, mod);
1349
+ if (text2) result.events[eventName(name2)] = text2;
1350
+ continue;
1351
+ }
1352
+ if (member2.type !== "TSPropertySignature") continue;
1353
+ const text = typed(member2.typeAnnotation?.typeAnnotation, mod);
1354
+ if (text) result.props[name2] = text;
1355
+ }
1356
+ if (sfc === "vue") {
1357
+ const emits = macroCall(mod.body, "defineEmits")?.typeArguments?.params?.[0];
1358
+ for (const member2 of membersOf(emits, aliases) ?? []) {
1359
+ if (member2.type === "TSPropertySignature") {
1360
+ const event = keyOf(member2);
1361
+ const tuple = member2.typeAnnotation?.typeAnnotation;
1362
+ const first = tuple?.type === "TSTupleType" ? tuple.elementTypes[0] : void 0;
1363
+ const element = tupleElement(first);
1364
+ const text = element && typed(element.type, mod);
1365
+ if (event && text) result.events[event] = element.optional ? `${text} | null` : text;
1366
+ }
1367
+ if (member2.type === "TSCallSignatureDeclaration") {
1368
+ const event = member2.params?.[0]?.typeAnnotation?.typeAnnotation?.literal?.value;
1369
+ const text = detail(member2.params, mod, 1);
1370
+ if (typeof event === "string" && text) result.events[event] = text;
1371
+ }
1372
+ }
1373
+ }
1374
+ } catch {
1375
+ }
1376
+ return result;
1377
+ }
1378
+
909
1379
  // src/embed.ts
910
1380
  function frameworkFor(file) {
911
1381
  if (file.endsWith(".vue")) return "vue";
@@ -955,18 +1425,25 @@ function normalize(options, root) {
955
1425
  return unique(entries);
956
1426
  }
957
1427
  function declarations(elements) {
958
- const blocks = elements.map(({ tag, props }) => {
1428
+ const blocks = elements.map(({ tag, props, types, docs }) => {
959
1429
  const id = name(tag);
960
- const members = props.filter((spec) => spec.kind !== "event").map((spec) => ` ${member(spec.name)}?: ${TS_TYPE[spec.kind] ?? "unknown"};`);
961
- const events = props.filter((spec) => spec.kind === "event").map((spec) => ` ${JSON.stringify(spec.event)}: CustomEvent;`);
1430
+ const members = props.filter((spec) => spec.kind !== "event").map(
1431
+ (spec) => `${jsdoc(docs.props[spec.name]?.description, " ")} ${member(spec.name)}?: ${types.props[spec.name] ?? TS_TYPE[spec.kind] ?? "unknown"};`
1432
+ );
1433
+ const events = props.filter((spec) => spec.kind === "event").map((spec) => {
1434
+ const detail = spec.event && types.events[spec.event];
1435
+ return `${jsdoc(docs.props[spec.name]?.description, " ")} ${JSON.stringify(spec.event)}: CustomEvent${detail ? `<${detail}>` : ""};`;
1436
+ });
962
1437
  const block = (kind, lines) => lines.length ? `export interface ${id}${kind} {
963
1438
  ${lines.join("\n")}
964
1439
  }` : `export interface ${id}${kind} {}`;
965
- return `${block("Props", members)}
1440
+ return `${types.declarations.map((text) => `${text}
1441
+
1442
+ `).join("")}${block("Props", members)}
966
1443
 
967
1444
  ${block("EventMap", events)}
968
1445
 
969
- export interface ${id}Element extends MountlyElement<${id}Props> {
1446
+ ${jsdoc(docs.description, "")}export interface ${id}Element extends MountlyElement<${id}Props> {
970
1447
  addEventListener<K extends keyof ${id}EventMap>(
971
1448
  type: K,
972
1449
  listener: (this: ${id}Element, event: ${id}EventMap[K]) => unknown,
@@ -1032,6 +1509,16 @@ ${jsx.join("\n")}
1032
1509
  }
1033
1510
  `;
1034
1511
  }
1512
+ function jsdoc(text, indent) {
1513
+ if (!text) return "";
1514
+ const lines = text.replace(/\*\//g, "*\\/").split("\n");
1515
+ if (lines.length === 1) return `${indent}/** ${lines[0]} */
1516
+ `;
1517
+ return `${indent}/**
1518
+ ${lines.map((line) => `${indent} *${line ? ` ${line}` : ""}`).join("\n")}
1519
+ ${indent} */
1520
+ `;
1521
+ }
1035
1522
  function member(prop) {
1036
1523
  return /^[A-Za-z_$][\w$]*$/.test(prop) ? prop : JSON.stringify(prop);
1037
1524
  }
@@ -1043,36 +1530,53 @@ function manifest(elements) {
1043
1530
  {
1044
1531
  schemaVersion: "1.0.0",
1045
1532
  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
- }))
1533
+ modules: elements.map(({ tag, entry, props, docs }) => {
1534
+ const cemType = (spec) => ({
1535
+ text: docs.props[spec.name]?.type ?? TS_TYPE[spec.kind] ?? "unknown"
1536
+ });
1537
+ const described = (spec) => {
1538
+ const { description, schema } = docs.props[spec.name] ?? {};
1539
+ return { ...description ? { description } : {}, ...schema ? { schema } : {} };
1540
+ };
1541
+ return {
1542
+ kind: "javascript-module",
1543
+ path: entry.component,
1544
+ declarations: [
1545
+ {
1546
+ kind: "class",
1547
+ name: name(tag),
1548
+ ...docs.description ? { description: docs.description } : {},
1549
+ ...docs.slots ? { slots: docs.slots } : {},
1550
+ customElement: true,
1551
+ tagName: tag,
1552
+ members: props.filter((spec) => spec.kind !== "event").map((spec) => ({
1553
+ kind: "field",
1554
+ name: spec.name,
1555
+ type: cemType(spec),
1556
+ ...described(spec)
1557
+ })),
1558
+ attributes: props.filter((spec) => spec.attribute).map((spec) => ({
1559
+ name: spec.attribute,
1560
+ fieldName: spec.name,
1561
+ type: cemType(spec),
1562
+ ...described(spec)
1563
+ })),
1564
+ events: props.filter((spec) => spec.kind === "event").map((spec) => ({
1565
+ name: spec.event,
1566
+ type: { text: "CustomEvent" },
1567
+ ...described(spec)
1568
+ }))
1569
+ }
1570
+ ],
1571
+ exports: [
1572
+ {
1573
+ kind: "custom-element-definition",
1574
+ name: tag,
1575
+ declaration: { name: name(tag), module: entry.component }
1576
+ }
1577
+ ]
1578
+ };
1579
+ })
1076
1580
  },
1077
1581
  null,
1078
1582
  2
@@ -1126,7 +1630,7 @@ function resolveCssToken(bundle) {
1126
1630
  output.code = output.code.replace(token, "void 0");
1127
1631
  continue;
1128
1632
  }
1129
- const href = relative3(dirname3(output.fileName), stylesheet).replace(/\\/g, "/");
1633
+ const href = relative3(dirname4(output.fileName), stylesheet).replace(/\\/g, "/");
1130
1634
  output.code = output.code.replace(
1131
1635
  token,
1132
1636
  `new URL(${JSON.stringify(`./${href}`)}, import.meta.url).href`
@@ -1234,7 +1738,7 @@ function shimWorkers(code) {
1234
1738
  walk(child, node, outside ? shadowed : hidden);
1235
1739
  }
1236
1740
  };
1237
- walk(parseAst2(code, { lang: "js" }), void 0, false);
1741
+ walk(parseAst4(code, { lang: "js" }), void 0, false);
1238
1742
  if (!callees.length) return null;
1239
1743
  let out = code;
1240
1744
  for (const { start, end } of callees.sort((a, b) => b.start - a.start)) {
@@ -1242,18 +1746,61 @@ function shimWorkers(code) {
1242
1746
  }
1243
1747
  return WORKER_SHIM + out;
1244
1748
  }
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
- });
1749
+ var NATIVE_BUBBLING = /* @__PURE__ */ new Set([
1750
+ "click",
1751
+ "dblclick",
1752
+ "input",
1753
+ "change",
1754
+ "submit",
1755
+ "reset",
1756
+ "select",
1757
+ "keydown",
1758
+ "keyup",
1759
+ "focusin",
1760
+ "focusout",
1761
+ "pointerdown",
1762
+ "pointerup",
1763
+ "mousedown",
1764
+ "mouseup",
1765
+ "contextmenu",
1766
+ "wheel",
1767
+ "beforeinput",
1768
+ "invalid"
1769
+ ]);
1770
+ function warnNativeEvents(elements, warn) {
1771
+ for (const { tag, props } of elements) {
1772
+ for (const spec of props) {
1773
+ if (spec.kind === "event" && spec.event && NATIVE_BUBBLING.has(spec.event)) {
1774
+ warn(
1775
+ `[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.`
1776
+ );
1777
+ }
1778
+ }
1779
+ }
1780
+ }
1781
+ function readElementSource(entry, tag, root) {
1782
+ const source = readFileSync2(resolve3(root, entry.component), "utf8");
1783
+ const opts = { file: entry.component, exportName: entry.exportName };
1784
+ const props = entry.props ?? extractProps(source, opts);
1251
1785
  if (!props) {
1252
1786
  throw new Error(
1253
1787
  `[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
1788
  );
1255
1789
  }
1256
- return props;
1790
+ const docs = extractDocs(source, opts);
1791
+ const types = entry.props ? { props: {}, events: {}, declarations: [] } : embedTypes(source, {
1792
+ ...opts,
1793
+ file: resolve3(root, entry.component),
1794
+ scope: name(tag),
1795
+ load: (file) => {
1796
+ try {
1797
+ return readFileSync2(file, "utf8");
1798
+ } catch {
1799
+ return void 0;
1800
+ }
1801
+ }
1802
+ });
1803
+ return { props, docs, types };
1257
1804
  }
1258
1805
  function defineElementsConfig(options) {
1259
1806
  const entryId = "virtual:mountly-embed";
@@ -1269,12 +1816,26 @@ function defineElementsConfig(options) {
1269
1816
  configResolved(config) {
1270
1817
  root = config.root;
1271
1818
  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
- }));
1819
+ elements = normalize(options, root).map(([tag, entry]) => {
1820
+ const { props, docs, types } = readElementSource(entry, tag, root);
1821
+ return {
1822
+ tag,
1823
+ entry,
1824
+ props,
1825
+ framework: entry.framework ?? frameworkFor(entry.component),
1826
+ docs,
1827
+ types
1828
+ };
1829
+ });
1830
+ const warn = config.logger?.warn.bind(config.logger) ?? console.warn;
1831
+ warnNativeEvents(elements, warn);
1832
+ if (!shadow) {
1833
+ for (const { tag } of elements.filter((element) => element.docs.slots)) {
1834
+ warn(
1835
+ `[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.`
1836
+ );
1837
+ }
1838
+ }
1278
1839
  },
1279
1840
  resolveId(id) {
1280
1841
  if (id === entryId || id.startsWith(widgetPrefix)) return `\0${id}`;
@@ -1282,10 +1843,11 @@ function defineElementsConfig(options) {
1282
1843
  load(id) {
1283
1844
  if (id === `\0${entryId}`) {
1284
1845
  const definitions = elements.map(
1285
- ({ tag, entry, props }) => `${JSON.stringify(tag)}: {
1846
+ ({ tag, entry, props, docs }) => `${JSON.stringify(tag)}: {
1286
1847
  load: () => import(${JSON.stringify(widgetPrefix + tag)}),
1287
1848
  props: ${JSON.stringify(props)},
1288
- trigger: ${JSON.stringify(entry.trigger ?? "connected")}
1849
+ trigger: ${JSON.stringify(entry.trigger ?? "connected")}${docs.slots ? `,
1850
+ slots: ${JSON.stringify(docs.slots.map((slot) => slot.name))}` : ""}
1289
1851
  }`
1290
1852
  );
1291
1853
  return `import { defineElements } from "mountly/embed";
@@ -1358,6 +1920,18 @@ export default createWidget(component[${JSON.stringify(element.entry.exportName
1358
1920
  // stylesheet nothing injects, which each element adopts for itself.
1359
1921
  cssCodeSplit: !shadow,
1360
1922
  rollupOptions: {
1923
+ ...options.peer ? {
1924
+ external: [
1925
+ ...new Set(
1926
+ [...frameworks].flatMap((framework) => [
1927
+ ...getFrameworkPeerExternals(framework),
1928
+ // Compiled Svelte imports `svelte/internal/client`; bundling it
1929
+ // beside an external `svelte` would mean two runtimes.
1930
+ ...framework === "svelte" ? [/^svelte\//] : []
1931
+ ])
1932
+ )
1933
+ ]
1934
+ } : {},
1361
1935
  input: entryId,
1362
1936
  output: { entryFileNames: "embed.js", chunkFileNames: "assets/[name]-[hash].js" }
1363
1937
  }
@@ -1366,23 +1940,6 @@ export default createWidget(component[${JSON.stringify(element.entry.exportName
1366
1940
  }
1367
1941
 
1368
1942
  // 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
1943
  function matchesFilter(id, include) {
1387
1944
  return include.some(
1388
1945
  (pattern) => typeof pattern === "string" ? id.includes(pattern) : pattern.test(id)
@@ -1556,6 +2113,7 @@ function defineMountlyWidgetConfig(options = {}) {
1556
2113
  export {
1557
2114
  defineElementsConfig,
1558
2115
  defineMountlyWidgetConfig,
2116
+ extractDocs,
1559
2117
  extractProps,
1560
2118
  getFrameworkPeerExternals,
1561
2119
  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;
@@ -0,0 +1,16 @@
1
+ import { type ExtractOptions } from "./props.js";
2
+ export interface EmbedTypes {
3
+ /** Per prop, its type as written, with copied names renamed. */
4
+ props: Record<string, string>;
5
+ /** Per DOM event, the type of its `detail`: the callback's first argument. */
6
+ events: Record<string, string>;
7
+ /** The declarations those types name, to emit beside them. */
8
+ declarations: string[];
9
+ }
10
+ export interface EmbedTypesOptions extends ExtractOptions {
11
+ /** Prefix for copied declarations, so two elements' `Line` types cannot collide. */
12
+ scope: string;
13
+ /** Read a file an import points at. Absent or `undefined`: imports stay opaque. */
14
+ load?: (file: string) => string | undefined;
15
+ }
16
+ export declare function embedTypes(code: string, options: EmbedTypesOptions): EmbedTypes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mountly-vite-plugin",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Vite lib build plugin for mountly vertical widgets (dual ESM output)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {