Migrate Component to Core Architecture
Migrate a React-only component to the core/React/Vue architecture pattern.
Usage
/migrate-component <ComponentName>
Example:
/migrate-component Switch
/migrate-component Table
Description
This skill migrates a component (or component family) from React-only implementation to a shared core architecture where:
- Core package (
@lumx/core): Contains framework-agnostic UI logic, tests (plain data only, no JSX), and stories (JSX renders using injected framework components) - React package (
@lumx/react): Thin wrapper that delegates to core - Vue package (
@lumx/vue): Thin wrapper that delegates to core
Component Families:
- A component name may refer to a family of components (e.g., "Table" includes Table, TableRow, TableCell, TableBody, TableHeader)
- When migrating a component family, the skill will:
- Discover all components in the family by scanning the React folder
- Analyze dependencies between components and external dependencies
- Determine migration order based on dependencies
- Migrate each component in the correct order through all phases
Component Organization:
- Sub-components go in the same folder as their parent component
- Example:
BadgeandBadgeWrapperboth live in thebadge/folder- Core:
/packages/lumx-core/src/js/components/Badge/contains bothBadgeandBadgeWrapper - React:
/packages/lumx-react/src/components/badge/contains bothBadge.tsxandBadgeWrapper.tsx - Vue:
/packages/lumx-vue/src/components/badge/contains bothBadge.tsxandBadgeWrapper.tsx
- Core:
- Example:
Tablefamily in thetable/folder- Core:
/packages/lumx-core/src/js/components/Table/containsindex.tsx(Table),TableRow.tsx,TableCell.tsx, etc. - React:
/packages/lumx-react/src/components/table/containsTable.tsx,TableRow.tsx,TableCell.tsx, etc. - Vue:
/packages/lumx-vue/src/components/table/containsTable.tsx,TableRow.tsx,TableCell.tsx, etc.
- Core:
- The folder name uses lowercase-with-dashes (e.g.,
badge/,table/), while the component names use PascalCase (e.g.,Badge,BadgeWrapper,Table,TableRow)
Prerequisites
Before running this skill, ensure:
- The component exists in
@lumx/reactand is fully functional - The component has existing tests and stories
- A reference component (like Checkbox) has already been migrated and can serve as a pattern
Phase 0: Discovery & Dependency Analysis
Goal: Discover all components in the family and determine the correct migration order.
IMPORTANT: This phase MUST be completed before starting any migration work.
Check for existing core implementation:
- BEFORE starting migration, check if the component already exists in
@lumx/core - Read
/packages/lumx-core/src/js/components/<ComponentName>/to see if any files exist - Check for existing artifacts:
- UI files:
index.tsx,<SubComponent>.tsx,constants.ts - Stories:
Stories.tsx(orStories.ts) - Tests:
Tests.ts
- UI files:
- Document what already exists:
- If core UI implementation exists → Plan to reuse and skip Phase 1 (UI extraction)
- If core Stories exist → Plan to reuse and skip Phase 2 Step 1 (Core stories creation)
- If core Tests exist → Plan to reuse and skip Phase 3 Step 2 (Core tests creation)
- If ANY existing core code is found:
- Review the existing code to understand its structure
- USE AskUserQuestion tool to inform the developer about what exists
- Ask if any changes to the existing core implementation are needed
- If changes are needed, document them and get approval before proceeding
- If no core implementation exists, proceed with full migration
- BEFORE starting migration, check if the component already exists in
Discover component family:
- Read
/packages/lumx-react/src/components/<component-name>/index.tsto find all exported components - List all
.tsxfiles in the component folder - Identify which component is the parent and which are sub-components
- Read
Analyze dependencies:
- For each component, check if it imports:
- Constants from a shared
constants.tsfile (dependency on parent) - Other components from the same folder (dependency on siblings)
- Components from
@lumx/reactor@lumx/core(external dependencies)
- Constants from a shared
- Verify external component dependencies are available in
@lumx/core - Document any blocking dependencies (components not yet migrated to core)
- For each component, check if it imports:
Determine migration order:
- If there's a
constants.tsfile, migrate the parent component first (it will create the constants in core) - If sub-components import the parent component, migrate the parent first
- Otherwise, components can be migrated in any order
- Create a numbered list of components in migration order
- If there's a
Present migration plan to developer:
- Show whether core implementation already exists
- Show discovered components
- Show dependency analysis
- Show proposed migration order
- List any blocking dependencies
- If core exists, list any proposed changes to core implementation
- Ask for confirmation before proceeding
Example for Table family:
Discovered components:
1. Table (parent - defines constants.ts)
2. TableBody (imports TABLE_CLASSNAME from constants)
3. TableCell (imports TABLE_CLASSNAME from constants, uses Icon)
4. TableHeader (imports TABLE_CLASSNAME from constants)
5. TableRow (imports TABLE_CLASSNAME from constants)
Dependencies:
- All sub-components depend on constants.ts (created by Table)
- TableCell uses Icon (already available in core ✓)
Migration order:
1. Table (parent, creates constants)
2. TableBody
3. TableCell
4. TableHeader
5. TableRow
Validation Checkpoint 0:
- Developer is informed about any existing core implementation (UI, Stories, Tests)
- Developer reviews and approves the migration plan
- Developer confirms all external dependencies are available or acceptable to skip
- If changes to existing core code are proposed, developer approves those changes
🛑 IMPORTANT: Validation Checkpoints
This skill has MANDATORY validation checkpoints where you MUST stop and wait for user approval:
- Checkpoint 1: After Phase 1 (UI Implementation) - Verify React/Vue components work
- Checkpoint 2a: After stories migration - Verify React stories in Storybook
- Checkpoint 2b: After Vue stories - Verify Vue stories in Storybook
- Checkpoint 3: After tests migration - Verify all tests pass
At each checkpoint:
- STOP immediately - Do not continue to the next phase
- USE the AskUserQuestion tool to present results and ask for approval
- WAIT for user response - Only proceed when user explicitly approves
- DO NOT skip or rush through checkpoints - Each validation is critical
Migration Steps
IMPORTANT: After Phase 0 approval, migrate each component in the determined order by going through Phases 1-6 for each component before moving to the next.
Phase 1: UI Extraction & Implementation
Goal: Extract the core UI logic and create thin wrappers for React and Vue.
IMPORTANT - Check for Existing Core Implementation:
- If Phase 0 discovered that core UI implementation already exists, skip steps 1-2 and proceed directly to step 3 (Update React wrapper)
- If any modifications to the existing core implementation are needed:
- ALWAYS use AskUserQuestion tool BEFORE making any changes to core code
- Present the proposed changes clearly with rationale
- Wait for user approval before modifying any core files
- If no core implementation exists, proceed with steps 1-2 to create it
Create core component files:
- If migrating the parent component first (has constants.ts in React):
packages/lumx-core/src/js/components/<ComponentName>/ ├── constants.ts (migrate from React) └── index.tsx (parent component) - For standalone components:
packages/lumx-core/src/js/components/<ComponentName>/ └── index.tsx - For sub-components being migrated after parent (e.g.,
TableRowafterTable):packages/lumx-core/src/js/components/<ParentComponentName>/ ├── index.tsx (parent component) └── <SubComponentName>.tsx (e.g., BadgeWrapper.tsx) - Sub-components use separate files (e.g.,
BadgeWrapper.tsx), notindex.tsx
- If migrating the parent component first (has constants.ts in React):
Extract UI logic:
- For parent components with constants.ts:
- First migrate
constants.tsto core (keep exact same structure) - Update React imports to use
@lumx/core/js/components/<Component>/constants
- First migrate
- For sub-components:
- Update imports:
import { CLASSNAME as PARENT_CLASSNAME } from './constants'→import { CLASSNAME as PARENT_CLASSNAME } from '@lumx/core/js/components/<Parent>/constants'
- Update imports:
- For all components:
- Change
childrenprop tolabel: JSXElement(framework-agnostic) - Add required
inputId: stringprop if needed (generated by wrappers) - Use functional JSX calls:
InputLabel({ ... })instead of<InputLabel ... /> - Remove React-specific code (Children.count, etc.)
- Check for new callback props (e.g.,
onCustomEvent,onSpecialAction) defined in core UI - If new callbacks are introduced, update
PropsToOverridein/packages/lumx-core/src/js/types/jsx/PropsToOverride.tsto include them - Export:
Component,ComponentProps,COMPONENT_NAME,CLASSNAME,DEFAULT_PROPS(or import from constants if applicable)
- Change
- For parent components with constants.ts:
Update React wrapper:
- Import UI component from core
- Import
ReactToJSXtype utility from@lumx/react/utils/type/ReactToJSX - Define props interface using
ReactToJSX<UIProps, 'additionalPropsToOmit'>instead of manualOmit - Transform into thin wrapper using
forwardRef - Use hooks:
useId,useTheme,useDisableStateProps,useMergeRefs - Map
children→labelfor core component - Call
UI({ ... })instead of rendering JSX - Maintain backward compatibility
Create Vue wrapper:
- For standalone components, create directory structure:
packages/lumx-vue/src/components/<component-name>/ ├── <Component>.tsx └── index.ts - For sub-components (e.g.,
BadgeWrapperalongsideBadge), add to existing parent folder:packages/lumx-vue/src/components/<parent-component-name>/ ├── <ParentComponent>.tsx ├── <SubComponent>.tsx (e.g., BadgeWrapper.tsx) └── index.ts (update to export both components) - Use
defineComponentwith render function - Use composables:
useTheme,useId,useDisableStateProps - Support both
labelprop and default slot - Emit events instead of onChange callbacks
- Use JSX rendering:
return (<ComponentUI ... />) - Add stop propagation:
event.stopImmediatePropagation() - Define props using
keysOf<ComponentProps>() - Set
name: 'Lumx<Component>' - Set
inheritAttrs: false - Create
index.tswith exports for components and types only:- Read the React component's
index.tsfile for reference - Export components and types (props, enums, utilities)
- Do NOT export
CLASSNAME,COMPONENT_NAME, orDEFAULT_PROPS— these are internal constants - Vue uses default export for the component:
export { default as Component } - React uses named export:
export { Component }
- Read the React component's
- For standalone components, create directory structure:
Update Vue package index:
export * from './components/<component-name>';
🛑 MANDATORY Validation Checkpoint 1 - STOP HERE:
- Run
yarn testto ensure no regressions - Run
yarn type-checkto verify TypeScript compilation - STOP AND USE AskUserQuestion tool to ask developer for validation:
- Present test and type-check results
- Ask: "Phase 1 complete. Please verify React components work correctly and Vue components render basic UI. Should I proceed to Phase 2 (Stories Migration)?"
- Options: "Yes, proceed" / "No, fix issues first"
- DO NOT PROCEED to Phase 2 until developer selects "Yes, proceed"
- If developer selects "No", fix issues and ask again
Phase 2: Stories Migration
Goal: Create shared core stories with JSX renders and thin framework-specific wrappers.
KEY ARCHITECTURE:
- Core stories use JSX (
.tsxfile) with framework components injected via acomponentsparameter - Both React and Vue story files are thin wrappers that pass
componentsanddecoratorsto core, then re-export stories - No
.vuetemplate files for stories — all rendering is done via JSX in core - No
withRenderutility — replaced by JSX render functions defined in core - Keep the same stories — Don't add or remove stories; migrate existing ones only
HOW IT WORKS:
- Core
.tsxfiles use"jsx": "preserve"— the JSX is NOT compiled by core - When React's build imports the core
.tsxfile, React's toolchain compiles JSX toReact.createElement - When Vue's build imports it, Vue's toolchain compiles JSX to
h()calls - This means the same JSX code works for both frameworks, as long as it uses injected components (not hardcoded imports from
@lumx/reactor@lumx/vue)
Step 1: Analyze and Create Core Stories
IMPORTANT - Check for Existing Core Stories:
- If Phase 0 discovered that
Stories.tsorStories.tsxalready exists in core, review it and plan changes - If any modifications to existing core stories are needed:
- ALWAYS use AskUserQuestion tool BEFORE making any changes
- Present proposed changes with clear rationale
- Wait for user approval before modifying core Stories
Read and analyze existing React stories:
- Identify all components used in the stories (Icon, Text, FlexBox, etc.)
- These components will be passed via the
componentsparameter - Identify framework-only stories (e.g., React-only stories using
GenericBlock) — these stay in the framework file
Create core stories (
packages/lumx-core/src/js/components/<ComponentName>/Stories.tsx):- File extension is
.tsx— JSX is used for render functions - Export
setup()function that takes{ component, components, render, decorators }and returns story configurations - The
componentsparameter receives framework-specific component implementations (e.g.,{ Badge, Icon }) - Use
overridesonly when a story needs completely different structure per framework (rare)
KEY RULES for core stories:
- NEVER put JSX in
args— all JSX must live inrenderfunctions. This includesargs.children,args.before,args.after,args.badge, and any other prop. JSX inargscauses errors in Vue storybook tests (vitest). Only serializable data (strings, numbers, booleans, enums, objects) should be inargs. - NEVER put JSX in
withCombinationsrows/sections/cols — combination values are merged intoargsat runtime, so they have the same restriction. Userenderfunctions to produce JSX for different variants instead. - Define each story and
metaas individualconstvariables — this enables stories to reference each other (e.g.,WithIcon.renderreused byAllTypography). Return them as a flat object:return { meta, StoryA, StoryB, ... }. - Stories that need JSX content get their own
renderfunction — the render function receives args (serializable data) and returns JSX using the injected framework components. - Stories can reuse other stories'
render— e.g.,AllTypographycan setrender: WithIcon.renderto reuse the same rendering. - Composite stories can call other renders — e.g.,
AllColorscan compose{WithText.render(args)},{WithIcon.render(args)}together. - ALWAYS destructure
childrenout of args in render functions — when a render function provides its own inline JSX children, it must destructurechildrenfrom the args to prevent the inheritedchildrenvalue (frommeta.args) from leaking via{...args}onto the component. In Vue, spreadingchildrenas a prop on a DOM element causes a"Failed setting prop children"warning becausechildrenis a read-only DOM property. Use({ children, ...args }: any) =>instead of(args: any) =>.
Pattern:
```tsx import type { SetupStoriesOptions } from '@lumx/core/stories/types'; import { colorArgType } from '@lumx/core/stories/controls/color'; import { withUndefined } from '@lumx/core/stories/controls/withUndefined'; import { mdiHeart } from '@lumx/icons'; import { ColorPalette } from '../../constants'; import { DEFAULT_PROPS } from '.'; export function setup({ component: Badge, components: { Icon, Thumbnail, FlexBox }, decorators: { withCombinations }, }: SetupStoriesOptions<{ decorators: 'withCombinations'; components: { Icon: any; Thumbnail: any; FlexBox: any }; }>) { // Define meta and each story as individual consts const meta = { component: Badge, render: (args: any) => <Badge {...args} />, argTypes: { color: colorArgType, }, args: DEFAULT_PROPS, }; /** Using badge with text children */ const WithText = { // JSX in render, NOT in args render: (args: any) => ( <Badge {...args}> <span>30</span> </Badge> ), }; /** With icon child — uses Icon from injected components */ const WithIcon = { render: (args: any) => ( <Badge {...args}> <Icon icon={mdiHeart} /> </Badge> ), }; /** All color combinations — composes other stories' renders */ const AllColors = { render: (args: any) => ( <FlexBox orientation="vertical" gap="regular"> {WithText.render(args)} {WithIcon.render(args)} </FlexBox> ), argTypes: { color: { control: false } }, decorators: [ withCombinations({ combinations: { // Only serializable data in combinations — NO JSX cols: { key: 'color', options: withUndefined(ColorPalette) }, }, }), ], }; // Return flat object with all consts return { meta, WithText, WithIcon, AllColors }; } ```More examples of the pattern:
When a story just needs different args (no JSX), it doesn't need a custom render:
```tsx /** Story with only serializable args — inherits meta render */ const Disabled = { args: { isDisabled: true }, }; ```When multiple stories share the same render:
```tsx /** Text with inline icons — destructure children to prevent leaking from meta.args */ const WithIcon = { render: ({ children, ...args }: any) => ( <Text {...args}> Some text <Icon icon={mdiHeart} /> with icons <Icon icon={mdiEarth} /> </Text> ), }; /** All typographies — reuses WithIcon's render */ const AllTypography = { render: WithIcon.render, argTypes: { typography: { control: false } }, decorators: [ withCombinations({ combinations: { rows: { key: 'typography', options: withUndefined(ALL_TYPOGRAPHY) }, }, }), ], }; ```When a component has slot-like props (before, after, badge), put the JSX in render, not args:
```tsx const DefaultRender = render || ((args: any) => <Toolbar {...args} />); /** Toolbar with all content areas */ const WithAll = { render: () => ( <DefaultRender before={<Icon icon={mdiMenu} />} after={<Icon icon={mdiMagnify} />} label="Page title" /> ), }; ```- File extension is
Step 2: Implement React Stories
Update React stories to use core setup:
Import
setupfrom core storiesPass framework components via
componentsand decorators viadecoratorsExport each story as a thin re-export:
export const StoryName = { ...stories.StoryName };Add framework-only stories (using components not available in core) as separate exports
Pattern:
import { Badge, Icon } from '@lumx/react'; import { withCombinations } from '@lumx/react/stories/decorators/withCombinations'; import { setup } from '@lumx/core/js/components/Badge/Stories'; const { meta, ...stories } = setup({ component: Badge, components: { Icon }, decorators: { withCombinations }, }); export default { title: 'LumX components/badge/Badge', ...meta, }; export const WithText = { ...stories.WithText }; export const WithIcon = { ...stories.WithIcon }; export const AllColors = { ...stories.AllColors }; // Framework-only story (uses components not in core setup) export const WithThumbnail = { args: { children: <Thumbnail ... />, }, };
🛑 MANDATORY Validation Checkpoint 2a - STOP HERE:
- Run
yarn type-checkto verify TypeScript compilation - STOP AND USE AskUserQuestion tool to ask developer for validation:
- Present type-check status
- Ask: "React stories migrated. Please verify ALL React stories render correctly in Storybook. Should I proceed to create Vue stories?"
- Options: "Yes, proceed to Vue stories" / "No, fix issues first"
- DO NOT PROCEED to Vue stories until developer selects "Yes, proceed to Vue stories"
- If developer selects "No", fix issues and ask again
Step 3: Implement Vue Stories
Create Vue stories as thin wrapper (
.tsxfile):- Import
setupfrom core stories - Pass Vue framework components via
componentsand Vue decorators viadecorators - Export each story as a thin re-export — identical structure to React
- No
.vuetemplate files needed — all rendering handled by JSX in core - IMPORTANT: Provide a
renderoverride when the Vue component uses slots instead of props. Since core stories now put JSX in render functions (not args), the render override maps slot-like props to Vue slots when the corerenderpasses them as JSX props/children.
Pattern A — Default slot only (e.g.,
childrenorlabelprop → default slot):```tsx import { Flag } from '@lumx/vue'; import { setup } from '@lumx/core/js/components/Flag/Stories'; const { meta, ...stories } = setup({ component: Flag, // Destructure `label` out of args and pass it as default slot (JSX children) render: ({ label, ...args }: any) => <Flag {...args}>{label}</Flag>, decorators: { /* ... */ }, }); ```Pattern B — Named slots (e.g.,
before,after,label→ named slots):```tsx import { Toolbar, Icon } from '@lumx/vue'; import { setup } from '@lumx/core/js/components/Toolbar/Stories'; const { meta, ...stories } = setup({ component: Toolbar, components: { Icon }, // Map props to Vue named slots using Vue JSX slot object syntax render: ({ label, before, after, ...args }: any) => ( <Toolbar {...args}> {{ default: label ? () => label : undefined, before: before ? () => before : undefined, after: after ? () => after : undefined, }} </Toolbar> ), }); ```Pattern C — No slot mapping needed (core render handles everything):
When the core stories already handle all JSX in their own
renderfunctions (the new default pattern), and the Vue component doesn't need slot mapping because the core render directly renders the component with children via JSX, norenderoverride is needed:```tsx import { Badge, FlexBox, Icon, Thumbnail } from '@lumx/vue'; import { withCombinations } from '@lumx/vue/stories/decorators/withCombinations'; import { setup } from '@lumx/core/js/components/Badge/Stories'; const { meta, ...stories } = setup({ component: Badge, components: { Icon, Thumbnail, FlexBox }, decorators: { withCombinations }, // No render override needed — core stories already define render per story }); ```How to decide which pattern to use:
- Check the core
Stories.tsx— if stories define their ownrenderfunctions that directly render the component with JSX children (the new pattern), Vue usually doesn't need a render override (Pattern C) - If the core stories use a shared
meta.renderthat receives slot-like content via args (e.g.,label,before,after), the Vue side needs to map those to slots (Pattern A or B) - Read the Vue component's
.tsxfile and check if it accessesslots(e.g.,slots.default?.(),slots.before?.()) - If it uses
slots.default?.()only → use Pattern A - If it uses named slots (e.g.,
slots.before?.(),slots.after?.()) → use Pattern B - If core stories already handle rendering inline (each story has its own render) → use Pattern C
Full example with re-exports:
```tsx import { Badge, FlexBox, Icon, Thumbnail } from '@lumx/vue'; import { withCombinations } from '@lumx/vue/stories/decorators/withCombinations'; import { setup } from '@lumx/core/js/components/Badge/Stories'; const { meta, ...stories } = setup({ component: Badge, components: { Icon, Thumbnail, FlexBox }, decorators: { withCombinations }, }); export default { title: 'LumX components/badge/Badge', ...meta, }; export const WithText = { ...stories.WithText }; export const WithIcon = { ...stories.WithIcon }; export const AllColors = { ...stories.AllColors }; ```- Import
🛑 MANDATORY Validation Checkpoint 2b - STOP HERE:
- Run
yarn testto ensure no regressions - Run
yarn type-checkto verify TypeScript compilation - STOP AND USE AskUserQuestion tool to ask developer for validation:
- Present test and type-check results
- Ask: "All Vue stories complete. Please verify ALL Vue stories render correctly in Storybook. Should I proceed to Phase 3 (Tests Migration)?"
- Options: "Yes, proceed to Phase 3" / "No, fix issues first"
- DO NOT PROCEED to Phase 3 until developer selects "Yes, proceed to Phase 3"
- If developer selects "No", fix issues and ask again
Phase 3: Tests Migration
Goal: Extract core tests and update framework-specific test suites.
IMPORTANT RULES:
- NO JSX ELEMENTS or component calls in core tests - Use plain data only
- NO interaction/event tests in core - Core tests should only test rendering, props, and DOM structure
- Event handler tests belong in React/Vue - Test
onClickinteractions in React tests, testemit('click')in Vue tests - Tests that need component children must use framework-specific setup - Don't migrate those to core
- Vue tests should mimic React tests - Include the same structure: core tests import, framework-specific describe block, and
commonTestsSuiteVTL(Vue) orcommonTestsSuiteRTL(React) - DO NOT add NOTE comments or explanatory comments in generated files - Keep code clean without meta-commentary
IMPORTANT - Check for Existing Core Tests:
- If Phase 0 discovered that
Tests.tsalready exists in core, skip step 2 and proceed to step 3 (React tests update) - If any modifications to existing core tests are needed:
- ALWAYS use AskUserQuestion tool BEFORE making any changes
- Present proposed changes with clear rationale
- Wait for user approval before modifying core Tests.ts
Read and analyze existing React tests:
- Identify tests that use plain data (strings, numbers, etc.) - these can migrate to core
- Identify tests that use JSX components (Icon, Thumbnail, etc.) - these stay in React/Vue only
- Document which tests cannot be migrated due to component dependencies
Create core tests (
packages/lumx-core/src/js/components/<ComponentName>/Tests.ts):NO JSX ELEMENTS or component calls allowed - Use plain data only
Export
setup()function that takes props andSetupOptionsExport default test suite function that receives
SetupOptionsand contains describe/it blocksOnly migrate tests that use plain data (strings, numbers, booleans)
Follow the Button pattern exactly
Example pattern:
import { getByClassName } from '../../../testing/queries'; import { SetupOptions } from '../../../testing'; import { ColorPalette } from '../../constants'; const CLASSNAME = 'lumx-badge'; /** * Mounts the component and returns common DOM elements / data needed in multiple tests further down. */ export const setup = (propsOverride: any = {}, { render, ...options }: SetupOptions<any>) => { const props = { ...propsOverride }; const wrapper = render(props, options); const badge = getByClassName(document.body, CLASSNAME); return { props, badge, wrapper }; }; export default (renderOptions: SetupOptions<any>) => { const { screen } = renderOptions; describe('Badge core tests', () => { describe('Props', () => { it('should use default props', () => { const { badge } = setup({ children: '30' }, renderOptions); expect(badge.className).toContain('lumx-badge'); expect(badge.className).toContain('lumx-badge--color-primary'); expect(badge).toHaveTextContent(/30/); }); it('should render color', () => { const { badge } = setup({ children: 'Badge', color: ColorPalette.red }, renderOptions); expect(badge).toHaveClass('lumx-badge--color-red'); }); }); }); };
Update React tests:
Import default export from core tests (the test suite)
Call the test suite with
{ render, screen }optionsKeep React-specific tests (ref forwarding, theme context, JSX children)
Keep
commonTestsSuiteRTL(React-specific)Example pattern:
import { commonTestsSuiteRTL } from '@lumx/react/testing/utils'; import { getByClassName } from '@lumx/react/testing/utils/queries'; import { render, screen } from '@testing-library/react'; import { Badge, BadgeProps } from './Badge'; import BaseBadgeTests from '@lumx/core/js/components/Badge/Tests'; const CLASSNAME = Badge.className as string; const setup = (propsOverride: Partial<BadgeProps> = {}) => { const props: BadgeProps = { children: <span>30</span>, ...propsOverride, }; render(<Badge {...props} />); const badge = getByClassName(document.body, CLASSNAME); return { badge, props }; }; describe(`<${Badge.displayName}>`, () => { // Run core tests BaseBadgeTests({ render: (props: BadgeProps) => render(<Badge {...props} />), screen, }); // React-specific tests describe('React', () => { it('should render empty children', () => { const { badge } = setup({ children: null }); expect(badge).toBeInTheDocument(); expect(badge).toBeEmptyDOMElement(); }); }); // Common tests suite commonTestsSuiteRTL(setup, { baseClassName: CLASSNAME, forwardClassName: 'badge', forwardAttributes: 'badge', forwardRef: 'badge', }); });
Create Vue tests (
packages/lumx-vue/src/components/<component-name>/<Component>.test.ts):IMPORTANT: Vue tests should mimic React tests exactly - Same structure with core tests, framework describe, and commonTestsSuite
Import default export from core tests (the test suite)
Import and use the core
setupfunctionCall the test suite with render function that converts
childrento slotsCreate a local setup function that wraps the core setup
Add
commonTestsSuiteVTL(Vue equivalent of React'scommonTestsSuiteRTL)Add Vue-specific tests (emit events, disabled states) if needed
Use
@testing-library/vueExample pattern:
import { render, screen } from '@testing-library/vue'; import BaseBadgeTests, { setup } from '@lumx/core/js/components/Badge/Tests'; import { CLASSNAME } from '@lumx/core/js/components/Badge'; import { commonTestsSuiteVTL, SetupRenderOptions } from '@lumx/vue/testing'; import { Badge } from '.'; describe('<Badge />', () => { const renderBadge = ({ children, ...props }: any, options?: SetupRenderOptions<any>) => render(Badge, { ...options, props, slots: children ? { default: children } : undefined, }); // Run core tests BaseBadgeTests({ render: renderBadge, screen, }); const setupBadge = (props: any = {}, options: SetupRenderOptions<any> = {}) => setup(props, { ...options, render: renderBadge, screen }); // Common tests suite commonTestsSuiteVTL(setupBadge, { baseClassName: CLASSNAME, forwardClassName: 'div', forwardAttributes: 'div', forwardRef: 'div', }); });
🛑 MANDATORY Validation Checkpoint 3 (Tests) - STOP HERE:
- Run
yarn testto ensure all tests pass - Run
yarn type-checkto verify TypeScript compilation - Verify core tests use only plain data (no JSX)
- Verify framework-specific tests remain in React/Vue
- STOP AND USE AskUserQuestion tool to ask developer for validation:
- Present test results (number of tests passing)
- Ask: "Phase 3 complete. All tests migrated and passing. Should I proceed to Phase 4 (Update CHANGELOG and verify builds)?"
- Options: "Yes, proceed to finalization" / "No, fix issues first"
- DO NOT PROCEED to Phase 4 until developer selects "Yes, proceed to finalization"
- If developer selects "No", fix issues and ask again
Important Notes:
- Tests with framework-specific rendering behavior (e.g., empty children) should stay in framework test files
- Vue uses slots for children, so the render helper must convert
childrenprop toslots.default - React renders empty for
nullchildren, Vue renders comment nodes<!----> - Vue tests should include
commonTestsSuiteVTLto match React'scommonTestsSuiteRTLstructure - Core
setup()function should return aliases if needed (e.g.,const div = badge;) forcommonTestsSuitecompatibility
Phase 4: Update Package Exports
Verify React package already exports component
Phase 5: Update CHANGELOG
IMPORTANT: Complete this phase ONCE for the entire component family after all components are migrated.
Add entry under [Unreleased]:
For single component:
### Added
- `@lumx/vue`:
- Create the `<Component>` component
### Changed
- `@lumx/core`:
- Moved `<Component>` from `@lumx/react`
For component family:
### Added
- `@lumx/vue`:
- Create the `<Component>` component family (`<Component>`, `<SubComponent1>`, `<SubComponent2>`, etc.)
### Changed
- `@lumx/core`:
- Moved `<Component>` component family from `@lumx/react` (`<Component>`, `<SubComponent1>`, `<SubComponent2>`, etc.)
Example for Table:
### Added
- `@lumx/vue`:
- Create the `Table` component family (`Table`, `TableBody`, `TableCell`, `TableHeader`, `TableRow`)
### Changed
- `@lumx/core`:
- Moved `Table` component family from `@lumx/react` (`Table`, `TableBody`, `TableCell`, `TableHeader`, `TableRow`)
Phase 6: Final Build Verification
IMPORTANT: After completing Phases 1-5 for ALL components in the family, perform final verification.
Build packages:
yarn build:core yarn build:react yarn build:vueFinal smoke test:
- Run full test suite:
yarn test - Verify all builds succeed
- Check Storybook for any console errors
- Verify all components in the family work together correctly
- Run full test suite:
Validate React/Vue parity:
CRITICAL: Check that all tests and stories are properly matched between React and Vue
For each component in the family:
Stories validation:
- Verify every
.stories.tsxfile in React has a corresponding.stories.tsxfile in Vue - Read both story files and compare exported story names
- Ensure all React stories have Vue equivalents (e.g., Default, WithHeader, AllStates)
- Both should be thin wrappers calling the same core
setup()with identical structure - Example check:
# React stories ls packages/lumx-react/src/components/<component-name>/*.stories.tsx # Vue stories ls packages/lumx-vue/src/components/<component-name>/*.stories.tsx
Tests validation:
- Verify every
.test.tsxfile in React has a corresponding.test.tsfile in Vue - Read test files and compare test structure:
- Core tests are imported and run in both React and Vue
- Framework-specific
describeblocks exist in both (React/Vue) commonTestsSuiteRTL(React) has equivalentcommonTestsSuiteVTL(Vue)- React-specific tests have Vue-specific equivalents where appropriate
- Check `commonTests
- Verify every
…(truncated)