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 +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 +628 -70
- package/dist/props.d.ts +20 -0
- package/dist/types.d.ts +16 -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
|
-
import { basename, dirname as
|
|
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(
|
|
961
|
-
|
|
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 `${
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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;
|
package/dist/types.d.ts
ADDED
|
@@ -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;
|