reactolith 1.0.19

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.
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var tsMorph = require('ts-morph');
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+
8
+ function generateWebTypes(options) {
9
+ const project = new tsMorph.Project({
10
+ tsConfigFilePath: options.tsconfig || "./tsconfig.json",
11
+ });
12
+ const componentsDir = path.resolve(options.componentsDir || "components/ui");
13
+ const files = fs
14
+ .readdirSync(componentsDir)
15
+ .filter((f) => f.endsWith(".tsx") || f.endsWith(".ts"));
16
+ const elements = [];
17
+ const prefix = options.prefix || "";
18
+ files.forEach((file) => {
19
+ const filePath = path.join(componentsDir, file);
20
+ const sourceFile = project.addSourceFileAtPath(filePath);
21
+ if (!sourceFile)
22
+ return;
23
+ // Strategy 1: Look for exported *Props types (existing behavior)
24
+ const propsFromTypes = extractFromExportedPropsTypes(sourceFile);
25
+ // Strategy 2: Look for exported React components and extract their props
26
+ const propsFromComponents = extractFromComponentFunctions(sourceFile);
27
+ // Merge results, preferring explicit Props types
28
+ const componentMap = new Map();
29
+ propsFromComponents.forEach((info) => {
30
+ componentMap.set(info.name, info);
31
+ });
32
+ propsFromTypes.forEach((info) => {
33
+ componentMap.set(info.name, info);
34
+ });
35
+ componentMap.forEach((info) => {
36
+ if (info.propsType === undefined)
37
+ return;
38
+ const { attributes, slots } = extractAttributesAndSlots(info.propsType, info.propsNode || info.sourceFile);
39
+ const tagName = prefix +
40
+ info.name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
41
+ const element = {
42
+ name: tagName,
43
+ description: `${info.name} component`,
44
+ attributes,
45
+ };
46
+ // Only add slots if there are any
47
+ if (slots.length > 0) {
48
+ element.slots = slots;
49
+ }
50
+ elements.push(element);
51
+ });
52
+ });
53
+ elements.sort((a, b) => a.name.localeCompare(b.name));
54
+ const webTypes = {
55
+ $schema: "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
56
+ name: options.libraryName || "reactolith-components",
57
+ version: options.libraryVersion || "1.0.0",
58
+ "js-types-syntax": "typescript",
59
+ "description-markup": "markdown",
60
+ contributions: {
61
+ html: {
62
+ elements,
63
+ },
64
+ },
65
+ };
66
+ const outFile = options.outFile || "web-types.json";
67
+ fs.writeFileSync(outFile, JSON.stringify(webTypes, null, 2));
68
+ }
69
+ /**
70
+ * Strategy 1: Extract from exported types ending with "Props"
71
+ */
72
+ function extractFromExportedPropsTypes(sourceFile) {
73
+ const results = [];
74
+ const exported = sourceFile.getExportedDeclarations();
75
+ for (const [name, decls] of exported) {
76
+ if (!name.endsWith("Props"))
77
+ continue;
78
+ const decl = decls[0];
79
+ if (!decl)
80
+ continue;
81
+ const type = decl.getType?.();
82
+ if (!type)
83
+ continue;
84
+ const componentName = name.substring(0, name.length - 5);
85
+ results.push({
86
+ name: componentName,
87
+ propsType: type,
88
+ propsNode: decl,
89
+ sourceFile,
90
+ });
91
+ }
92
+ return results;
93
+ }
94
+ /**
95
+ * Strategy 2: Extract props from exported React component functions
96
+ */
97
+ function extractFromComponentFunctions(sourceFile) {
98
+ const results = [];
99
+ const exported = sourceFile.getExportedDeclarations();
100
+ for (const [name, decls] of exported) {
101
+ // Skip Props types (handled by Strategy 1)
102
+ if (name.endsWith("Props"))
103
+ continue;
104
+ // Skip non-PascalCase names (not React components)
105
+ if (!/^[A-Z]/.test(name))
106
+ continue;
107
+ const decl = decls[0];
108
+ if (!decl)
109
+ continue;
110
+ const propsInfo = extractPropsFromDeclaration(decl);
111
+ if (propsInfo) {
112
+ results.push({
113
+ name,
114
+ propsType: propsInfo.type,
115
+ propsNode: propsInfo.node,
116
+ sourceFile,
117
+ });
118
+ }
119
+ }
120
+ // Also check for default export
121
+ const defaultExport = sourceFile.getDefaultExportSymbol();
122
+ if (defaultExport) {
123
+ const decls = defaultExport.getDeclarations();
124
+ for (const decl of decls) {
125
+ let propsInfo = extractPropsFromDeclaration(decl);
126
+ // If no props found directly, check if it's an ExportAssignment with an Identifier
127
+ if (!propsInfo && tsMorph.Node.isExportAssignment(decl)) {
128
+ const expr = decl.getExpression?.();
129
+ if (expr && tsMorph.Node.isIdentifier(expr)) {
130
+ // Try to resolve the identifier to its declaration
131
+ propsInfo = resolveIdentifierToProps(expr, sourceFile);
132
+ }
133
+ }
134
+ if (propsInfo) {
135
+ // Use filename as component name for default exports, converting kebab-case to PascalCase
136
+ const fileName = path.basename(sourceFile.getFilePath(), path.extname(sourceFile.getFilePath()));
137
+ const componentName = fileName
138
+ .split("-")
139
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
140
+ .join("");
141
+ results.push({
142
+ name: componentName,
143
+ propsType: propsInfo.type,
144
+ propsNode: propsInfo.node,
145
+ sourceFile,
146
+ });
147
+ }
148
+ }
149
+ }
150
+ return results;
151
+ }
152
+ /**
153
+ * Resolve an identifier to its props - handles local variables and imports
154
+ */
155
+ function resolveIdentifierToProps(identifier, sourceFile) {
156
+ const identifierName = identifier.getText();
157
+ // First, check if it's a local variable declaration
158
+ const localVar = sourceFile.getVariableDeclaration(identifierName);
159
+ if (localVar) {
160
+ return extractPropsFromDeclaration(localVar);
161
+ }
162
+ // Check if it's a local function declaration
163
+ const localFunc = sourceFile.getFunction(identifierName);
164
+ if (localFunc) {
165
+ return extractPropsFromDeclaration(localFunc);
166
+ }
167
+ // Check if it's an imported symbol - try to get props directly from the type
168
+ const identifierType = identifier.getType();
169
+ const callSignatures = identifierType.getCallSignatures();
170
+ if (callSignatures.length > 0) {
171
+ const sig = callSignatures[0];
172
+ const params = sig.getParameters();
173
+ if (params.length > 0) {
174
+ const firstParam = params[0];
175
+ const paramType = firstParam.getTypeAtLocation(identifier);
176
+ const returnType = sig.getReturnType();
177
+ if (isJsxReturnType(returnType)) {
178
+ return { type: paramType, node: identifier };
179
+ }
180
+ }
181
+ else {
182
+ // No params but returns JSX - component without props
183
+ const returnType = sig.getReturnType();
184
+ if (isJsxReturnType(returnType)) {
185
+ return { type: null, node: identifier };
186
+ }
187
+ }
188
+ }
189
+ // Fallback: try to resolve from the imported module source file
190
+ const importDecls = sourceFile.getImportDeclarations();
191
+ for (const importDecl of importDecls) {
192
+ // Check named imports
193
+ const namedImports = importDecl.getNamedImports();
194
+ for (const namedImport of namedImports) {
195
+ const importedName = namedImport.getAliasNode()?.getText() || namedImport.getName();
196
+ if (importedName === identifierName) {
197
+ // Found the import - resolve from the imported module
198
+ return resolveImportedComponent(importDecl, namedImport.getName());
199
+ }
200
+ }
201
+ // Check default import
202
+ const defaultImport = importDecl.getDefaultImport();
203
+ if (defaultImport && defaultImport.getText() === identifierName) {
204
+ return resolveImportedComponent(importDecl, "default");
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+ /**
210
+ * Resolve props from an imported component
211
+ */
212
+ function resolveImportedComponent(importDecl, exportName, _currentSourceFile) {
213
+ try {
214
+ const resolvedModule = importDecl.getModuleSpecifierSourceFile();
215
+ if (!resolvedModule) {
216
+ return null;
217
+ }
218
+ // Get the exported declaration from the module
219
+ const exported = resolvedModule.getExportedDeclarations();
220
+ if (exportName === "default") {
221
+ const defaultExport = resolvedModule.getDefaultExportSymbol();
222
+ if (defaultExport) {
223
+ const decls = defaultExport.getDeclarations();
224
+ for (const decl of decls) {
225
+ const propsInfo = extractPropsFromDeclaration(decl);
226
+ if (propsInfo)
227
+ return propsInfo;
228
+ }
229
+ }
230
+ }
231
+ else {
232
+ const decls = exported.get(exportName);
233
+ if (decls && decls.length > 0) {
234
+ for (const decl of decls) {
235
+ const propsInfo = extractPropsFromDeclaration(decl);
236
+ if (propsInfo)
237
+ return propsInfo;
238
+ }
239
+ }
240
+ }
241
+ }
242
+ catch {
243
+ // Module resolution failed - this is okay for external modules
244
+ }
245
+ return null;
246
+ }
247
+ /**
248
+ * Extract props type from a function/variable declaration
249
+ */
250
+ function extractPropsFromDeclaration(decl) {
251
+ // Handle function declarations: export function Button(props: ButtonProps) {}
252
+ if (tsMorph.Node.isFunctionDeclaration(decl)) {
253
+ return extractPropsFromFunction(decl);
254
+ }
255
+ // Handle variable declarations: export const Button = (props: ButtonProps) => {}
256
+ if (tsMorph.Node.isVariableDeclaration(decl)) {
257
+ const varDecl = decl;
258
+ const initializer = varDecl.getInitializer();
259
+ if (initializer && tsMorph.Node.isArrowFunction(initializer)) {
260
+ return extractPropsFromArrowFunction(initializer);
261
+ }
262
+ if (initializer && tsMorph.Node.isFunctionExpression(initializer)) {
263
+ return extractPropsFromFunctionExpression(initializer);
264
+ }
265
+ // Handle React.forwardRef, React.memo, etc.
266
+ if (initializer && tsMorph.Node.isCallExpression(initializer)) {
267
+ const args = initializer.getArguments();
268
+ for (const arg of args) {
269
+ if (tsMorph.Node.isArrowFunction(arg)) {
270
+ return extractPropsFromArrowFunction(arg);
271
+ }
272
+ if (tsMorph.Node.isFunctionExpression(arg)) {
273
+ return extractPropsFromFunctionExpression(arg);
274
+ }
275
+ }
276
+ }
277
+ // Handle property access expressions: const Select = SelectPrimitive.Root
278
+ if (initializer && (tsMorph.Node.isPropertyAccessExpression(initializer) || tsMorph.Node.isIdentifier(initializer))) {
279
+ // Try to get the type from the variable declaration
280
+ const varType = varDecl.getType();
281
+ // Check if this is a React component type (has Props in the call signature)
282
+ const callSignatures = varType.getCallSignatures();
283
+ if (callSignatures.length > 0) {
284
+ const sig = callSignatures[0];
285
+ const params = sig.getParameters();
286
+ if (params.length > 0) {
287
+ const firstParam = params[0];
288
+ const paramType = firstParam.getTypeAtLocation(varDecl);
289
+ const returnType = sig.getReturnType();
290
+ if (isJsxReturnType(returnType)) {
291
+ return { type: paramType, node: varDecl };
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+ // Handle export default function() {}
298
+ if (tsMorph.Node.isExportAssignment(decl)) {
299
+ const expr = decl.getExpression?.();
300
+ if (expr) {
301
+ if (tsMorph.Node.isArrowFunction(expr)) {
302
+ return extractPropsFromArrowFunction(expr);
303
+ }
304
+ if (tsMorph.Node.isFunctionExpression(expr)) {
305
+ return extractPropsFromFunctionExpression(expr);
306
+ }
307
+ }
308
+ }
309
+ return null;
310
+ }
311
+ function extractPropsFromFunction(func) {
312
+ // Check if this looks like a React component (returns JSX)
313
+ const returnType = func.getReturnType();
314
+ if (!isJsxReturnType(returnType))
315
+ return null;
316
+ const params = func.getParameters();
317
+ if (params.length === 0) {
318
+ // No params - return empty props type
319
+ return { type: null, node: func };
320
+ }
321
+ const firstParam = params[0];
322
+ const type = firstParam.getType();
323
+ return { type, node: firstParam };
324
+ }
325
+ function extractPropsFromArrowFunction(func) {
326
+ // Check if this looks like a React component (returns JSX)
327
+ const returnType = func.getReturnType();
328
+ if (!isJsxReturnType(returnType))
329
+ return null;
330
+ const params = func.getParameters();
331
+ if (params.length === 0) {
332
+ // No params - return empty props type
333
+ return { type: null, node: func };
334
+ }
335
+ const firstParam = params[0];
336
+ const type = firstParam.getType();
337
+ return { type, node: firstParam };
338
+ }
339
+ function extractPropsFromFunctionExpression(func) {
340
+ // Check if this looks like a React component (returns JSX)
341
+ const returnType = func.getReturnType();
342
+ if (!isJsxReturnType(returnType))
343
+ return null;
344
+ const params = func.getParameters();
345
+ if (params.length === 0) {
346
+ // No params - return empty props type
347
+ return { type: null, node: func };
348
+ }
349
+ const firstParam = params[0];
350
+ const type = firstParam.getType();
351
+ return { type, node: firstParam };
352
+ }
353
+ /**
354
+ * Check if a return type looks like JSX (React.ReactElement, JSX.Element, etc.)
355
+ */
356
+ function isJsxReturnType(type) {
357
+ const text = type.getText();
358
+ return (text.includes("Element") ||
359
+ text.includes("ReactNode") ||
360
+ text.includes("ReactElement") ||
361
+ text.includes("JSX") ||
362
+ text === "null" ||
363
+ text.includes("| null"));
364
+ }
365
+ /**
366
+ * Check if a type represents a React slot (ReactNode, ReactElement, etc.)
367
+ */
368
+ function isSlotType(typeText) {
369
+ // Exact patterns that indicate a slot type
370
+ const slotPatterns = [
371
+ "ReactNode",
372
+ "ReactElement",
373
+ "JSX.Element",
374
+ ];
375
+ // Check if type is a slot type (but not a function returning ReactNode or event handler)
376
+ const isSlot = slotPatterns.some((pattern) => typeText.includes(pattern));
377
+ // Exclude event handlers and functions
378
+ const isFunction = typeText.includes("=>") ||
379
+ typeText.includes("EventHandler") ||
380
+ typeText.includes("Handler<");
381
+ return isSlot && !isFunction;
382
+ }
383
+ /**
384
+ * Extract attributes and slots from a Type
385
+ */
386
+ function extractAttributesAndSlots(type, contextNode) {
387
+ const attributes = [];
388
+ const slots = [];
389
+ // Handle null type (components with no props)
390
+ if (!type) {
391
+ return { attributes, slots };
392
+ }
393
+ type.getProperties().forEach((prop) => {
394
+ const propName = prop.getName();
395
+ // Skip internal React props
396
+ if (["key", "ref"].includes(propName))
397
+ return;
398
+ const propType = prop.getTypeAtLocation(contextNode);
399
+ const typeText = cleanTypeText(propType.getText());
400
+ const description = getPropertyDescription(prop);
401
+ const required = !prop.isOptional();
402
+ // Check if this prop is a slot (ReactNode type)
403
+ if (isSlotType(typeText)) {
404
+ // "children" becomes the "default" slot
405
+ const slotName = propName === "children" ? "default" : propName;
406
+ slots.push({
407
+ name: slotName,
408
+ description: description || `Content for the ${slotName} slot`,
409
+ });
410
+ return;
411
+ }
412
+ // Skip children if it's not a ReactNode (e.g., string children)
413
+ if (propName === "children")
414
+ return;
415
+ // Regular attribute
416
+ const attr = {
417
+ name: toKebabCase(propName),
418
+ description: description || undefined,
419
+ required,
420
+ };
421
+ // Check for boolean types first (including optional booleans)
422
+ if (typeText === "boolean" || typeText === "boolean | undefined") {
423
+ // Boolean attributes can be used without value
424
+ attr.value = {
425
+ kind: "no-value",
426
+ type: "boolean",
427
+ };
428
+ }
429
+ else if (typeText.includes("|")) {
430
+ // Parse union types for enum-like values
431
+ const values = typeText
432
+ .split("|")
433
+ .map((v) => v.trim())
434
+ .filter((v) => v !== "undefined" && v !== "null");
435
+ // Check if all values are string literals (quoted strings only)
436
+ // Exclude primitive types like boolean, string, number, etc.
437
+ const primitiveTypes = [
438
+ "boolean",
439
+ "string",
440
+ "number",
441
+ "object",
442
+ "any",
443
+ "unknown",
444
+ "never",
445
+ ];
446
+ const stringLiteralValues = values.filter((v) => /^["'].*["']$/.test(v));
447
+ const hasOnlyStringLiterals = stringLiteralValues.length === values.length && values.length > 0;
448
+ const hasOnlyPrimitives = values.every((v) => primitiveTypes.includes(v));
449
+ if (hasOnlyStringLiterals) {
450
+ attr.value = {
451
+ kind: "plain",
452
+ type: typeText,
453
+ };
454
+ // Add enum values for better autocomplete
455
+ attr.values = stringLiteralValues
456
+ .map((v) => v.replace(/['"]/g, ""))
457
+ .map((v) => ({ name: v }));
458
+ }
459
+ else if (hasOnlyPrimitives ||
460
+ values.some((v) => v.includes("=>"))) {
461
+ // Function types or primitive unions
462
+ attr.value = {
463
+ kind: "expression",
464
+ type: typeText,
465
+ };
466
+ }
467
+ else {
468
+ attr.value = {
469
+ kind: "plain",
470
+ type: typeText,
471
+ };
472
+ }
473
+ }
474
+ else {
475
+ attr.value = {
476
+ kind: "plain",
477
+ type: typeText,
478
+ };
479
+ }
480
+ attributes.push(attr);
481
+ });
482
+ return { attributes, slots };
483
+ }
484
+ /**
485
+ * Clean up type text for display
486
+ */
487
+ function cleanTypeText(text) {
488
+ // Remove import(...) paths
489
+ return text
490
+ .replace(/import\([^)]+\)\./g, "")
491
+ .replace(/\s+/g, " ")
492
+ .trim();
493
+ }
494
+ /**
495
+ * Convert camelCase to kebab-case
496
+ */
497
+ function toKebabCase(str) {
498
+ return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
499
+ }
500
+ /**
501
+ * Try to extract JSDoc description from a property
502
+ */
503
+ function getPropertyDescription(prop) {
504
+ const declarations = prop.getDeclarations();
505
+ for (const decl of declarations) {
506
+ const jsDocs = decl.getJsDocs?.();
507
+ if (jsDocs && jsDocs.length > 0) {
508
+ return jsDocs[0].getDescription?.()?.trim();
509
+ }
510
+ }
511
+ return undefined;
512
+ }
513
+
514
+ /**
515
+ * Detect the default tsconfig file.
516
+ * Prefers tsconfig.app.json (common in Vite/modern setups) over tsconfig.json
517
+ */
518
+ function detectDefaultTsconfig() {
519
+ if (fs.existsSync("./tsconfig.app.json")) {
520
+ return "./tsconfig.app.json";
521
+ }
522
+ return "./tsconfig.json";
523
+ }
524
+ function printHelp() {
525
+ console.log(`
526
+ Usage: generate-web-types [options]
527
+
528
+ Options:
529
+ --components, -c <dir> Components directory (default: components/ui)
530
+ --tsconfig, -t <file> TypeScript config file (default: tsconfig.app.json if exists, else tsconfig.json)
531
+ --out, -o <file> Output file (default: web-types.json)
532
+ --name, -n <name> Library name (default: reactolith-components)
533
+ --version, -v <version> Library version (default: 1.0.0)
534
+ --prefix, -p <prefix> Element name prefix (default: "")
535
+ --help, -h Show this help message
536
+
537
+ Examples:
538
+ generate-web-types -c src/components -o web-types.json
539
+ generate-web-types --components ./ui --prefix ui- --name my-ui-lib
540
+ `);
541
+ }
542
+ function parseArgs(args) {
543
+ const result = {};
544
+ let i = 0;
545
+ while (i < args.length) {
546
+ const arg = args[i];
547
+ if (arg === "--help" || arg === "-h") {
548
+ printHelp();
549
+ process.exit(0);
550
+ }
551
+ if (arg.startsWith("-")) {
552
+ const key = arg.replace(/^-+/, "");
553
+ const value = args[i + 1];
554
+ switch (key) {
555
+ case "components":
556
+ case "c":
557
+ result.componentsDir = value;
558
+ break;
559
+ case "tsconfig":
560
+ case "t":
561
+ result.tsconfig = value;
562
+ break;
563
+ case "out":
564
+ case "o":
565
+ result.outFile = value;
566
+ break;
567
+ case "name":
568
+ case "n":
569
+ result.libraryName = value;
570
+ break;
571
+ case "version":
572
+ case "v":
573
+ result.libraryVersion = value;
574
+ break;
575
+ case "prefix":
576
+ case "p":
577
+ result.prefix = value;
578
+ break;
579
+ }
580
+ i += 2;
581
+ }
582
+ else {
583
+ // Positional arguments (legacy support)
584
+ if (!result.componentsDir) {
585
+ result.componentsDir = arg;
586
+ }
587
+ else if (!result.tsconfig) {
588
+ result.tsconfig = arg;
589
+ }
590
+ else if (!result.outFile) {
591
+ result.outFile = arg;
592
+ }
593
+ i++;
594
+ }
595
+ }
596
+ return result;
597
+ }
598
+ const options = parseArgs(process.argv.slice(2));
599
+ generateWebTypes({
600
+ componentsDir: options.componentsDir || "components/ui",
601
+ tsconfig: options.tsconfig || detectDefaultTsconfig(),
602
+ outFile: options.outFile || "web-types.json",
603
+ libraryName: options.libraryName || "reactolith-components",
604
+ libraryVersion: options.libraryVersion || "1.0.0",
605
+ prefix: options.prefix || "",
606
+ });
607
+ //# sourceMappingURL=generate-web-types.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-web-types.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}