@nanostores/vue
Version:
Vue integration for Nano Stores, a tiny state manager with many atomic tree-shakable stores
472 lines (430 loc) • 11 kB
JavaScript
/* eslint-disable perfectionist/sort-objects */
import { buildCreatorLogger, buildLogger } from '@nanostores/logger'
import { setupDevtoolsPlugin } from '@vue/devtools-api'
import { onMount, onSet } from 'nanostores'
const LAYER_ID = 'nanostores'
const INSPECTOR_ID = 'nanostores'
const PLUGIN_CONFIG = {
componentStateTypes: ['Nano Stores'],
enableEarlyProxy: true,
homepage: 'https://github.com/nanostores',
id: 'io.github.nanostores',
label: 'Nano Stores',
logo: 'https://nanostores.github.io/nanostores/logo.svg',
packageName: '@nanostores/vue',
settings: {
keepUnmounted: {
defaultValue: false,
label: 'Keep unmounted',
type: 'boolean'
},
realtime: {
defaultValue: true,
label: 'Real-time update detection',
type: 'boolean'
},
reduceDataUsage: {
defaultValue: true,
label: 'Reduce data usage',
type: 'boolean'
}
}
}
const TAGS = {
creator: {
backgroundColor: 0xbb5100,
label: 'creator',
textColor: 0xffffff
},
nanostores: {
backgroundColor: 0x000000,
label: 'nano stores',
textColor: 0xffffff
},
unmounted: {
backgroundColor: 0x5e5e5e,
label: 'unmounted',
textColor: 0xffffff
}
}
let lastNodeId = 0
let lastGroupId = 0
const getNodeId = () => (lastNodeId += 1)
const getGroupId = () => (lastGroupId += 1)
function find(target, text) {
return target.some(item =>
item.toString().toLowerCase().includes(text.toLowerCase())
)
}
function isAtom(store) {
return store.setKey === undefined
}
function isCreator(store) {
return 'cache' in store && 'build' in store
}
function notifyComponentUpdate(api, ...args) {
let last = 0
let now = api.now()
if (now - last > 1000) {
api.notifyComponentUpdate(...args)
last = now
}
}
/**
* @param data Can include `app`, `inspectorId`, `nodeId` and `type`.
*/
function isValidPayload(payload, data) {
for (let [key, value] of Object.entries(data)) {
if (payload[key] !== value) return false
}
return true
}
function createLogger(
api,
app,
store,
storeName,
inspectorNode,
opts,
groupId
) {
buildLogger(
store,
storeName,
{
change: ({ actionId, changed, newValue, oldValue, valueMessage }) => {
let data = {
changed,
valueMessage,
newValue,
oldValue
}
if (valueMessage === undefined) delete data.valueMessage
if (changed === undefined) delete data.changed
let subtitle = 'was changed'
if (changed) subtitle += ` in the ${changed} key`
api.addTimelineEvent({
event: {
data,
groupId: actionId || lastGroupId,
subtitle,
time: api.now(),
title: storeName
},
layerId: LAYER_ID
})
api.sendInspectorState(INSPECTOR_ID)
},
mount: () => {
let data = {
event: 'mount',
store,
storeName
}
if (api.getSettings().reduceDataUsage) {
delete data.store
}
api.addTimelineEvent({
event: {
data,
groupId: groupId || getGroupId(),
subtitle: 'was mounted',
time: api.now(),
title: storeName
},
layerId: LAYER_ID
})
},
unmount: () => {
let data = {
event: 'unmount',
store,
storeName
}
if (api.getSettings().reduceDataUsage) {
delete data.store
}
api.addTimelineEvent({
event: {
data,
groupId: lastGroupId,
subtitle: 'was unmounted',
time: api.now(),
title: storeName
},
layerId: LAYER_ID
})
}
},
opts
)
api.on.getInspectorState(payload => {
if (
!isValidPayload(payload, {
app,
inspectorId: INSPECTOR_ID,
nodeId: inspectorNode.id
})
) {
return
}
payload.state = {
State: isAtom(store)
? [
{
editable: true,
key: 'value',
value: store.value
}
]
: Object.entries(store.value).map(([key, value]) => ({
editable: true,
key,
value
})),
Store: {
active: store.active,
listeners: store.lc
}
}
})
api.on.editInspectorState(payload => {
if (
!isValidPayload(payload, {
app,
inspectorId: INSPECTOR_ID,
nodeId: inspectorNode.id
})
) {
return
}
let { path, state } = payload
if (isAtom(store)) {
store.set(state.value)
} else {
store.setKey(path[0], state.value)
}
})
}
function createCreatorLogger(
api,
app,
creator,
creatorName,
inspectorNode,
opts
) {
inspectorNode.tags.push(TAGS.creator)
buildCreatorLogger(
creator,
creatorName,
{
build: ({ store, storeName }) => {
let childId = `${creatorName}:${store.value.id}`
let childNode = inspectorNode.children.find(i => i.id === childId)
if (childNode) {
childNode.tags = []
} else {
childNode = {
children: [],
id: childId,
label: storeName,
tags: []
}
inspectorNode.children.push(childNode)
}
let groupId = getGroupId()
let data = {
event: 'build',
storeName,
store,
by: creatorName
}
if (api.getSettings().reduceDataUsage) {
delete data.store
}
api.addTimelineEvent({
event: {
data,
groupId,
subtitle: `was built by ${creatorName}`,
time: api.now(),
title: storeName
},
layerId: LAYER_ID
})
api.sendInspectorTree(INSPECTOR_ID)
createLogger(api, app, store, storeName, childNode, opts, groupId)
onMount(store, () => {
return () => {
if (api.getSettings().keepUnmounted) {
childNode.tags.push(TAGS.unmounted)
} else {
let index = inspectorNode.children.findIndex(
i => i.id === childId
)
index > -1 && inspectorNode.children.splice(index, 1)
}
api.sendInspectorTree(INSPECTOR_ID)
}
})
}
},
opts
)
api.on.getInspectorState(payload => {
if (
!isValidPayload(payload, {
app,
inspectorId: INSPECTOR_ID,
nodeId: inspectorNode.id
})
) {
return
}
let { reduceDataUsage } = api.getSettings()
payload.state = {
Cache: creator.cache
}
if (reduceDataUsage) {
payload.state.Cache = {
keys: Object.keys(creator.cache)
}
}
opts.getInspectorState &&
opts.getInspectorState(payload, creator, {
reduceDataUsage
})
})
}
export function devtools(app, storesOrCreators, opts = {}) {
let inspectorTree = []
let api = null
setupDevtoolsPlugin({ ...PLUGIN_CONFIG, app }, _api => {
api = _api
if (storesOrCreators) {
for (let [storeName, store] of Object.entries(storesOrCreators)) {
let inspectorNode = {
children: [],
id: getNodeId(),
label: storeName,
tags: []
}
inspectorTree.push(inspectorNode)
api.sendInspectorTree(INSPECTOR_ID)
if (isCreator(store)) {
createCreatorLogger(api, app, store, storeName, inspectorNode, opts)
} else {
createLogger(api, app, store, storeName, inspectorNode, opts)
}
}
}
api.addTimelineLayer({
color: 0x0059d1,
id: LAYER_ID,
label: PLUGIN_CONFIG.label
})
api.addInspector({
icon: 'storage',
id: INSPECTOR_ID,
label: PLUGIN_CONFIG.label,
treeFilterPlaceholder: 'Search for stores by id, label or tag'
})
api.on.visitComponentTree(payload => {
if (!isValidPayload(payload, { app })) return
let proxy = payload.componentInstance?.proxy
if (proxy && '_nanostores' in proxy) {
payload.treeNode.tags.push(TAGS.nanostores)
}
})
api.on.getInspectorTree(payload => {
if (!isValidPayload(payload, { app, inspectorId: INSPECTOR_ID })) return
payload.rootNodes = payload.filter
? inspectorTree.filter(node => {
let target = [
node.id,
node.label,
...node.tags.map(tag => tag.label)
]
let found = find(target, payload.filter)
let children
if (node.children) {
children = node.children.some(childNode =>
find(
[
childNode.id,
childNode.label,
...childNode.tags.map(tag => tag.label)
],
payload.filter
)
)
}
return found || children
})
: inspectorTree
})
let unbindSet = []
api.on.inspectComponent(payload => {
if (!isValidPayload(payload, { app })) return
let { realtime } = api.getSettings()
if (unbindSet.length > 0) {
for (let unbind of unbindSet) unbind()
unbindSet = []
}
let instanceStores = payload.componentInstance.proxy._nanostores || []
instanceStores.forEach((store, index) => {
if (realtime) {
unbindSet.push(
onSet(store, () => {
notifyComponentUpdate(api, payload.componentInstance)
})
)
}
payload.instanceData.state.push({
editable: true,
key: index.toString(),
type: PLUGIN_CONFIG.componentStateTypes[0],
value: store.value
})
})
})
api.on.editComponentState(payload => {
if (
!isValidPayload(payload, {
app,
type: PLUGIN_CONFIG.componentStateTypes[0]
})
) {
return
}
let {
componentInstance,
path: [index, key],
state: { newKey, remove, value }
} = payload
let store = componentInstance.proxy._nanostores[index]
if (isAtom(store)) {
let oldValue = store.value
let newValue = value
if (key) {
if (Array.isArray(oldValue)) {
newValue = oldValue
newValue[key] = value
} else {
newValue = { ...oldValue, [key]: value }
}
}
store.set(newValue)
} else {
if (payload.path.length > 2) return
if (remove) store.setKey(key)
if (newKey) {
store.setKey(newKey, value)
} else {
store.setKey(key, value)
}
}
})
})
}