#!/usr/bin/env node // When new components are added (not when existing ones are changed), // run this script to generate React, Vue, and Svelte wrappers import { existsSync } from 'node:fs' import fs from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' // Get the directory of this script const scriptDir = path.dirname(fileURLToPath(import.meta.url)) const rootDir = path.resolve(scriptDir, '../../..') // Define paths // source: const COMPONENTS_DIR = path.join(rootDir, 'packages/@uppy/components/src') // destinations: const REACT_DIR = path.join( rootDir, 'packages/@uppy/react/src/headless/generated', ) const VUE_DIR = path.join(rootDir, 'packages/@uppy/vue/src/headless/generated') const SVELTE_DIR = path.join( rootDir, 'packages/@uppy/svelte/src/lib/components/headless/generated', ) /** * Parse prop names from a TypeScript component file * Extracts property names from the Props type definition, excluding 'ctx' */ async function parsePropsFromFile(filePath) { const content = await fs.readFile(filePath, 'utf-8') // Match the Props type definition: export type ComponentNameProps = { ... } const propsMatch = content.match( /export\s+type\s+\w+Props\s*=\s*\{([^}]+)\}/s, ) if (!propsMatch) { return [] } const propsBlock = propsMatch[1] // Extract property names (handle optional ? and required properties) const propNames = [] const propRegex = /^\s*(\w+)\??:/gm for (const match of propsBlock.matchAll(propRegex)) { const propName = match[1] // Exclude 'ctx' as it's provided by the context if (propName !== 'ctx') { propNames.push(propName) } } return propNames } // Templates const REACT_TEMPLATE = `\ // This file was generated by build-components.mjs // ANY EDITS WILL BE OVERWRITTEN! import { %%ComponentName%% as %%PreactComponentName%%, type %%PropsTypeName%%, } from '@uppy/components' import { h as preactH, render as preactRender } from 'preact' import { useContext, useEffect, useRef } from 'react' import { UppyContext } from '../UppyContextProvider.js' export default function %%ComponentName%%(props: Omit<%%PropsTypeName%%, 'ctx'>) { const ref = useRef(null) const ctx = useContext(UppyContext) useEffect(() => { if (ref.current) { preactRender( preactH(%%PreactComponentName%%, { ...props, ctx, } satisfies %%PropsTypeName%%), ref.current, ) } }, [ctx, props]) return
} ` const VUE_TEMPLATE = `\ // This file was generated by build-components.mjs // ANY EDITS WILL BE OVERWRITTEN! import { defineComponent, h, onMounted, ref, watch } from 'vue' import { %%ComponentName%% as %%PreactComponentName%%, type %%PropsTypeName%%, } from '@uppy/components' import { h as preactH, render as preactRender } from 'preact' import { useUppyContext } from '../useUppyContext.js' export default defineComponent({ name: '%%ComponentName%%', props: %%PropsArray%%, setup(props) { const containerRef = ref(null) const ctx = useUppyContext() function render%%ComponentName%%() { if (containerRef.value) { preactRender( preactH(%%PreactComponentName%%, { ...props, ctx, } satisfies %%PropsTypeName%%), containerRef.value, ) } } onMounted(() => { render%%ComponentName%%() }) watch(ctx, () => { render%%ComponentName%%() }) watch( () => props, () => { render%%ComponentName%%() }, { deep: true }, ) return () => h('div', { ref: containerRef }) }, }) ` const SVELTE_TEMPLATE = `\
` try { // Check if components directory exists await fs.access(COMPONENTS_DIR).catch(() => { throw new Error(`Components directory not found: ${COMPONENTS_DIR}`) }) await Promise.all( [REACT_DIR, VUE_DIR, SVELTE_DIR].map(async (dir) => { if (!existsSync(dir)) { await fs.mkdir(dir, { recursive: true }) } }), ) // Read all files in components directory const files = await fs.readdir(COMPONENTS_DIR) // Filter for .tsx files const tsxFiles = files.filter((file) => file.endsWith('.tsx')) console.log(`Found ${tsxFiles.length} Preact component(s) to process\n`) // Track generated components for index files const reactComponents = [] const vueComponents = [] const svelteComponents = [] // Process each tsx file for (const file of tsxFiles) { try { const componentName = path.basename(file, '.tsx') const propsTypeName = `${componentName}Props` const preactComponentName = `Preact${componentName}` const filePath = path.join(COMPONENTS_DIR, file) // Parse props from the source file const propNames = await parsePropsFromFile(filePath) const propsArray = JSON.stringify(propNames) // Generate React wrapper const reactContent = REACT_TEMPLATE.replace( /%%ComponentName%%/g, componentName, ) .replace(/%%PreactComponentName%%/g, preactComponentName) .replace(/%%PropsTypeName%%/g, propsTypeName) // Generate Vue wrapper const vueContent = VUE_TEMPLATE.replace( /%%ComponentName%%/g, componentName, ) .replace(/%%PreactComponentName%%/g, preactComponentName) .replace(/%%PropsTypeName%%/g, propsTypeName) .replace(/%%PropsArray%%/g, propsArray) // Generate Svelte wrapper const svelteContent = SVELTE_TEMPLATE.replace( /%%ComponentName%%/g, componentName, ) .replace(/%%PreactComponentName%%/g, preactComponentName) .replace(/%%PropsTypeName%%/g, propsTypeName) // Write files const reactFilePath = path.join(REACT_DIR, `${componentName}.tsx`) const vueFilePath = path.join(VUE_DIR, `${componentName}.ts`) const svelteFilePath = path.join(SVELTE_DIR, `${componentName}.svelte`) await fs.writeFile(reactFilePath, reactContent) await fs.writeFile(vueFilePath, vueContent) await fs.writeFile(svelteFilePath, svelteContent) // Add to component lists for index files reactComponents.push(componentName) vueComponents.push(componentName) svelteComponents.push(componentName) console.log( `√ ${componentName} (props: ${propNames.join(', ') || 'none'})`, ) } catch (error) { console.error(`Error processing component ${file}:`, error) } } // Generate index files if (reactComponents.length > 0) { const reactIndexContent = reactComponents .map((name) => `export { default as ${name} } from './${name}.js'`) .join('\n') await fs.writeFile( path.join(REACT_DIR, 'index.ts'), `${reactIndexContent}\n`, ) console.log(`\nExporting React components from ${REACT_DIR}`) } if (vueComponents.length > 0) { const vueIndexContent = vueComponents .map((name) => `export { default as ${name} } from './${name}.js'`) .join('\n') await fs.writeFile(path.join(VUE_DIR, 'index.ts'), `${vueIndexContent}\n`) console.log(`Exporting Vue components from ${VUE_DIR}`) } if (svelteComponents.length > 0) { const svelteIndexContent = svelteComponents .map((name) => `export { default as ${name} } from './${name}.svelte'`) .join('\n') await fs.writeFile( path.join(SVELTE_DIR, 'index.ts'), `${svelteIndexContent}\n`, ) console.log(`Exporting Svelte components from ${SVELTE_DIR}`) } console.log('\nAll wrappers and index files generated successfully!') } catch (error) { console.error('Error generating wrappers:', error) process.exit(1) }