Repository navigation
Description
Goal: let a widget declare idAliases, so that operations whose relatedId names a deprecated widget id keep applying after the widget is renamed. This is the widget-side mirror of the slot idAliases added in #178 (from #65).
Why: a widget id is a targeting surface. Five of the seven WidgetOperationTypes take a relatedId (INSERT_AFTER, INSERT_BEFORE, REPLACE, REMOVE, OPTIONS); only APPEND and PREPEND do not. When the relatedId matches no widget, findRelatedWidgetIndex returns null and the operation is a silent no-op, so renaming a widget id breaks every site config that targets it, with nothing to see. ADR 0016 already treats default widget ids as public surface ("All existing inner slot ids and default widget ids are preserved"), and ADR 0009 gives slots a versioning and deprecation path, but widgets have neither a deprecation path nor an alias. No combination of existing operations substitutes: a widget carries one id, and an app's own operations run before an operator's, so the app cannot sequence a compatibility operation after the operator's REMOVE or REPLACE.
Concrete case: openedx/frontend-app-learner-dashboard#878 renames the dashboard header links from Courses and Discover New to My Learning and Explore. The widget ids org.openedx.frontend.widget.learnerDashboard.headerLinkCourses.v1 and ...headerLinkDiscover.v1 shipped in frontend-app-learner-dashboard 1.0.0, so renaming them to match is a breaking change for operators who hide, replace or reorder around those links. With widget aliases the app renames the ids and keeps the old ones as aliases, the way a slot can today.
Note
The plan below was generated by Claude (Claude Code) and reviewed before posting.
Claude Plan
Written 2026-10-30 against ae5ec93 on main.
Baseline
- Slot aliases (feat: support slot ID aliases #178):
SlottakesidAliasesand puts them inSlotContext;useSlotOperationspasses[id, ...idAliases]togetSlotOperations, which matchesoperation.slotIdagainst that list. The PR touched runtime code and tests only; nothing underdocs/and no slot showcase entry mentionsidAliases. - Widget identity:
WidgetIdentityPropsis{ id, role? }, carried byAPPEND,PREPEND,INSERT_AFTER,INSERT_BEFOREandREPLACE.createIdentifiedWidget(runtime/slots/widget/utils.tsx) builds anIdentifiedWidget{ id, role?, node }and wraps the node inWidgetProvider, whoseWidgetContextcarriesslotId,widgetIdandrole. - Targeting:
findRelatedWidgetIndexmatchesrelatedIdagainstwidget.idby equality and returns the first match in list order, ornull;insertAfterWidget,insertBeforeWidget,replaceWidgetandremoveWidgetdo nothing onnull.useWidgetOptionsForId(slotId, widgetId)(runtime/slots/widget/hooks.ts) keepsOPTIONSoperations whoserelatedId === widgetId.useSortedWidgetOperationsorders two insert operations bya.id === b.relatedId; its guardisWidgetRelativeOperationcoversINSERT_AFTERandINSERT_BEFOREonly, soREPLACE,REMOVEandOPTIONSget no dependency ordering today.WidgetList.byIdandwithoutIdfilter byw.id === id. - Public surface:
runtime/slots/widget/index.tsre-exports its hooks, utils, types andWidgetContext, andruntime/index.tsre-exports./slots, soIdentifiedWidget,WidgetContext,useWidgetOptionsForIdandcreateWidgetsare package API. Every change below is additive: an optional field or an optional trailing parameter.
Change
- Types (
runtime/slots/widget/types.ts).idAliases?: string[]onWidgetIdentityProps, with the TSDoc that is this feature's documentation, and onIdentifiedWidget.WidgetContext(WidgetContext.tsx) gainswidgetIdAliases?: string[], named besidewidgetId. - One match helper in
runtime/slots/widget/utils.tsx, exported sohooks.tscan use it:matchesWidgetId(identity: { id: string; idAliases?: string[] }, id: string)returnsidentity.id === id || (identity.idAliases?.includes(id) ?? false). Both an operation withWidgetIdentityPropsand anIdentifiedWidgetfit the parameter structurally. createIdentifiedWidgetcopiesidAliasesfrom the operation onto the identified widget and passes it toWidgetProvideraswidgetIdAliases, whichWidgetProviderputs in the context.findRelatedWidgetIndex,byIdandwithoutIdcompare throughmatchesWidgetId. This coversinsertAfterWidget,insertBeforeWidget,replaceWidgetandremoveWidgetin one place.useWidgetOptionsForId(slotId, widgetId, widgetIdAliases = []): a third, optional parameter; anOPTIONSoperation applies whenmatchesWidgetId({ id: widgetId, idAliases: widgetIdAliases }, operation.relatedId).useWidgetOptionsreadswidgetIdAliasesfromWidgetContextand passes it through. Existing callers of the two-argument form are unchanged.useSortedWidgetOperationscompares throughmatchesWidgetId(a, b.relatedId)andmatchesWidgetId(b, a.relatedId), so an insert relative to an alias is still sorted after the operation that declares the alias. The guard stays as it is; ordering forREPLACE,REMOVEandOPTIONSis a pre-existing limit and not this change.- Precedence. When a
relatedIdmatches one widget's alias and another widget's id, the first widget in list order wins, exactly as two widgets with the same id resolve today. No ranking of id over alias. - Docs. The TSDoc on
WidgetIdentityProps.idAliases, in the style of the per-member comments onWidgetOperationTypes: other ids the widget answers to; a relative operation (INSERT_AFTER,INSERT_BEFORE,REPLACE,REMOVE,OPTIONS) whoserelatedIdnames one of them applies as if it namedid; when a widget id is renamed, keep the old id here for at least one release so site configs that target it keep working. No showcase entry (none exists for slot aliases either) and no ADR (feat: support slot ID aliases #178 added none, and this changes no decision in ADR 0009 or ADR 0016).
Not here: dependency ordering for REPLACE, REMOVE and OPTIONS in the sort; any runtime notice when an operation targets an alias.
Tests
runtime/slots/widget/utils.test.tsx, a newdescribeon aliases using the existingmakeAppendOphelper extended withidAliases: each ofINSERT_AFTER,INSERT_BEFORE,REPLACEandREMOVEconfigured against an alias applies (asserted onidentifiedids and order); against the primary id still applies; arelatedIdmatching neither id nor alias is a no-op;byIdandwithoutIdmatch an alias; precedence: a widget with idxlisted before a widget with aliasx, and the reverse, each time the earlier one is targeted;identifiedexposesidAliases.runtime/slots/widget/hooks.test.tsx(new), in the shape ofruntime/slots/layout/hooks.test.tsxwith../hooksmocked:useSortedWidgetOperationsplaces an insert relative to an alias after the operation declaring that alias, in both declaration orders;useWidgetOptionsForIdmerges options declared against an alias and against the primary id and ignores others;useWidgetOptionsreadswidgetIdAliasesfrom aWidgetContext.Providerwrapper.runtime/slots/Slot.test.tsx, beside feat: support slot ID aliases #178's site-config block: anAPPENDwidget declaringidAliases: ['old-widget']and aREMOVEwithrelatedId: 'old-widget'renders nothing from that widget.npm run typecheck,npm run lint,npm test.
Commit and PR
One commit, feat: support widget ID aliases, the sibling of #178's feat: support slot ID aliases; .releaserc makes it a minor release. Commitlint is .github/workflows/commitlint.yml calling openedx/.github/.github/workflows/commitlint.yml@master with no local config, the same chain as the learning repo. Branch widget-id-aliases on the fork, PR against main, "Resolves #334". Verification is the test suite; the first consumer check is openedx/frontend-app-learner-dashboard#878 adopting the alias for its two header link ids, outside this PR.
Activity
feat: support widget ID aliases
feat: support widget ID aliases
feat: support widget ID aliases
feat: support widget ID aliases
feat: support widget ID aliases
openedx-semantic-release-bot commentedon Nov 2, 2026
🎉 This issue has been resolved in version 2.0.0-alpha.19 🎉
The release is available on:
Your semantic-release bot 📦🚀