mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
@uppy/vue: support kebab-case props in generated components (#6125)
## 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>
This commit is contained in:
parent
8912bafaf4
commit
fa23832f6a
8 changed files with 96 additions and 58 deletions
6
.changeset/clean-monkeys-smoke.md
Normal file
6
.changeset/clean-monkeys-smoke.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@uppy/components": patch
|
||||
"@uppy/vue": patch
|
||||
---
|
||||
|
||||
- Fix Vue components to work with kebab-case props (`:edit-file` instead of `:editFile`)
|
||||
|
|
@ -25,6 +25,37 @@ const SVELTE_DIR = path.join(
|
|||
'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
|
||||
|
|
@ -60,18 +91,18 @@ export default function %%ComponentName%%(props: Omit<%%PropsTypeName%%, 'ctx'>)
|
|||
const VUE_TEMPLATE = `\
|
||||
// This file was generated by build-components.mjs
|
||||
// ANY EDITS WILL BE OVERWRITTEN!
|
||||
import { defineComponent, ref, watch, onMounted, h } from 'vue'
|
||||
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 { shallowEqualObjects } from 'shallow-equal'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<%%PropsTypeName%%, 'ctx'>>({
|
||||
export default defineComponent({
|
||||
name: '%%ComponentName%%',
|
||||
setup(props, { attrs }) {
|
||||
props: %%PropsArray%%,
|
||||
setup(props) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
|
|
@ -79,7 +110,7 @@ export default defineComponent<Omit<%%PropsTypeName%%, 'ctx'>>({
|
|||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(%%PreactComponentName%%, {
|
||||
...(attrs as %%PropsTypeName%%),
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies %%PropsTypeName%%),
|
||||
containerRef.value,
|
||||
|
|
@ -97,11 +128,10 @@ export default defineComponent<Omit<%%PropsTypeName%%, 'ctx'>>({
|
|||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
render%%ComponentName%%()
|
||||
}
|
||||
() => {
|
||||
render%%ComponentName%%()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
|
|
@ -174,6 +204,11 @@ 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(
|
||||
|
|
@ -190,6 +225,7 @@ try {
|
|||
)
|
||||
.replace(/%%PreactComponentName%%/g, preactComponentName)
|
||||
.replace(/%%PropsTypeName%%/g, propsTypeName)
|
||||
.replace(/%%PropsArray%%/g, propsArray)
|
||||
|
||||
// Generate Svelte wrapper
|
||||
const svelteContent = SVELTE_TEMPLATE.replace(
|
||||
|
|
@ -213,7 +249,9 @@ try {
|
|||
vueComponents.push(componentName)
|
||||
svelteComponents.push(componentName)
|
||||
|
||||
console.log(`√ ${componentName}`)
|
||||
console.log(
|
||||
`√ ${componentName} (props: ${propNames.join(', ') || 'none'})`,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error processing component ${file}:`, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {
|
|||
Dropzone as PreactDropzone,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH, render as preactRender } from 'preact'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { defineComponent, h, onMounted, ref, watch } from 'vue'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<DropzoneProps, 'ctx'>>({
|
||||
export default defineComponent({
|
||||
name: 'Dropzone',
|
||||
setup(props, { attrs }) {
|
||||
props: ['width', 'height', 'note', 'noClick'],
|
||||
setup(props) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ export default defineComponent<Omit<DropzoneProps, 'ctx'>>({
|
|||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactDropzone, {
|
||||
...(attrs as DropzoneProps),
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies DropzoneProps),
|
||||
containerRef.value,
|
||||
|
|
@ -38,11 +38,10 @@ export default defineComponent<Omit<DropzoneProps, 'ctx'>>({
|
|||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderDropzone()
|
||||
}
|
||||
() => {
|
||||
renderDropzone()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {
|
|||
FilesGrid as PreactFilesGrid,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH, render as preactRender } from 'preact'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { defineComponent, h, onMounted, ref, watch } from 'vue'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<FilesGridProps, 'ctx'>>({
|
||||
export default defineComponent({
|
||||
name: 'FilesGrid',
|
||||
setup(props, { attrs }) {
|
||||
props: ['editFile', 'columns', 'imageThumbnail'],
|
||||
setup(props) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ export default defineComponent<Omit<FilesGridProps, 'ctx'>>({
|
|||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactFilesGrid, {
|
||||
...(attrs as FilesGridProps),
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies FilesGridProps),
|
||||
containerRef.value,
|
||||
|
|
@ -38,11 +38,10 @@ export default defineComponent<Omit<FilesGridProps, 'ctx'>>({
|
|||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderFilesGrid()
|
||||
}
|
||||
() => {
|
||||
renderFilesGrid()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {
|
|||
FilesList as PreactFilesList,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH, render as preactRender } from 'preact'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { defineComponent, h, onMounted, ref, watch } from 'vue'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<FilesListProps, 'ctx'>>({
|
||||
export default defineComponent({
|
||||
name: 'FilesList',
|
||||
setup(props, { attrs }) {
|
||||
props: ['editFile', 'imageThumbnail'],
|
||||
setup(props) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ export default defineComponent<Omit<FilesListProps, 'ctx'>>({
|
|||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactFilesList, {
|
||||
...(attrs as FilesListProps),
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies FilesListProps),
|
||||
containerRef.value,
|
||||
|
|
@ -38,11 +38,10 @@ export default defineComponent<Omit<FilesListProps, 'ctx'>>({
|
|||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderFilesList()
|
||||
}
|
||||
() => {
|
||||
renderFilesList()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {
|
|||
type ProviderIconProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH, render as preactRender } from 'preact'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { defineComponent, h, onMounted, ref, watch } from 'vue'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<ProviderIconProps, 'ctx'>>({
|
||||
export default defineComponent({
|
||||
name: 'ProviderIcon',
|
||||
setup(props, { attrs }) {
|
||||
props: ['provider', 'fill'],
|
||||
setup(props) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ export default defineComponent<Omit<ProviderIconProps, 'ctx'>>({
|
|||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactProviderIcon, {
|
||||
...(attrs as ProviderIconProps),
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies ProviderIconProps),
|
||||
containerRef.value,
|
||||
|
|
@ -38,11 +38,10 @@ export default defineComponent<Omit<ProviderIconProps, 'ctx'>>({
|
|||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderProviderIcon()
|
||||
}
|
||||
() => {
|
||||
renderProviderIcon()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {
|
|||
type ThumbnailProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH, render as preactRender } from 'preact'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { defineComponent, h, onMounted, ref, watch } from 'vue'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<ThumbnailProps, 'ctx'>>({
|
||||
export default defineComponent({
|
||||
name: 'Thumbnail',
|
||||
setup(props, { attrs }) {
|
||||
props: ['file', 'width', 'height', 'images'],
|
||||
setup(props) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ export default defineComponent<Omit<ThumbnailProps, 'ctx'>>({
|
|||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactThumbnail, {
|
||||
...(attrs as ThumbnailProps),
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies ThumbnailProps),
|
||||
containerRef.value,
|
||||
|
|
@ -38,11 +38,10 @@ export default defineComponent<Omit<ThumbnailProps, 'ctx'>>({
|
|||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderThumbnail()
|
||||
}
|
||||
() => {
|
||||
renderThumbnail()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {
|
|||
type UploadButtonProps,
|
||||
} from '@uppy/components'
|
||||
import { h as preactH, render as preactRender } from 'preact'
|
||||
import { shallowEqualObjects } from 'shallow-equal'
|
||||
import { defineComponent, h, onMounted, ref, watch } from 'vue'
|
||||
import { useUppyContext } from '../useUppyContext.js'
|
||||
|
||||
export default defineComponent<Omit<UploadButtonProps, 'ctx'>>({
|
||||
export default defineComponent({
|
||||
name: 'UploadButton',
|
||||
setup(props, { attrs }) {
|
||||
props: [],
|
||||
setup(props) {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const ctx = useUppyContext()
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ export default defineComponent<Omit<UploadButtonProps, 'ctx'>>({
|
|||
if (containerRef.value) {
|
||||
preactRender(
|
||||
preactH(PreactUploadButton, {
|
||||
...(attrs as UploadButtonProps),
|
||||
...props,
|
||||
ctx,
|
||||
} satisfies UploadButtonProps),
|
||||
containerRef.value,
|
||||
|
|
@ -38,11 +38,10 @@ export default defineComponent<Omit<UploadButtonProps, 'ctx'>>({
|
|||
|
||||
watch(
|
||||
() => props,
|
||||
(current, old) => {
|
||||
if (!shallowEqualObjects(current, old)) {
|
||||
renderUploadButton()
|
||||
}
|
||||
() => {
|
||||
renderUploadButton()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
return () => h('div', { ref: containerRef })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue