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 +13 -0
- package/dist/docs.d.ts +31 -0
- package/dist/embed.d.ts +12 -1
- package/dist/externals.d.ts +11 -0
- package/dist/index.d.ts +3 -10
- package/dist/index.js +313 -63
- package/dist/props.d.ts +20 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
5
|
-
export
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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(
|
|
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
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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;
|