feat: add observable naming, creation hook, and babel auto-naming transform#645
feat: add observable naming, creation hook, and babel auto-naming transform#645DorianMazur wants to merge 1 commit intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds devtools-facing infrastructure to improve discoverability of observables at runtime and provide stable names for debugging, including compiler assistance via the Babel transform.
Changes:
- Added optional
{ name }options toobservable/observablePrimitive, storing_nameon the root node. - Added a global
onObservableCreatedhook (and internal notifier) fired synchronously on observable creation. - Extended the Babel plugin to auto-inject
{ name: '<varName>' }into observable factory calls, plus tests for naming + creation hooks.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/devtools.test.ts | New tests covering _name, creation hook behavior, and path derivation via parent/key chain. |
| tests/babel.test.ts | New Babel plugin tests verifying auto-name injection behavior and non-target cases. |
| src/observableInterfaces.ts | Adds ObservableOptions and _name?: string to node info for naming support. |
| src/observable.ts | Threads optional ObservableOptions into observable factory functions. |
| src/middleware.ts | Adds global onObservableCreated subscription + notification implementation. |
| src/createObservable.ts | Sets _name from options and calls notifyObservableCreated after creation. |
| src/babel/index.ts | Implements auto-name injection for observable / observablePrimitive based on import + variable name. |
| index.ts | Exposes onObservableCreated via internal export for downstream integrations. |
Comments suppressed due to low confidence (1)
src/babel/index.ts:33
- In the
@legendapp/state/reactimport scan, this loop assumes every specifier is an ImportSpecifier (accessesspecifiers[i].imported.name). Default or namespace imports would makeimportedundefined and can crash the Babel plugin. Please guard onspec.type === 'ImportSpecifier'before readingspec.imported.name(similar to the observable import handling below).
if (source === '@legendapp/state/react') {
const specifiers = path.node.specifiers;
for (let i = 0; i < specifiers.length; i++) {
const s = specifiers[i].imported.name;
if (!hasLegendImport && (s === 'Computed' || s === 'Memo' || s === 'Show')) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| if (args.length === 0) { | ||
| // observable() -> observable(undefined, { name: '...' }) | ||
| args.push(identifier('undefined')); | ||
| args.push(nameOption); |
There was a problem hiding this comment.
For the zero-arg call case, the transform injects an Identifier named undefined. Since undefined can be shadowed by a local binding, this can change runtime semantics in some files. Prefer generating an unshadowable undefined value (e.g., void 0) instead of identifier('undefined').
| for (const handler of creationHandlers) { | ||
| try { | ||
| handler(node); | ||
| } catch (error) { | ||
| console.error('Error in onObservableCreated handler:', error); | ||
| } | ||
| } |
There was a problem hiding this comment.
notifyObservableCreated iterates directly over creationHandlers. If handlers are added/removed during notification (including a handler unsubscribing itself), iteration order/coverage can become surprising. Consider iterating over a snapshot (e.g., Array.from(creationHandlers)) similar to how middleware events snapshot handler sets later in this file.
Add devtools infrastructure: observable naming + creation tracking
This adds the core pieces needed to build a devtools plugin (e.g. for rozenite).
What changed:
observable()andobservablePrimitive()now accept an optional second arg{ name: 'myStore' }which sets_nameon the root nodeonObservableCreated(handler)global hook in middleware.ts, fires synchronously when any observable is created, zero cost when nothing is subscribedconst foo = observable({})becomesconst foo = observable({}, { name: 'foo' })at compile time_namefield added toBaseNodeInfo, child path is derivable by walkingparent/keychainWhy:
To build a legend-state devtools plugin, you need to be able to discover observables as they're created and show meaningful names for them. Without compiler support, observables are anonymous. The babel transform solves this automatically.