scripts/build-types.ts 7.4 K raw
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_DIR = "dist";
8
const REACT_OUTPUT = "custom-elements-jsx.ts";
9
const SVELTE_OUTPUT = "custom-elements-svelte.ts";
10
const VUE_OUTPUT = "custom-elements-vue.ts";
11
const TYPESCRIPT_OUTPUT = "custom-elements.ts";
12
13
interface ComponentInfo {
14
	tagName: string;
15
	attributes: string[];
16
	events: string[];
17
}
18
19
function convertEventName(eventName: string): string {
20
	// Convert kebab-case events to React camelCase handlers
21
	const words = eventName.split("-");
22
	const camelCase = words
23
		.map((word, index) =>
24
			index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1),
25
		)
26
		.join("");
27
	return "on" + camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
28
}
29
30
async function parseComponent(filePath: string): Promise<ComponentInfo | null> {
31
	const content = await readFile(filePath, "utf-8");
32
33
	// Extract tag name from customElements.define()
34
	const tagNameMatch = content.match(
35
		/customElements\.define\(['"`]([^'"`]+)['"`]/,
36
	);
37
	if (!tagNameMatch || !tagNameMatch[1]) return null;
38
39
	const tagName = tagNameMatch[1];
40
41
	// Extract attributes from observedAttributes
42
	const attributesMatch = content.match(
43
		/static get observedAttributes\(\)\s*\{\s*return\s*\[([\s\S]*?)\]/,
44
	);
45
	const attributes: string[] = [];
46
	if (attributesMatch && attributesMatch[1]) {
47
		const attributesStr = attributesMatch[1];
48
		const attrMatches = attributesStr.match(/['"`]([^'"`]+)['"`]/g);
49
		if (attrMatches) {
50
			attributes.push(...attrMatches.map((attr) => attr.slice(1, -1)));
51
		}
52
	}
53
54
	// Extract events from dispatchEvent calls
55
	const eventMatches = content.match(/new CustomEvent\(['"`]([^'"`]+)['"`]/g);
56
	const events: string[] = [];
57
	if (eventMatches) {
58
		for (const match of eventMatches) {
59
			const eventMatch = match.match(/['"`]([^'"`]+)['"`]/);
60
			if (eventMatch && eventMatch[1]) {
61
				events.push(eventMatch[1]);
62
			}
63
		}
64
	}
65
66
	// Remove duplicates
67
	const uniqueEvents = [...new Set(events)];
68
69
	return {
70
		tagName,
71
		attributes,
72
		events: uniqueEvents,
73
	};
74
}
75
76
function generateReactTypes(components: ComponentInfo[]): string {
77
	const intrinsicElements = components
78
		.map((comp) => {
79
			const attributeProps = comp.attributes
80
				.map((attr) => `    '${attr}'?: string;`)
81
				.join("\n");
82
83
			const eventHandlers = comp.events
84
				.map((event) => {
85
					const handlerName = convertEventName(event);
86
					return `    ${handlerName}?: (event: CustomEvent) => void;`;
87
				})
88
				.join("\n");
89
90
			return `  '${comp.tagName}': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement> & {
91
${attributeProps}
92
${eventHandlers}
93
  }, HTMLElement>;`;
94
		})
95
		.join("\n");
96
97
	// Common CSS properties for EVM components
98
	const cssProperties = `  '--color-background'?: string;
99
  '--color-foreground'?: string;
100
  '--color-primary'?: string;
101
  '--color-secondary'?: string;
102
  '--border-radius'?: string;`;
103
104
	return `import type React from 'react';
105
106
declare module 'react' {
107
  namespace JSX {
108
    interface IntrinsicElements {
109
${intrinsicElements}
110
    }
111
  }
112
113
  interface CSSProperties {
114
    // Norns UI CSS Custom Properties
115
${cssProperties}
116
  }
117
}
118
119
export interface CustomElements {
120
${intrinsicElements}
121
}
122
123
export interface CustomCssProperties {
124
${cssProperties}
125
}
126
`;
127
}
128
129
function generateSvelteTypes(components: ComponentInfo[]): string {
130
	const svelteHTMLElements = components
131
		.map((comp) => {
132
			const attributeProps = comp.attributes
133
				.map((attr) => `      '${attr}'?: string;`)
134
				.join("\n");
135
136
			const eventHandlers = comp.events
137
				.map((event) => {
138
					return `      'on:${event}'?: (event: CustomEvent) => void;`;
139
				})
140
				.join("\n");
141
142
			const allProps = [attributeProps, eventHandlers]
143
				.filter((p) => p)
144
				.join("\n");
145
146
			return `    '${comp.tagName}': {\n${allProps}\n    };`;
147
		})
148
		.join("\n");
149
150
	return `declare module 'svelte/elements' {
151
  export interface SvelteHTMLElements {
152
${svelteHTMLElements}
153
  }
154
}
155
156
export {};
157
`;
158
}
159
160
function generateTypeScriptTypes(components: ComponentInfo[]): string {
161
	const elementInterfaces = components
162
		.map((comp) => {
163
			const attributeProps = comp.attributes
164
				.map((attr) => `  setAttribute('${attr}', value: string): void;`)
165
				.join("\n");
166
167
			const eventHandlers = comp.events
168
				.map((event) => {
169
					return `  addEventListener(type: '${event}', listener: (event: CustomEvent) => void): void;`;
170
				})
171
				.join("\n");
172
173
			return `interface ${toPascalCase(comp.tagName)}Element extends HTMLElement {
174
${attributeProps}
175
${eventHandlers}
176
}`;
177
		})
178
		.join("\n\n");
179
180
	const htmlElementTagMap = components
181
		.map((comp) => {
182
			return `  '${comp.tagName}': ${toPascalCase(comp.tagName)}Element;`;
183
		})
184
		.join("\n");
185
186
	return `${elementInterfaces}
187
188
declare global {
189
  interface HTMLElementTagNameMap {
190
${htmlElementTagMap}
191
  }
192
}
193
194
export {};
195
`;
196
}
197
198
function generateVueTypes(components: ComponentInfo[]): string {
199
	const globalComponents = components
200
		.map((comp) => {
201
			const attributeProps = comp.attributes
202
				.map((attr) => `      '${attr}'?: string;`)
203
				.join("\n");
204
205
			const eventHandlers = comp.events
206
				.map((event) => {
207
					// Convert to camelCase for Vue's @event syntax
208
					const camelEvent = event
209
						.split("-")
210
						.map((word, i) =>
211
							i === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1),
212
						)
213
						.join("");
214
					return `      'on${camelEvent.charAt(0).toUpperCase() + camelEvent.slice(1)}'?: (event: CustomEvent) => void;`;
215
				})
216
				.join("\n");
217
218
			const allProps = [attributeProps, eventHandlers]
219
				.filter((p) => p)
220
				.join("\n");
221
222
			return `    '${comp.tagName}': {\n${allProps}\n    };`;
223
		})
224
		.join("\n");
225
226
	return `declare module 'vue' {
227
  export interface GlobalComponents {
228
${globalComponents}
229
  }
230
}
231
232
export {};
233
`;
234
}
235
236
function toPascalCase(str: string): string {
237
	return str
238
		.split("-")
239
		.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
240
		.join("");
241
}
242
243
async function main() {
244
	try {
245
		console.log("🔧 Building type definitions...");
246
247
		// Read component files
248
		const files = await readdir(COMPONENTS_DIR);
249
		const jsFiles = files.filter((file) => file.endsWith(".js"));
250
251
		const components: ComponentInfo[] = [];
252
253
		for (const file of jsFiles) {
254
			const filePath = join(COMPONENTS_DIR, file);
255
			const componentInfo = await parseComponent(filePath);
256
			if (componentInfo) {
257
				components.push(componentInfo);
258
			}
259
		}
260
261
		console.log(`📦 Found ${components.length} custom elements`);
262
263
		// Generate React types
264
		const reactTypesCode = generateReactTypes(components);
265
		await writeFile(join(OUTPUT_DIR, REACT_OUTPUT), reactTypesCode);
266
		console.log(`✅ Generated React types: ${REACT_OUTPUT}`);
267
268
		// Generate Svelte types
269
		const svelteTypesCode = generateSvelteTypes(components);
270
		await writeFile(join(OUTPUT_DIR, SVELTE_OUTPUT), svelteTypesCode);
271
		console.log(`✅ Generated Svelte types: ${SVELTE_OUTPUT}`);
272
273
		// Generate Vue types
274
		const vueTypesCode = generateVueTypes(components);
275
		await writeFile(join(OUTPUT_DIR, VUE_OUTPUT), vueTypesCode);
276
		console.log(`✅ Generated Vue types: ${VUE_OUTPUT}`);
277
278
		// Generate TypeScript types
279
		const tsTypesCode = generateTypeScriptTypes(components);
280
		await writeFile(join(OUTPUT_DIR, TYPESCRIPT_OUTPUT), tsTypesCode);
281
		console.log(`✅ Generated TypeScript types: ${TYPESCRIPT_OUTPUT}`);
282
283
		console.log("🎉 Type definitions generated successfully!");
284
	} catch (error) {
285
		console.error("❌ Error generating type definitions:", error);
286
		process.exit(1);
287
	}
288
}
289
290
if (import.meta.main) {
291
	main();
292
}