Skip to content

Commit

Permalink
feat: add devtools support
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Sep 23, 2020
1 parent cfde91f commit 849cb3f
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 5 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"scripts": {
"build": "rollup -c rollup.config.js",
"build:dts": "api-extractor run --local --verbose",
"size": "rollup -c size-checks/rollup.config.js && node scripts/check-size.js",
"release": "bash scripts/release.sh",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1",
"lint": "prettier -c --parser typescript \"{src,__tests__,e2e}/**/*.[jt]s?(x)\"",
Expand Down Expand Up @@ -57,6 +58,8 @@
"@rollup/plugin-replace": "^2.3.3",
"@types/jest": "^26.0.14",
"@types/node": "^14.11.2",
"@vue/devtools-api": "^6.0.0-beta.2",
"brotli": "^1.3.2",
"codecov": "^3.6.1",
"conventional-changelog-cli": "^2.1.0",
"jest": "^26.4.2",
Expand Down
24 changes: 24 additions & 0 deletions scripts/check-size.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const { gzipSync } = require('zlib')
const { compress } = require('brotli')

function checkFileSize(filePath) {
if (!fs.existsSync(filePath)) {
return
}
const file = fs.readFileSync(filePath)
const minSize = (file.length / 1024).toFixed(2) + 'kb'
const gzipped = gzipSync(file)
const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb'
const compressed = compress(file)
const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb'
console.log(
`${chalk.gray(
chalk.bold(path.basename(filePath))
)} min:${minSize} / gzip:${gzippedSize} / brotli:${compressedSize}`
)
}

checkFileSize(path.resolve(__dirname, '../size-checks/dist/small.js'))
58 changes: 58 additions & 0 deletions size-checks/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import path from 'path'
import ts from 'rollup-plugin-typescript2'
import replace from '@rollup/plugin-replace'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { terser } from 'rollup-plugin-terser'

/** @type {import('rollup').RollupOptions} */
const config = {
external: ['vue'],
output: {
file: path.resolve(__dirname, './dist/small.js'),
format: 'es',
},
input: path.resolve(__dirname, './small.js'),
plugins: [
replace({
__DEV__: false,
// this is only used during tests
__TEST__: false,
// If the build is expected to run directly in the browser (global / esm builds)
__BROWSER__: true,
// is targeting bundlers?
__BUNDLER__: false,
__GLOBAL__: false,
// is targeting Node (SSR)?
__NODE_JS__: false,
__VUE_PROD_DEVTOOLS__: false,
}),
ts({
check: false,
tsconfig: path.resolve(__dirname, '../tsconfig.json'),
cacheRoot: path.resolve(__dirname, '../node_modules/.rts2_cache'),
tsconfigOverride: {
compilerOptions: {
sourceMap: false,
declaration: false,
declarationMap: false,
},
exclude: ['__tests__', 'test-dts'],
},
}),
resolve(),
commonjs(),
terser({
format: {
comments: false,
},
module: true,
compress: {
ecma: 2015,
pure_getters: true,
},
}),
],
}

export default config
5 changes: 5 additions & 0 deletions size-checks/small.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createPinia, defineStore } from '../dist/pinia.esm-bundler'

createPinia()
// @ts-ignore
export default defineStore()
152 changes: 152 additions & 0 deletions src/devtools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
CustomInspectorNode,
CustomInspectorState,
setupDevtoolsPlugin,
} from '@vue/devtools-api'
import { App } from 'vue'
import { getRegisteredStores, registerStore } from './rootStore'
import { GenericStore, NonNullObject } from './types'

function formatDisplay(display: string) {
return {
_custom: {
display,
},
}
}

let isAlreadyInstalled: boolean | undefined

export function addDevtools(app: App, store: GenericStore, req: NonNullObject) {
registerStore(store)
setupDevtoolsPlugin(
{
id: 'pinia',
label: 'Pinia 🍍',
app,
},
(api) => {
api.on.inspectComponent((payload, ctx) => {
if (payload.instanceData) {
payload.instanceData.state.push({
type: '🍍 ' + store.id,
key: 'state',
editable: false,
value: store.state,
})
}
})

// watch(router.currentRoute, () => {
// // @ts-ignore
// api.notifyComponentUpdate()
// })

const mutationsLayerId = 'pinia:mutations'
const piniaInspectorId = 'pinia'

if (!isAlreadyInstalled) {
api.addTimelineLayer({
id: mutationsLayerId,
label: `Pinia 🍍`,
color: 0xe5df88,
})

api.addInspector({
id: piniaInspectorId,
label: 'Pinia 🍍',
icon: 'storage',
treeFilterPlaceholder: 'Search stores',
})

isAlreadyInstalled = true
} else {
// @ts-ignore
api.notifyComponentUpdate()
api.sendInspectorTree(piniaInspectorId)
api.sendInspectorState(piniaInspectorId)
}

store.subscribe((mutation, state) => {
// rootStore.state[store.id] = state
const data: Record<string, any> = {
store: formatDisplay(mutation.storeName),
type: formatDisplay(mutation.type),
}

if (mutation.payload) {
data.payload = mutation.payload
}

// @ts-ignore
api.notifyComponentUpdate()
api.sendInspectorState(piniaInspectorId)

api.addTimelineEvent({
layerId: mutationsLayerId,
event: {
time: Date.now(),
data,
// TODO: remove when fixed
meta: {},
},
})
})

api.on.getInspectorTree((payload) => {
if (payload.app === app && payload.inspectorId === piniaInspectorId) {
const stores = Array.from(getRegisteredStores())

payload.rootNodes = (payload.filter
? stores.filter((store) =>
store.id.toLowerCase().includes(payload.filter.toLowerCase())
)
: stores
).map(formatStoreForInspectorTree)
}
})

api.on.getInspectorState((payload) => {
if (payload.app === app && payload.inspectorId === piniaInspectorId) {
const stores = Array.from(getRegisteredStores())
const store = stores.find((store) => store.id === payload.nodeId)

if (store) {
payload.state = {
options: formatStoreForInspectorState(store),
}
} else {
__VUE_DEVTOOLS_TOAST__(
`🍍 store "${payload.nodeId}" not found`,
'error'
)
}
}
})

// trigger an update so it can display new registered stores
// @ts-ignore
api.notifyComponentUpdate()
__VUE_DEVTOOLS_TOAST__(`🍍 "${store.id}" store installed`)
}
)
}

function formatStoreForInspectorTree(store: GenericStore): CustomInspectorNode {
return {
id: store.id,
label: store.id,
tags: [],
}
}

function formatStoreForInspectorState(
store: GenericStore
): CustomInspectorState[string] {
const fields: CustomInspectorState[string] = [
{ editable: false, key: 'id', value: formatDisplay(store.id) },
{ editable: true, key: 'state', value: store.state },
]

return fields
}
9 changes: 9 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Global compile-time constants
declare var __DEV__: boolean
declare var __FEATURE_PROD_DEVTOOLS__: boolean
declare var __BROWSER__: boolean
declare var __CI__: boolean
declare var __VUE_DEVTOOLS_TOAST__: (
message: string,
type?: 'normal' | 'error' | 'warning'
) => void
11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export { createStore } from './store'
export { setActiveReq, setStateProvider, getRootState } from './rootStore'
import { createStore } from './store'
export {
setActiveReq,
setStateProvider,
getRootState,
createPinia,
} from './rootStore'
export { StateTree, StoreGetter, Store } from './types'
// TODO: deprecate createStore
export { createStore, createStore as defineStore }
27 changes: 27 additions & 0 deletions src/rootStore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { App } from 'vue'
import { NonNullObject, StateTree, GenericStore } from './types'

/**
Expand Down Expand Up @@ -55,3 +56,29 @@ export function getRootState(req: NonNullObject): Record<string, StateTree> {

return rootState
}

/**
* Client-side application instance used for devtools
*/
export let clientApp: App | undefined
export const setClientApp = (app: App) => (clientApp = app)
export const getClientApp = () => clientApp

export function createPinia() {
return {
install(app: App) {
setClientApp(app)
},
}
}

/**
* Registered stores
*/
export const stores = new Set<GenericStore>()

export function registerStore(store: GenericStore) {
stores.add(store)
}

export const getRegisteredStores = () => stores
19 changes: 17 additions & 2 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import {
setActiveReq,
storesMap,
getInitialState,
getClientApp,
} from './rootStore'
import { addDevtools } from './devtools'

const IS_CLIENT = typeof window !== 'undefined'

function innerPatch<T extends StateTree>(
target: T,
Expand Down Expand Up @@ -200,8 +204,19 @@ export function createStore<
(store = buildStore(id, state, getters, actions, getInitialState(id)))
)

// TODO: client devtools when availables
// if (isClient) useStoreDevtools(store)
if (IS_CLIENT && __BROWSER__ && (__DEV__ || __FEATURE_PROD_DEVTOOLS__)) {
const app = getClientApp()
if (app) {
addDevtools(app, store, req)
} else {
console.warn(
`[🍍]: store was instantiated before calling\n` +
`app.use(pinia)\n` +
`Make sure to install pinia's plugin by using createPinia:\n` +
`linkto docs TODO`
)
}
}
}

return store
Expand Down
6 changes: 5 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"include": ["src/**/*.ts", "__tests__/**/*.ts"],
"include": [
"src/global.d.ts",
"src/**/*.ts",
"__tests__/**/*.ts"
],
"compilerOptions": {
"baseUrl": ".",
"rootDir": ".",
Expand Down
Loading

0 comments on commit 849cb3f

Please sign in to comment.