Migrate WikiLambda Vue Frontend to Composition API so that the codebase is more maintainable, modern, and easier to extend with improved type safety and logic reuse. Also it is in line with how Codex uses Vue 3.
Acceptance Criteria
- All Vue components use the Composition API in their <script> blocks.
- Shared logic previously in mixins is refactored into composable functions in the composables/ directory.
- Pinia stores might initially not need any updates, because they should work with both Composition and Options API. We do need to update the usage of the store in the components.
- The application builds and runs without errors or warnings.
- All existing tests pass.
- No linting errors
- Documentation is updated to reflect the new patterns and best practices.
- (not possible) All JavaScript and Vue files use ES module syntax (import/export) instead of CommonJS (require/module.exports).
📖 Migration Guide
Migration types
Composables (9 available)
// In your component setup(): const { proxy } = getCurrentInstance(); // Error handling const errors = useError( { keyPath: props.keyPath } ); // Clipboard operations const clipboard = useClipboard( ); // Event logging const eventLog = useEventLog(); // Type utilities const typeUtils = useType(); // ZObject access const zobject = useZObject( { keyPath: props.KeyPath } ); // Page title management const pageTitle = usePageTitle(); // Scroll behavior const scroll = useScroll(); // Metadata configuration const metadata = useMetadata();
Stores
All stores accessible through useMainStore():
Per Pinia docs, use storeToRefs() to destructure state and getters while maintaining reactivity:
Key Rules
- Destructure getters/state with storeToRefs() - Maintains reactivity
- Destructure actions directly from store - They're just functions, no reactivity needed
- Or access everything via store.property - Simpler, always works
Why? Pinia getters and state are reactive refs. Direct destructuring without storeToRefs() breaks reactivity. Actions are plain functions and can be destructured directly.
setup( props, { emit } ) { const store = useMainStore(); // ✅ CORRECT: Use storeToRefs when destructuring getters/state const { isInitialized, getCurrentView } = storeToRefs( store ); // ✅ CORRECT: Actions can be destructured directly (they're just functions) const { setValueByKeyPath, fetchZids } = store; // ✅ CORRECT: Access store directly in computed/methods const displayValue = computed( () => store.getLabelData( someId ) ); function handleClick() { setValueByKeyPath( value ); // ✅ Destructured action store.fetchZids( zids ); // ✅ Or call directly } return { // Reactive refs from storeToRefs isInitialized, getCurrentView, // Other data displayValue, handleClick }; }
Components
Key Rules
✅ DO:
- Use single <script> tag
- Use setup( props, { emit } ) inside defineComponent()
- Access props via props.propName
- Use emit() for events
- Use inject( 'i18n' ) for internationalization (MediaWiki provides i18n via app.provide)
- Access store via store.property (e.g., store.getLabelData, store.setValueByKeyPath())
- Use storeToRefs() ONLY for getters returned by reference in the return statement
- Explicitly return all values/methods template needs
- Destructure only needed methods from composables
❌ DON'T:
- Use two <script> tags
- Use spread operators in returns
- Use this (use props/emit instead)
- Use getCurrentInstance() for i18n (use inject( 'i18n' ) instead)
- Destructure from store (can cause reactivity issues and test complications)
- Return everything (only what template needs)
vue
<script>
const { computed, defineComponent, inject, ref } = require( 'vue' );
const useZObject = require( '../../composables/useZObject.js' );
const useMainStore = require( '../../store/index.js' );
module.exports = exports = defineComponent( {
name: 'my-component',
components: {
'child-component': ChildComponent
},
props: {
keyPath: {
type: String,
required: true
},
objectValue: {
type: Object,
required: true
}
},
emits: [ 'update-value' ],
setup( props, { emit } ) {
// 1. Inject i18n (provided by MediaWiki)
const i18n = inject( 'i18n' );
// 2. Use composables (destructure only what's needed)
const { getZObjectType, getZBooleanValue } = useZObject();
// 3. Access store (DO NOT destructure - use store.property)
const store = useMainStore();
// 4. Reactive state
const localState = ref( 'initial' );
// 5. Computed properties
const displayValue = computed( () => getZObjectType( props.objectValue ) );
// 6. Methods
function handleUpdate( value ) {
store.setValueByKeyPath( value ); // Use store.action()
emit( 'update-value', value );
}
// 7. MUST explicitly return everything the template uses
return {
// Only return what template actually uses
getZObjectType,
displayValue,
handleUpdate,
// Expose store getters/actions used in template
getLabelData: store.getLabelData
};
}
} );
</script>Common Conversions
| From (Options API) | To (Composition API) |
|---|---|
| data() { return { x: 0 } } | const x = ref( 0 ) |
| computed: { y() { return this.x * 2 } } | const y = computed( () => x.value * 2 ) |
| methods: { doIt() {} } | function doIt() {} |
| mounted() {} | onMounted( () => {} ) |
| this.x | x.value (for refs) |
| this.$i18n | proxy.$i18n (via getCurrentInstance) |
| mapState( store, ['x'] ) | const { x } = storeToRefs( store ) |
| mapActions( store, ['doIt'] ) | const { doIt } = store |
| mixins: [ myMixin ] | const mixin = useMyComposable() |
🗺️ Conversion Roadmap 🤖
Phase 1: Mixins → Composables (8/8) ✅
All mixins should be converted to composables in resources/ext.wikilambda.app/composables/:
| Old Mixin | New Composable | Purpose |
|---|---|---|
| clipboardMixin.js | useClipboard.js | Clipboard operations |
| errorMixin.js | useError.js | Error handling |
| eventLogMixin.js | useEventLog.js | Event logging |
| metadataMixin.js | useMetadata.js | Metadata configuration |
| pageTitleMixin.js | usePageTitle.js | Page title management |
| scrollMixin.js | useScroll.js | Scroll behavior |
| typeMixin.js | useType.js | Type utilities |
| zobjectMixin.js | useZObject.js | ZObject access |
Est. Total: 1-2 days
Phase 2. Pinia Store Modules → Import to Composition API
The stores import should be converted to Composition api style using useMainStore and setup()
Phase 3: Component Migration
- Convert all Vue components to Composition API
- Update component tests
Batch Convert ALL Components
- Week 1: Base + Widget components (~27 components, Batches 1-2)
- Days 1-2: Base components (12) - Foundation for all others
- Days 3-5: Widget components (15) - UI controls
- Week 2: Function + Specialized + View components (~56 components, Batches 3-5)
- Days 1-3: Function components (20) - Core functionality
- Days 4-5: Specialized + View components (36) - Final push
AI Workflow:
- Use AI to analyze existing converted components as patterns
- Manual review and testing of critical paths
Est. Total: 10-20 days (2/4 weeks)
Phase 4: Final Cleanup, Testing, quality and cleanup
- Full integration testing (~1 day)
- Performance optimization & bug fixes (~1 day)
- Documentation updates (ongoing)
- Update all Jest tests that are still broken
- Fix linting errors
- Run full test suite
- Update documentation [TODO]
Est. Total: 3-5 days
Learnings and Follow-up work
Learnings
- Composition API groups logic by feature, not by option type. We did not do this for the sake of review but we should do this as follow-up work.
- Composition API integrates naturally with TypeScript because everything is just JavaScript functions and variables. Is it possible for us to consider this?
- Ran into a lot of nextTick usage which is usually not needed. removed these
- Some html elements do not have the correct semantic html element (like using an a for a button), we should perhaps go through the codebase and fix those occasions. Perhaps also do an accessibility check for these.
- For unit testing, we want to place greater emphasis on user-focused behavior testing, ensuring our components work the way users actually interact with them.
Follow-up work
- Start grouping logic in Vue by feature: https://phabricator.wikimedia.org/T410497
- Consider using Typescript if allowed by RL/MW : Will be discussed in the team
- Check for elements that are not using the correct semantic html element: https://phabricator.wikimedia.org/T410498