feat: added initial type builds cc3b3385
Steve · 2025-09-21 23:04 3 file(s) · +186 −25
package.json +3 −1
10 10
	"files": [
11 11
		"dist"
12 12
	],
13 +
	"types": "dist/custom-elements-jsx.d.ts",
13 14
	"scripts": {
14 -
		"build": "bun build src/index.ts --outdir dist --target node && bun run build:components",
15 +
		"build": "bun build src/index.ts --outdir dist --target node && bun run build:components && bun run build:types",
15 16
		"build:components": "bun scripts/build-components.ts",
17 +
		"build:types": "bun scripts/build-types.ts",
16 18
		"build:site": "bun scripts/build-site",
17 19
		"dev": "bun site/index.html --console"
18 20
	},
scripts/build-types.ts (added) +159 −0
1 +
#!/usr/bin/env bun
2 +
3 +
import { readFile, writeFile, readdir } from "node:fs/promises";
4 +
import { join } from "node:path";
5 +
6 +
const COMPONENTS_DIR = "src/components";
7 +
const OUTPUT_PATH = "dist/custom-elements-jsx.d.ts";
8 +
9 +
interface ComponentInfo {
10 +
	tagName: string;
11 +
	attributes: string[];
12 +
	events: string[];
13 +
}
14 +
15 +
function convertEventName(eventName: string): string {
16 +
	// Convert kebab-case events to React camelCase handlers
17 +
	const words = eventName.split("-");
18 +
	const camelCase = words
19 +
		.map((word, index) =>
20 +
			index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1),
21 +
		)
22 +
		.join("");
23 +
	return "on" + camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
24 +
}
25 +
26 +
async function parseComponent(filePath: string): Promise<ComponentInfo | null> {
27 +
	const content = await readFile(filePath, "utf-8");
28 +
29 +
	// Extract tag name from customElements.define()
30 +
	const tagNameMatch = content.match(
31 +
		/customElements\.define\(['"`]([^'"`]+)['"`]/,
32 +
	);
33 +
	if (!tagNameMatch || !tagNameMatch[1]) return null;
34 +
35 +
	const tagName = tagNameMatch[1];
36 +
37 +
	// Extract attributes from observedAttributes
38 +
	const attributesMatch = content.match(
39 +
		/static get observedAttributes\(\)\s*\{\s*return\s*\[([\s\S]*?)\]/,
40 +
	);
41 +
	const attributes: string[] = [];
42 +
	if (attributesMatch && attributesMatch[1]) {
43 +
		const attributesStr = attributesMatch[1];
44 +
		const attrMatches = attributesStr.match(/['"`]([^'"`]+)['"`]/g);
45 +
		if (attrMatches) {
46 +
			attributes.push(...attrMatches.map((attr) => attr.slice(1, -1)));
47 +
		}
48 +
	}
49 +
50 +
	// Extract events from dispatchEvent calls
51 +
	const eventMatches = content.match(/new CustomEvent\(['"`]([^'"`]+)['"`]/g);
52 +
	const events: string[] = [];
53 +
	if (eventMatches) {
54 +
		for (const match of eventMatches) {
55 +
			const eventMatch = match.match(/['"`]([^'"`]+)['"`]/);
56 +
			if (eventMatch && eventMatch[1]) {
57 +
				events.push(eventMatch[1]);
58 +
			}
59 +
		}
60 +
	}
61 +
62 +
	// Remove duplicates
63 +
	const uniqueEvents = [...new Set(events)];
64 +
65 +
	return {
66 +
		tagName,
67 +
		attributes,
68 +
		events: uniqueEvents,
69 +
	};
70 +
}
71 +
72 +
function generateJSXTypes(components: ComponentInfo[]): string {
73 +
	const intrinsicElements = components
74 +
		.map((comp) => {
75 +
			const attributeProps = comp.attributes
76 +
				.map((attr) => `    '${attr}'?: string;`)
77 +
				.join("\n");
78 +
79 +
			const eventHandlers = comp.events
80 +
				.map((event) => {
81 +
					const handlerName = convertEventName(event);
82 +
					return `    ${handlerName}?: (event: CustomEvent) => void;`;
83 +
				})
84 +
				.join("\n");
85 +
86 +
			return `  '${comp.tagName}': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement> & {
87 +
${attributeProps}
88 +
${eventHandlers}
89 +
  }, HTMLElement>;`;
90 +
		})
91 +
		.join("\n");
92 +
93 +
	// Common CSS properties for EVM components
94 +
	const cssProperties = `  '--color-background'?: string;
95 +
  '--color-foreground'?: string;
96 +
  '--color-primary'?: string;
97 +
  '--color-secondary'?: string;
98 +
  '--border-radius'?: string;`;
99 +
100 +
	return `import type React from 'react';
101 +
102 +
declare module 'react' {
103 +
  namespace JSX {
104 +
    interface IntrinsicElements {
105 +
${intrinsicElements}
106 +
    }
107 +
  }
108 +
  
109 +
  interface CSSProperties {
110 +
    // Norns UI CSS Custom Properties
111 +
${cssProperties}
112 +
  }
113 +
}
114 +
115 +
export interface CustomElements {
116 +
${intrinsicElements}
117 +
}
118 +
119 +
export interface CustomCssProperties {
120 +
${cssProperties}
121 +
}
122 +
`;
123 +
}
124 +
125 +
async function main() {
126 +
	try {
127 +
		console.log("🔧 Building JSX types...");
128 +
129 +
		// Read component files
130 +
		const files = await readdir(COMPONENTS_DIR);
131 +
		const jsFiles = files.filter((file) => file.endsWith(".js"));
132 +
133 +
		const components: ComponentInfo[] = [];
134 +
135 +
		for (const file of jsFiles) {
136 +
			const filePath = join(COMPONENTS_DIR, file);
137 +
			const componentInfo = await parseComponent(filePath);
138 +
			if (componentInfo) {
139 +
				components.push(componentInfo);
140 +
			}
141 +
		}
142 +
143 +
		console.log(`📦 Found ${components.length} custom elements`);
144 +
145 +
		// Generate JSX intrinsic elements types
146 +
		const jsxTypesCode = generateJSXTypes(components);
147 +
		await writeFile(OUTPUT_PATH, jsxTypesCode);
148 +
		console.log(`✅ Generated JSX types: ${OUTPUT_PATH}`);
149 +
150 +
		console.log("🎉 JSX types generated successfully!");
151 +
	} catch (error) {
152 +
		console.error("❌ Error generating JSX types:", error);
153 +
		process.exit(1);
154 +
	}
155 +
}
156 +
157 +
if (import.meta.main) {
158 +
	main();
159 +
}
tsconfig.json +24 −24
1 1
{
2 -
  "compilerOptions": {
3 -
    // Environment setup & latest features
4 -
    "lib": ["ESNext"],
5 -
    "target": "ESNext",
6 -
    "module": "Preserve",
7 -
    "moduleDetection": "force",
8 -
    "jsx": "react-jsx",
9 -
    "allowJs": true,
2 +
	"compilerOptions": {
3 +
		// Environment setup & latest features
4 +
		"lib": ["ESNext", "DOM", "DOM.Iterable"],
5 +
		"target": "ESNext",
6 +
		"module": "Preserve",
7 +
		"moduleDetection": "force",
8 +
		"jsx": "react-jsx",
9 +
		"allowJs": true,
10 10
11 -
    // Bundler mode
12 -
    "moduleResolution": "bundler",
13 -
    "allowImportingTsExtensions": true,
14 -
    "verbatimModuleSyntax": true,
15 -
    "noEmit": true,
11 +
		// Bundler mode
12 +
		"moduleResolution": "bundler",
13 +
		"allowImportingTsExtensions": true,
14 +
		"verbatimModuleSyntax": true,
15 +
		"noEmit": true,
16 16
17 -
    // Best practices
18 -
    "strict": true,
19 -
    "skipLibCheck": true,
20 -
    "noFallthroughCasesInSwitch": true,
21 -
    "noUncheckedIndexedAccess": true,
22 -
    "noImplicitOverride": true,
17 +
		// Best practices
18 +
		"strict": true,
19 +
		"skipLibCheck": true,
20 +
		"noFallthroughCasesInSwitch": true,
21 +
		"noUncheckedIndexedAccess": true,
22 +
		"noImplicitOverride": true,
23 23
24 -
    // Some stricter flags (disabled by default)
25 -
    "noUnusedLocals": false,
26 -
    "noUnusedParameters": false,
27 -
    "noPropertyAccessFromIndexSignature": false
28 -
  }
24 +
		// Some stricter flags (disabled by default)
25 +
		"noUnusedLocals": false,
26 +
		"noUnusedParameters": false,
27 +
		"noPropertyAccessFromIndexSignature": false
28 +
	}
29 29
}