Skip to content

Conversation

@Murderlon
Copy link
Member

@Murderlon Murderlon commented Jan 8, 2026

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:

<!-- 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:

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:

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:

// 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.

Vue convention is to use kebab-case for props in templates (e.g., `:edit-file`),
but the generated Vue components weren't handling this correctly.

The issue: Vue's `attrs` preserves the casing from templates. When using
`:edit-file`, Vue passes `attrs['edit-file']` to the component, but Preact
expects camelCase (`editFile`).

The fix: Generate Vue components with explicit `props` arrays. When Vue
components declare their props, Vue automatically converts kebab-case template
usage to camelCase in the `props` object.

This is the standard Vue approach - not a hacky conversion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@changeset-bot
Copy link

changeset-bot bot commented Jan 8, 2026

🦋 Changeset detected

Latest commit: dda2970

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@uppy/components Patch
@uppy/vue Patch
@uppy/react Patch
@uppy/svelte Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Murderlon Murderlon changed the title fix(vue): support kebab-case props in generated components @uppy/vue: support kebab-case props in generated components Jan 8, 2026
@Murderlon Murderlon self-assigned this Jan 8, 2026
@Murderlon Murderlon requested review from mifi and qxprakash January 8, 2026 10:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 8 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +35 to +44
// 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]

Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern for matching Props type definitions uses [^}]+ which will only capture content up to the first closing brace. While this works for the current simple prop types, it will break if a prop has an inline object type with nested braces. Consider using a more robust parsing approach, such as TypeScript's Compiler API or a recursive brace-matching algorithm, to handle nested type definitions.

Suggested change
// 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]
// Match the start of the Props type definition: export type ComponentNameProps = { ... }
const propsStartRegex = /export\s+type\s+\w+Props\s*=\s*\{/m
const propsStartMatch = propsStartRegex.exec(content)
if (!propsStartMatch) {
return []
}
// Find the full Props block by matching balanced braces, to support nested object types
const propsBlockStart = propsStartMatch.index + propsStartMatch[0].length
let braceDepth = 1
let i = propsBlockStart
while (i < content.length && braceDepth > 0) {
const ch = content[i]
if (ch === '{') {
braceDepth += 1
} else if (ch === '}') {
braceDepth -= 1
}
i += 1
}
if (braceDepth !== 0) {
// Unbalanced braces; fall back to no props
return []
}
const propsBlock = content.slice(propsBlockStart, i - 1)

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@mifi mifi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i assume you tested this, and since it seems now that this is most likely just a temporary solution, i think we can merge it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants