mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
## Summary
- Fix Vue components to work with kebab-case props (`:edit-file` instead
of `:editFile`)
- Update `migrate.mjs` to parse prop names from TypeScript source files
and generate explicit `props` arrays
## Problem
The generated Vue components didn't work correctly with Vue's standard
kebab-case prop convention:
```vue
<!-- This didn't work -->
<FilesList :edit-file="handleEdit" />
<!-- Only this worked (non-standard) -->
<FilesList :editFile="handleEdit" />
```
## Root Cause
The original Vue template used `attrs` to pass props to Preact:
```ts
setup(props, { attrs }) {
preactRender(preactH(PreactComponent, {
...(attrs as Props), // attrs preserves kebab-case!
ctx,
}), container)
}
```
When using `:edit-file` in a Vue template, Vue passes
`attrs['edit-file']` (kebab-case preserved), but Preact expects
`editFile` (camelCase).
## Solution
Generate Vue components with explicit `props` declarations:
```ts
defineComponent({
props: ['editFile', 'columns', 'imageThumbnail'],
setup(props) {
preactRender(preactH(PreactComponent, {
...props, // Vue already converted kebab → camelCase
ctx,
}), container)
}
})
```
When Vue components declare their props, Vue automatically converts
kebab-case template usage to camelCase in the `props` object. This is
standard Vue behavior.
## Why Simpler Alternatives Don't Work
### "Just use `props` instead of `attrs`"
Without a `props` declaration, Vue doesn't know which attributes are
props. The `props` object will be empty and everything goes to `attrs`:
```ts
// Without props declaration
defineComponent({
setup(props, { attrs }) {
// props = {} (empty!)
// attrs = { 'edit-file': fn } (kebab-case preserved)
}
})
```
### "Accept all props dynamically"
Vue doesn't have a "accept all props and convert casing" option. You
must explicitly declare which props you expect for Vue to handle the
conversion.
### "Just document to use camelCase"
This works but violates Vue conventions. Every Vue developer expects
`:edit-file` to work.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Prakash <qxprakash@gmail.com>
295 lines
8.4 KiB
JavaScript
295 lines
8.4 KiB
JavaScript
#!/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 <div ref={ref} />
|
|
}
|
|
`
|
|
|
|
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<HTMLElement | null>(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 = `\
|
|
<script lang="ts">
|
|
// This file was generated by build-components.mjs
|
|
// ANY EDITS WILL BE OVERWRITTEN!
|
|
import { getContext, mount } from 'svelte'
|
|
import {
|
|
%%ComponentName%% as %%PreactComponentName%%,
|
|
type %%PropsTypeName%%,
|
|
type UppyContext,
|
|
} from '@uppy/components'
|
|
import { h as preactH, render as preactRender } from 'preact'
|
|
import { UppyContextKey } from '../UppyContextProvider.svelte'
|
|
|
|
const props: Omit<%%PropsTypeName%%, 'ctx'> = $props()
|
|
const ctx = getContext<UppyContext>(UppyContextKey)
|
|
let container: HTMLElement
|
|
|
|
$effect(() => {
|
|
if (container) {
|
|
preactRender(
|
|
preactH(%%PreactComponentName%%, {
|
|
...props,
|
|
ctx,
|
|
} satisfies %%PropsTypeName%%),
|
|
container,
|
|
)
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<div bind:this={container}></div>
|
|
`
|
|
|
|
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)
|
|
}
|