mountly-vite-plugin 1.6.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/dist/index.js CHANGED
@@ -613,8 +613,8 @@ function getSelfContainedExternals(framework) {
613
613
 
614
614
  // src/embed.ts
615
615
  import { globSync, readFileSync as readFileSync2 } from "fs";
616
- import { basename, dirname as dirname3, extname, relative as relative3, resolve as resolve3 } from "path";
617
- import { parseAst as parseAst3 } from "vite";
616
+ import { basename, dirname as dirname4, extname, relative as relative3, resolve as resolve3 } from "path";
617
+ import { parseAst as parseAst4 } from "vite";
618
618
 
619
619
  // src/docs.ts
620
620
  import { parseAst as parseAst2 } from "vite";
@@ -1099,6 +1099,283 @@ function extractDocs(code, options = {}) {
1099
1099
  }
1100
1100
  }
1101
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
+
1102
1379
  // src/embed.ts
1103
1380
  function frameworkFor(file) {
1104
1381
  if (file.endsWith(".vue")) return "vue";
@@ -1148,18 +1425,25 @@ function normalize(options, root) {
1148
1425
  return unique(entries);
1149
1426
  }
1150
1427
  function declarations(elements) {
1151
- const blocks = elements.map(({ tag, props }) => {
1428
+ const blocks = elements.map(({ tag, props, types, docs }) => {
1152
1429
  const id = name(tag);
1153
- const members = props.filter((spec) => spec.kind !== "event").map((spec) => ` ${member(spec.name)}?: ${TS_TYPE[spec.kind] ?? "unknown"};`);
1154
- 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
+ });
1155
1437
  const block = (kind, lines) => lines.length ? `export interface ${id}${kind} {
1156
1438
  ${lines.join("\n")}
1157
1439
  }` : `export interface ${id}${kind} {}`;
1158
- return `${block("Props", members)}
1440
+ return `${types.declarations.map((text) => `${text}
1441
+
1442
+ `).join("")}${block("Props", members)}
1159
1443
 
1160
1444
  ${block("EventMap", events)}
1161
1445
 
1162
- export interface ${id}Element extends MountlyElement<${id}Props> {
1446
+ ${jsdoc(docs.description, "")}export interface ${id}Element extends MountlyElement<${id}Props> {
1163
1447
  addEventListener<K extends keyof ${id}EventMap>(
1164
1448
  type: K,
1165
1449
  listener: (this: ${id}Element, event: ${id}EventMap[K]) => unknown,
@@ -1225,6 +1509,16 @@ ${jsx.join("\n")}
1225
1509
  }
1226
1510
  `;
1227
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
+ }
1228
1522
  function member(prop) {
1229
1523
  return /^[A-Za-z_$][\w$]*$/.test(prop) ? prop : JSON.stringify(prop);
1230
1524
  }
@@ -1336,7 +1630,7 @@ function resolveCssToken(bundle) {
1336
1630
  output.code = output.code.replace(token, "void 0");
1337
1631
  continue;
1338
1632
  }
1339
- const href = relative3(dirname3(output.fileName), stylesheet).replace(/\\/g, "/");
1633
+ const href = relative3(dirname4(output.fileName), stylesheet).replace(/\\/g, "/");
1340
1634
  output.code = output.code.replace(
1341
1635
  token,
1342
1636
  `new URL(${JSON.stringify(`./${href}`)}, import.meta.url).href`
@@ -1444,7 +1738,7 @@ function shimWorkers(code) {
1444
1738
  walk(child, node, outside ? shadowed : hidden);
1445
1739
  }
1446
1740
  };
1447
- walk(parseAst3(code, { lang: "js" }), void 0, false);
1741
+ walk(parseAst4(code, { lang: "js" }), void 0, false);
1448
1742
  if (!callees.length) return null;
1449
1743
  let out = code;
1450
1744
  for (const { start, end } of callees.sort((a, b) => b.start - a.start)) {
@@ -1493,7 +1787,20 @@ function readElementSource(entry, tag, root) {
1493
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.`
1494
1788
  );
1495
1789
  }
1496
- return { props, docs: extractDocs(source, opts) };
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 };
1497
1804
  }
1498
1805
  function defineElementsConfig(options) {
1499
1806
  const entryId = "virtual:mountly-embed";
@@ -1510,13 +1817,14 @@ function defineElementsConfig(options) {
1510
1817
  root = config.root;
1511
1818
  assertSingleCompiler(config.plugins);
1512
1819
  elements = normalize(options, root).map(([tag, entry]) => {
1513
- const { props, docs } = readElementSource(entry, tag, root);
1820
+ const { props, docs, types } = readElementSource(entry, tag, root);
1514
1821
  return {
1515
1822
  tag,
1516
1823
  entry,
1517
1824
  props,
1518
1825
  framework: entry.framework ?? frameworkFor(entry.component),
1519
- docs
1826
+ docs,
1827
+ types
1520
1828
  };
1521
1829
  });
1522
1830
  const warn = config.logger?.warn.bind(config.logger) ?? console.warn;
@@ -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.6.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": {