diff --git a/package.json b/package.json index 717a5b85..cda89608 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "openinula", + "name": "inula", "description": "OpenInula is a JavaScript framework library.", "version": "0.0.1", "private": true, diff --git a/packages/inula-dev-tools/src/contentScript/index.ts b/packages/inula-dev-tools/src/contentScript/index.ts new file mode 100644 index 00000000..a77affdc --- /dev/null +++ b/packages/inula-dev-tools/src/contentScript/index.ts @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { injectSrc, injectCode } from '../utils/injectUtils'; +import { checkMessage } from '../utils/transferUtils'; +import { DevToolContentScript, DevToolHook, DevToolBackground } from '../utils/constants'; +import { changeSource } from '../utils/transferUtils'; + +// 页面的 window 对象不能直接通过 contentScript 代码修改,只能通过添加 js 代码往页面 window 注入 hook +const rendererURL = chrome.runtime.getURL('/injector.js'); +if (window.performance.getEntriesByType('navigation')) { + const entryType = (window.performance.getEntriesByType('navigation')[0] as any).type; + if (entryType === 'navigate') { + injectSrc(rendererURL); + } else if (entryType === 'reload' && !(window as any).__INULA_DEV_HOOK__) { + let rendererCode; + const request = new XMLHttpRequest(); + request.addEventListener('load', function () { + rendererCode = this.responseText; + }); + request.open('GET', rendererURL, false); + request.send(); + injectCode(rendererCode); + } +} + +// 监听来自页面的信息 +window.addEventListener( + 'message', + event => { + // 只监听来自本页面的消息 + if (event.source !== window) { + return; + } + + const data = event.data; + if (checkMessage(data, DevToolHook)) { + changeSource(data, DevToolContentScript); + // 传递给 background + chrome.runtime.sendMessage(data); + } + }, + false +); + +// 监听来自 background 的消息 +chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { + // 该方法可以监听页面 contentScript 和插件的消息 + // 没有 tab 信息说明消息来自插件 + if (!sender.tab && checkMessage(message, DevToolBackground)) { + changeSource(message, DevToolContentScript); + // 传递消息给页面 + window.postMessage(message, '*'); + } + sendResponse({ status: 'ok' }); +}); diff --git a/packages/inula-dev-tools/src/main/index.ts b/packages/inula-dev-tools/src/main/index.ts new file mode 100644 index 00000000..2de25903 --- /dev/null +++ b/packages/inula-dev-tools/src/main/index.ts @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { render, createElement } from 'openinula'; +import Panel from '../panel/Panel'; +import PanelX from '../panelX/PanelX'; + +let panelCreated = false; + +const viewSource = () => { + setTimeout(() => { + chrome.devtools.inspectedWindow.eval(` + if (window.$type != null) { + if ( + window.$type && + window.$type.prototype && + window.$type.prototype.render + ) { + // 类组件 + inspect(window.$type.prototype.render); + } else { + // 函数组件 + inspect(window.$type); + } + } + `); + }, 100); +}; + +const inspectVNode = () => { + chrome.devtools.inspectedWindow.eval( + ` + window.__INULA_DEV_HOOK__ && window.__INULA_DEV_HOOK__.$0 !== $0 + ? (inspect(window.__INULA_DEV_HOOK__.$0.realNode), true) + : false + `, + (_, error) => { + if (error) { + console.error(error); + } + } + ); +}; + +let currentPanel = null; + +chrome.devtools.inspectedWindow.eval( + 'window.__INULA_DEV_HOOK__', + function (isInula, error) { + if (!isInula || panelCreated) { + return; + } + + panelCreated = true; + chrome.devtools.panels.create( + 'Inula', + '', + 'panel.html', + (extensionPanel) => { + extensionPanel.onShown.addListener((panel) => { + if (currentPanel === panel) { + return; + } + currentPanel = panel; + const container = panel.document.getElementById('root'); + const element = createElement(Panel, { viewSource, inspectVNode }); + render(element, container); + }); + } + ); + + chrome.devtools.panels.create( + 'InulaX', + '', + 'panelX.html', + (extensionPanel) => { + extensionPanel.onShown.addListener((panel) => { + if (currentPanel === panel) { + return; + } + currentPanel = panel; + const container = panel.document.getElementById('root'); + const element = createElement(PanelX, {}); + render(element, container); + }); + } + ); + } +); diff --git a/packages/inula-dev-tools/src/main/main.html b/packages/inula-dev-tools/src/main/main.html new file mode 100644 index 00000000..210cec23 --- /dev/null +++ b/packages/inula-dev-tools/src/main/main.html @@ -0,0 +1,30 @@ + + + + + + + + + + +
+

Inula dev tools!

+
+ + + diff --git a/packages/inula-dev-tools/src/panelX/ActionRunner.tsx b/packages/inula-dev-tools/src/panelX/ActionRunner.tsx new file mode 100644 index 00000000..7b76145b --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/ActionRunner.tsx @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { useState } from 'openinula'; +import { Modal } from './Modal'; +import { highlight, sendMessage } from './utils'; + +function executeAction(storeId: string, name: string, args: any[]) { + sendMessage({ + type: 'inulax run action', + tabId: chrome.devtools.inspectedWindow.tabId, + storeId, + action: name, + args, + }); +} + +function queryAction(storeId: string, name: string, args: any[]) { + sendMessage({ + type: 'inulax queue action', + tabId: chrome.devtools.inspectedWindow.tabId, + storeId, + action: name, + args, + }); +} + +export function ActionRunner({ foo, storeId, actionName }) { + const [data, setState] = useState({ + modal: false, + gatheredAttrs: [], + query: false, + }); + const modalIsOpen = data.modal; + const gatheredAttrs = data.gatheredAttrs; + function setData(val) { + const newData = { + modal: data.modal, + gatheredAttrs: data.gatheredAttrs, + }; + + Object.entries(val).forEach(([key, value]) => (newData[key] = value)); + + setState(newData as any); + } + + const plainFunction = foo.replace(/\{.*}/gms, ''); + const attributes = plainFunction + .replace(/^.*\(/g, '') + .replace(/\).*$/, '') + .split(/, ?/) + .filter((item, index) => index > 0); + + return ( + <> + { + if (attributes.length > 0) { + setData({ modal: false, gatheredAttrs: [], query: false }); + } else { + executeAction(storeId, actionName, gatheredAttrs); + } + }} + > + + ☼ + { + e.preventDefault(); + if (attributes.len > 0) { + setData({ modal: true, gatheredAttrs: [], query: true }); + } else { + queryAction(storeId, actionName, gatheredAttrs); + } + }} + > + ⌛︎{' '} + + + + {plainFunction} + {' {...}'} + + + {modalIsOpen ? ( + { + setData({ modal: false }); + }} + then={data => { + if (gatheredAttrs.length === attributes.length - 1) { + setData({ modal: false }); + executeAction(storeId, actionName, gatheredAttrs.concat(data)); + } else { + setData({ + gatheredAttrs: gatheredAttrs.concat([data]), + }); + } + }} + > +

{data.query ? 'Query action:' : 'Run action:'}

+

{highlight(plainFunction, attributes[gatheredAttrs.length])}

+
+ ) : null} + + ); +} diff --git a/packages/inula-dev-tools/src/panelX/DiffTree.tsx b/packages/inula-dev-tools/src/panelX/DiffTree.tsx new file mode 100644 index 00000000..05c55cfd --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/DiffTree.tsx @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useState } from 'openinula'; +import styles from './PanelX.less'; +import { Tree } from './Tree'; +import {displayValue, omit} from './utils'; + +type Mutation = { + mutation: boolean; + items?: Mutation[]; + attributes?: { [key: string]: Mutation }; + values?: Mutation[]; + entries?: Mutation[]; + from?: any; + to?: any; +}; + +export function DiffTree({ + mutation, + indent = 0, + index = '', + expand = false, + search = '', + forcedExpand = false, + omitAttrs = [], + doNotDisplayIcon = false, + forcedLabel = null, + className, +}: { + mutation: Mutation; + indent: number; + index?: string | number; + expand?: boolean; + search: string; + forcedExpand?: boolean; + omitAttrs: string[]; + doNotDisplayIcon?: boolean; + forcedLabel?: string | number | null; + className?: string; +}) { + if (omitAttrs.length && mutation.attributes) { + mutation.attributes = omit(mutation.attributes, ...omitAttrs); + mutation.from = mutation.from && omit(mutation.from, ...omitAttrs); + mutation.to = mutation.to && omit(mutation.to, ...omitAttrs); + } + const [expanded, setExpanded] = useState(expand); + + const deleted = mutation.mutation && !('to' in mutation); + const newValue = mutation.mutation && !('from' in mutation); + const mutated = mutation.mutation; + + const isArray = mutated && mutation.items; + const isObject = mutated && mutation.attributes; + const isMap = mutated && mutation.entries; + const isSet = mutated && mutation.values; + const isPrimitive = !isArray && !isObject && !isMap && !isSet; + + if (!mutated) { + return ( + + ); + } + + if (newValue) { + return ( + + ); + } + + if (deleted) { + return ( + + ); + } + + return ( +
{ + e.stopPropagation(); + }} + > + { + setExpanded(!expanded); + }} + > + {new Array(Math.max(indent, 0)).fill( )} + {isPrimitive ? ( + // 如果两个 value 是基本变量并且不同,则简单显示不同点 +
{ + e.stopPropagation(); + }} + > + + +
+ ) : ( + // 如果至少有一个是复杂变量,则需要展开按钮 + <> + {forcedExpand ? '' : expanded ? : } + {index === 0 || index ? ( + {displayValue(index, search)}: + ) : ( + '' + )} + {isArray ? ( + // 如果都是数组进行比较 + expanded ? ( + [ + Array(Math.max(mutation.from.length, mutation.to.length)) + .fill(true) + .map((i, index) => { + return ( +
+ {mutation.items[index].mutation ? ( + + ) : ( + + )} +
+ ); + }), + ] + ) : ( + forcedLabel || `Array(${mutation.to?.length})` + ) + ) : isSet ? ( + expanded ? ( +
+
+ {forcedLabel || `Set(${mutation.to?.values.length})`} +
+ {Array( + Math.max( + mutation.from?.values.length, + mutation.to?.values.length + ) + ) + .fill(true) + .map((i ,index) => ( +
+ {mutation.values[index].mutation ? ( + + ) : ( + + )} +
+ ))} +
+ ) : ( + + {forcedLabel || `Set(${mutation.to?.values.length})`} + + ) + ) : isMap ? ( + expanded ? ( + <> + + {forcedLabel || `Map(${mutation.to?.entries.length})`} + + {Array( + Math.max( + mutation.from?.entries.length, + mutation.to?.entries.length + ) + ) + .fill(true) + .map((i, index) => + mutation.entries[index].mutation ? ( +
+ +
+ ) : ( +
+ +
+ ) + )} + + ) : ( + + {forcedLabel || `Map(${mutation.to?.entries.length})`} + + ) + ) : expanded ? ( + // 如果都是 object 进行比较 + Object.entries(mutation.attributes).map(([key, item]) => { + return item.mutation ? ( + e.stopPropagation()}> + { + + } + + ) : ( + e.stopPropagation()}> + { + + } + + ); + }) + ) : ( + forcedLabel || '{ ... }' + )} + + )} +
+
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/EventLog.tsx b/packages/inula-dev-tools/src/panelX/EventLog.tsx new file mode 100644 index 00000000..265f3243 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/EventLog.tsx @@ -0,0 +1,402 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useEffect, useState, useRef } from 'openinula'; +import { DevToolPanel } from '../utils/constants'; +import { + initBackgroundConnection, + addBackgroundMessageListener, + removeBackgroundMessageListener, +} from '../panelConnection'; +import { Table } from './Table'; +import { Tree } from './Tree'; +import {fullTextSearch, omit} from './utils'; +import styles from './PanelX.less'; +import { Checkbox } from '../utils/Checkbox'; +import { DiffTree } from './DiffTree'; + +const eventTypes = { + INITIALIZED: 'inulax store initialized', + STATE_CHANGE: 'inulax state change', + SUBSCRIBED: 'inulax subscribed', + UNSUBSCRIBED: 'inulax unsubscribed', + ACTION: 'inulax action', + ACTION_QUEUED: 'inulax action queued', + QUEUE_PENDING: 'inulax queue pending', + QUEUE_FINISHED: 'inulax queue finished', +}; + +const otherTypes = { + GET_EVENTS: 'inulax getEvents', + GET_PERSISTENCE: 'inulax getPersistence', + EVENTS: 'inulax events', + FLUSH_EVENTS: 'inulax flush events', + SET_PERSISTENT: 'inulax setPersistent', + RESET_EVENTS: 'inulax resetEvents', +}; + +function extractDataByType(message, search) { + if (message.type === eventTypes.ACTION) { + return ( +
{ + e.stopPropagation(); + }} + > + +
+ ); + } + + if (message.type === eventTypes.STATE_CHANGE) { + return ( +
{ + e.stopPropagation(); + }} + > + {`${message.data.change.vNodes.length} nodes changed:`} + { + return ( + + {vNode.type}() + + ); + })} + /> +
+ ); + } + + return N/A +} + +export default function EventLog({ setNextStore, setEventFilter, eventFilter }) { + const [log, setLog] = useState([]); + const [initlized, setInitlized] = useState(false); + const [persistent, setPersistent] = useState(false); + const filterField = useRef(null); + + const addFilter = (key, value) => { + const filters = { ...eventFilter }; + filters[key] = value; + + setEventFilter(filters); + }; + + const removeFilter = key => { + const filters = { ...eventFilter }; + delete filters[key]; + + setEventFilter(filters); + }; + + if (!initlized) { + setTimeout(() => { + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload: { + type: otherTypes.GET_EVENTS, + tabId: chrome.devtools.inspectedWindow.tabId, + }, + from: DevToolPanel, + }); + + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload: { + type: otherTypes.GET_PERSISTENCE, + tabId: chrome.devtools.inspectedWindow.tabId, + }, + from: DevToolPanel, + }); + }, 100); + } + + useEffect(() => { + const lisener = message => { + if (message.payload.type.startsWith('inulax')) { + if (message.payload.type === otherTypes.EVENTS) { + setLog(message.payload.events); + setInitlized(true); + } else if (message.payload.type === otherTypes.SET_PERSISTENT) { + setPersistent(message.payload.persistent); + } else if (message.payload.type === otherTypes.FLUSH_EVENTS) { + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload: { + type: otherTypes.GET_EVENTS, + tabId: chrome.devtools.inspectedWindow.tabId, + }, + from: DevToolPanel, + }); + } + } + }; + initBackgroundConnection('panel'); + addBackgroundMessageListener(lisener); + return () => { + removeBackgroundMessageListener(lisener); + }; + }); + + const filters = Object.entries(eventFilter); + const usedTypes = { all: 0 }; + + const processedData = log + .filter(event => { + if (!Object.values(eventTypes).includes(event.message.type)) { + return false; + } + usedTypes.all++; + if (!usedTypes[event.message.type]) { + usedTypes[event.message.type] = 1; + } else { + usedTypes[event.message.type]++; + } + + if (!filters.length) { + return true; + } + + return !filters.some(([key, value]) => { + if (key === 'fulltext') { + const result = fullTextSearch(event, value); + return !result; + } + const keys = key.split('.'); + let search = event; + keys.forEach(attr => { + search = search[attr]; + }); + return value !== search; + }); + }) + .map(event => { + const date = new Date(event.timestamp); + + return { + id: event.id, + timestamp: event.timestamp, + type: event.message.type, + time: `${date.toLocaleTimeString()} - ${date.toLocaleDateString()}`, + state: event.message.type === eventTypes.STATE_CHANGE ? ( + + ) : ( + + ), + storeClick: ( + { + e.preventDefault(); + setNextStore(event.message.data.store.id); + }} + > + {event.message.data.store.id} + + ), + additionalData: extractDataByType( + event.message, + eventFilter['fulltext'] + ), + storeId: event.message.data.store.id, + event, + }; + }); + + return ( +
+
+ { + if (!filterField.current.value) { + removeFilter('fulltext'); + } + addFilter('fulltext', filterField.current.value); + }} + /> + + {' | '} + { + e.stopPropagation(); + + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload: { + type: otherTypes.SET_PERSISTENT, + tabId: chrome.devtools.inspectedWindow.tabId, + persistent: !persistent, + }, + from: DevToolPanel, + }); + + setPersistent(!persistent); + }} + > + Persistent events + + {' | '} + + {eventFilter['message.data.store.id'] ? ( + + {' | '} + { + setNextStore(eventFilter['message.data.store.id']); + }} + >{` Displaying: [${eventFilter['message.data.store.id']}] `} + + + ) : null} +
+
+ + {Object.values(eventTypes).map(eventType => { + return ( + + ); + })} +
+ { + const message = data.event.message; + return { + type: data.type, + store: { + actions: Object.fromEntries( + Object.entries(message.data.store.$config.actions).map( + ([id, action]) => { + return [ + id, + (action as string).replace(/\{.*}/gms, '{...}').replace('function ', ''), + ]; + } + ) + ), + computed: Object.fromEntries( + Object.keys(message.data.store.$c).map(key => [ + key, + message.data.store.expanded[key], + ]) + ), + state: message.data.store.$s, + id: message.data.store.id, + }, + // data: omit(data, 'storeClick', 'additionalData'), + }; + }} + search={eventFilter.fulltext ? eventFilter.fulltext : ''} + /> + + ); +} diff --git a/packages/inula-dev-tools/src/panelX/Modal.tsx b/packages/inula-dev-tools/src/panelX/Modal.tsx new file mode 100644 index 00000000..267d5493 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Modal.tsx @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import Inula, { useRef, useState } from 'openinula'; + +export function Modal({ + closeModal, + then, + children, +}: { + closeModal: () => void; + then: (value: any) => void; + children?: any[]; +}) { + const inputRef = useRef(null); + const [error, setError] = useState(null); + + setTimeout(() => { + inputRef.current.focus(); + inputRef.current.value = ''; + }, 10); + + const tryGatherData = () => { + let data; + try { + data = eval(inputRef.current.value); + } catch (err) { + setError(err); + return; + } + + if (then) { + then(data); + } + }; + + return ( +
+
+

{children}

+

+ { + if (key === 'Enter') { + tryGatherData(); + } + }} + /> +

+ {error ?

Variable parsing error

: null} +

+ + +

+
+
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/PanelX.less b/packages/inula-dev-tools/src/panelX/PanelX.less new file mode 100644 index 00000000..04656c3c --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/PanelX.less @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +@import '../components/assets.less'; + +.displayData { + background-color: rgb(241, 243, 244); +} + +.app { + display: flex; + flex-direction: row; + height: 100%; + font-size: @common-font-size; +} + +div.wrapper { + margin: 15px; + position: relative; + width: calc(100% - 30px); + display: block; +} + +div.table { + display: table; + vertical-align: top; + width: calc(100%); + background-color: white; + position: relative; +} + +div.row { + display: table-row; + + &:nth-child(2n + 1) { + background-color: rgb(241, 243, 244); + .default { + background-color: rgb(241, 243, 244); + } + } +} + +div.cell { + display: table-cell; + cursor: pointer; + padding: 5px; +} + +div.half { + width: calc(50% - 8px); + float: left; +} + +div.header { + background-color: rgb(241, 243, 244); + font-weight: bold; +} + +div.row.active { + background-color: #00a; + color: white; +} + +button.tab { + border: 1px solid grey; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + &.active { + border-bottom: none; + color: black; + font-weight: bold; + background-color: white; + } +} + +span.highlighted { + background-color: #ff0; +} + +.grey { + color: grey; +} + +.red { + color: #a00; +} + +.blue { + color: #00a; +} + +.purple { + color: #909; +} + +.bold { + font-weight: bold; +} + +.link { + font-weight: bold; + text-decoration: underline; + cursor: pointer; + color: #00a; +} + +.compositeInput { + background-color: white; + border: 1px solid grey; + display: inline-block; + border-radius: 0; + padding: 5px; + &.left { + border-right: 0; + margin-right: 0; + padding-right: 0; + } + + &.right { + border-left: 0; + margin-left: 0; + } + + &:focus-visible { + outline: none; + } +} + +.filterButton { + background-color: transparent; + padding: 5px; + border-radius: 5px; + border: 0; + &.active { + background-color: #ddd; + } +} + +.added { + background-color: #afa; + &::before { + font-weight: bold; + color: #0a0; + } +} + +.deleted { + background-color: #faa; + text-decoration-line: line-through; + &::before { + font-weight: bold; + color: #a00; + } +} + +.changed { + background-color: #ffa; + &::before { + font-weight: bold; + color: #ca0; + } +} + +.default { + background-color: white; +} + +.floatingButton { + right: 5px; + position: absolute; + height: 17px; + width: 17px; + font-size: 10px; + padding: 0; + cursor: pointer; +} + +.scrollable { + max-height: calc(100vh - 65px); + overflow: auto; + + div.row { + display: block; + } +} diff --git a/packages/inula-dev-tools/src/panelX/PanelX.tsx b/packages/inula-dev-tools/src/panelX/PanelX.tsx new file mode 100644 index 00000000..b3038c86 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/PanelX.tsx @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useState } from 'openinula'; +import EventLog from './EventLog'; +import Stores from './Stores'; +import styles from './PanelX.less'; + +export default function PanelX() { + const [active, setActive] = useState('stores'); + const [nextStoreId, setNextStoreId] = useState(null); + const [eventFilter, setEventFilter] = useState({}); + + function showFilterEvents(filter) { + setActive('events'); + setEventFilter(filter); + } + + const tabs = [ + { + id: 'stores', + title: 'Stores', + getComponent: () => ( + + ), + }, + { + id: 'events', + title: 'Events', + getComponents: () => ( + { + setNextStoreId(id); + setActive('stores'); + }} + setEventFilter={setEventFilter} + eventFilter={eventFilter} + /> + ), + }, + ]; + + return ( +
+
+ {tabs.map(tab => + tab.id === active ? ( + + ) : ( + + ) + )} +
+ {tabs.find(item => item.id === active).getComponent()} +
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/Stores.tsx b/packages/inula-dev-tools/src/panelX/Stores.tsx new file mode 100644 index 00000000..aa7c60e5 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Stores.tsx @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useState, useEffect } from 'openinula'; +import { + initBackgroundConnection, + addBackgroundMessageListener, + removeBackgroundMessageListener, + postMessageToBackground, +} from '../panelConnection'; +import { Table } from './Table'; +import { omit, sendMessage } from './utils'; +import styles from './PanelX.less'; +import { Highlight, RemoveHighlight } from '../utils/constants'; +import { ActionRunner } from './ActionRunner'; +import { Tree } from './Tree'; + +export default function Stores({ nextStoreId, showFilteredEvents }) { + const [stores, setStores] = useState([]); + const [initialized, setInitialized] = useState(false); + + if (!initialized) { + setTimeout(() => { + sendMessage({ + type: 'inulax getStores', + tabId: chrome.devtools.inspectedWindow.tabId, + }); + }, 100); + } + + useEffect(() => { + const listener = message => { + if (message.payload.type.startsWith('inulax')) { + // 过滤 inula 消息 + if (message.payload.type === 'inulax stores') { + // Stores 更新 + setStores(message.payload.stores); + setInitialized(true); + } else if (message.payload.type === 'inulax flush stores') { + // Flush store + sendMessage({ + type: 'inulax getStores', + tabId: chrome.devtools.inspectedWindow.tabId, + }); + } else if (message.payload.type === 'inulax observed components') { + // observed components 更新 + setStores( + stores.map(store => { + store.observedComponents = message.payload.data[store.id] || []; + return store; + }) + ); + } + } + }; + initBackgroundConnection('panel'); + addBackgroundMessageListener(listener); + return () => { + removeBackgroundMessageListener(listener); + }; + }); +} diff --git a/packages/inula-dev-tools/src/panelX/Table.tsx b/packages/inula-dev-tools/src/panelX/Table.tsx new file mode 100644 index 00000000..81ab7248 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Table.tsx @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useState } from 'openinula'; +import {Tree} from './Tree'; +import styles from './PanelX.less'; + +type displayKeysType = [string, string][]; + +export function Table({ + data, + dataKey = 'id', + displayKeys, + activate, + displayDataProcessor, + search = '', +}: { + data; + dataKey: string; + displayKeys: displayKeysType; + activate?: { + [key: string]: any; + }; + displayDataProcessor: (data: { [key: string]: any }) => { + [key: string]: any; + }; + search: string; +}) { + const [keyToDisplay, setKeyToDisplay] = useState(false); + const [manualOverride, setManualOverride] = useState(false); + let displayRow = null; + + if (!manualOverride && activate) { + data.forEach(item => { + if (displayRow) { + return; + } + let notMatch = false; + Object.entries(activate).forEach(([key, value]) => { + if (notMatch) { + return; + } + if (item[key] !== value) { + notMatch = true; + } + }); + if (notMatch) { + return; + } + displayRow = item; + }); + } else if (manualOverride && keyToDisplay) { + data.forEach(item => { + if (displayRow) { + return; + } + if (item[dataKey] === keyToDisplay) { + displayRow = item; + } + }); + } + + if (displayRow) { + const [attr, title] = displayKeys[0]; + return ( +
+
+
+
{title}
+
+
+ + {data.map(row => ( +
{ + setManualOverride(true); + setKeyToDisplay( + keyToDisplay === row[dataKey] ? null : row[dataKey] + ); + }} + > +
{row?.[attr] || ''}
+
+ ))} +
+
+ +
+
+
+ Data: + +
+
+
+ +
+
+ +
+
+
+
+
+ ); + } else { + return ( +
+
+
+ {displayKeys.map(([key, title]) => ( +
{title}
+ ))} +
+ {data.map(item => ( +
{ + setManualOverride(true); + setKeyToDisplay(item[dataKey]); + }} + className={styles.row} + > + {displayKeys.map(([key, title]) => ( +
{item[key]}
+ ))} +
+ ))} +
+
+ ); + } +} diff --git a/packages/inula-dev-tools/src/panelX/Tree.tsx b/packages/inula-dev-tools/src/panelX/Tree.tsx new file mode 100644 index 00000000..3091c1a4 --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/Tree.tsx @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import { useState } from 'openinula'; +import styles from './PanelX.less'; +import {Modal} from './Modal'; +import { displayValue, omit } from './utils'; + +export function Tree({ + data, + indent = 0, + index = '', + expand = false, + search = '', + forcedExpand = false, + onEdit = null, + omitAttrs = [], + className, + forcedLabel = null, +}: { + data: any; + indent?: number; + index?: string | number; + expand?: boolean; + search?: string; + forcedExpand?: boolean; + className?: string | undefined + omitAttrs?: string[]; + onEdit?: (path: any[], value: any) => void | null; + forcedLabel?: string | number | null; +}) { + const [expanded, setExpanded] = useState(expand); + const [modal, setModal] = useState(false); + const isArray = Array.isArray(data); + const isObject = data && !isArray && typeof data === 'object'; + const isSet = isObject && data?._type === 'Set'; + const isWeakSet = isObject && data?._type === 'WeakSet'; + const isMap = isObject && data?._type === 'Map'; + const isWeakMap = isObject && data?._type === 'WeakMap'; + const isVNode = isObject && data.vtype; + const canBeExpanded = isArray || (isObject && !isWeakSet && !isWeakMap); + + if (isObject && omitAttrs?.length) { + data = omit(data, ...omitAttrs); + } + + return canBeExpanded ? ( +
{ + e.stopPropagation(); + }} + > + { + setExpanded(!expanded); + }} + > + {new Array(Math.max(indent, 0)).fill( )} + {forcedExpand || isVNode ? null : expanded ? ( + + ) : ( + + )} + {index === 0 || index ? ( + <> + {displayValue(index, search)}: + + ) : ( + '' + )} + {forcedLabel + ? forcedLabel + : expanded + ? isVNode + ? null + : Array.isArray(data) + ? `Array(${data.length})` + : isMap + ? `Map(${data.entries.length})` + : isSet + ? `Set(${data.values.length})` + : '{ ... }' + : isWeakMap + ? 'WeakMap()' + : isWeakSet + ? 'WeakSet()' + : isMap + ? `Map(${data.entries.length})` + : isSet + ? `Set(${data.values.length})` + : Array.isArray(data) + ? `Array(${data.length})` + : '{ ... }'} + + {expanded || isVNode ? ( + isArray ? ( + <> + {data.map((value, index) => { + return ( +
+ { + onEdit(path.concat([index]), val); + } + : null + } + /> +
+ ); + })} + + ) : isVNode ? ( + data + ) : isMap ? ( +
+ {data.entries.map(([key, value]) => { + return ( + + ); + })} +
+ ) : isSet ? ( + data.values.map(item => { + return ( +
+ +
+ ); + }) + ) : ( + Object.entries(data).map(([key, value]) => { + return ( +
+ { + onEdit(path.concat([key]), val); + } + : null + } + /> +
+ ); + }) + ) + ) : ( + '' + )} +
+ ) : ( +
+ {new Array(indent).fill( )} + + {typeof index !== 'undefined' ? ( + <> + {displayValue(index, search)}: + + ) : ( + '' + )} + {displayValue(data, search)} + {onEdit && !isWeakSet && !isWeakMap ? ( // TODO: editable weak set and map + <> + { + setModal(true); + }} + > + ☼ + + {onEdit && modal ? ( + { + setModal(false); + }} + then={data => { + onEdit([], data); + setModal(false); + }} + > +

Edit value:

{index} +
+ ) : null} + + ) : null} +
+
+ ); +} diff --git a/packages/inula-dev-tools/src/panelX/index.tsx b/packages/inula-dev-tools/src/panelX/index.tsx new file mode 100644 index 00000000..7e46144c --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import PanelX from './PanelX'; + +export default PanelX; diff --git a/packages/inula-dev-tools/src/panelX/panelX.html b/packages/inula-dev-tools/src/panelX/panelX.html new file mode 100644 index 00000000..4a211dcf --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/panelX.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + +
+ + + + diff --git a/packages/inula-dev-tools/src/panelX/utils.tsx b/packages/inula-dev-tools/src/panelX/utils.tsx new file mode 100644 index 00000000..1f0d807f --- /dev/null +++ b/packages/inula-dev-tools/src/panelX/utils.tsx @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2023 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +import * as Inula from 'openinula'; +import styles from './PanelX.less'; +import { DevToolPanel } from '../utils/constants'; + +export function highlight(source, search) { + if (!search || !source?.split) { + return source; + } + + const parts = source.split(search); + const result = []; + + for (let i= 0; i < parts.length * 2 - 1; i++) { + if (i % 2) { + result.push({search}); + } else { + result.push(parts[i / 2]); + } + } + + return result; +} + +export function displayValue(val: any, search = '') { + if (typeof val === 'boolean') { + return ( + + {highlight(val ? 'true' : 'false', search)} + + ); + } + + if (val === '') { + return {'""'}; + } + + if (typeof val === 'undefined') { + return {highlight('undefined', search)}; + } + + if (val === 'null') { + return {highlight('null', search)}; + } + + if (typeof val === 'string') { + if (val.match(/^function\s?\(/)) { + return ( + + ƒ + {highlight( + val.match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''), + search + )} + + ); + } + return "{highlight(val, search)}"; + } + if (typeof val === 'number') { + return {highlight('' + val, search)}; + } + if (typeof val === 'function') { + const args = val.toString().match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''); + return ( + + ƒ + {highlight(args, search)} + + ); + } + if (typeof val === 'object') { + if (val?._type === 'WeakSet') { + return WeakSet(); + } + + if (val?._type === 'WeakMap') { + return WeakMap(); + } + } +} + +export function fullTextSearch(value, search) { + if (!value) { + return false; + } + + if (Array.isArray(value)) { + return value.some(val => fullTextSearch(val, search)); + } + + if (typeof value === 'object') { + if (value?._type === 'Set') { + return value.values.some(val => fullTextSearch(val, search)); + } + if (value?._type === 'Map') { + return value.entries.some( + (key, val) => fullTextSearch(key, search) || fullTextSearch(val, search) + ); + } + return Object.values(value).some(val => fullTextSearch(val, search)); + } + + return value.toString().includes(search); +} + +export function omit(obj, ...attrs) { + const res = { ...obj }; + attrs.forEach(attr => delete res[attr]); + return res; +} + +export function stringify(data) { + if (typeof data === 'string' && data.startsWith('function(')) { + return ( + + ƒ + {data.match(/^function\([\w,]*\)/g)[0].substring(8)} + + ); + } + + if (!data) { + return displayValue(data); + } + + if (Array.isArray(data)) { + return `Array(${data.length})`; + } + + if (typeof data === 'object') { + return `{${Object.entries(data).map(([key, value]) => { + if (typeof value === 'string' && value.startsWith('function(')) { + return ( + + {key} + + ƒ + {value.match(/^function\([\w,]*\)/g)[0].substring(8)} + + + ); + } + if (!value) { + return ( + + {key}:{displayValue(value)} + + ); + } + if (Array.isArray(value)) { + return ( + + {key}:{' '} + {`Array(${value.length})`} + + ); + } + if (typeof value === 'object') { + if ((value as any)?._type === 'WeakSet') { + return ( + + {key}: {'WeakSet()'} + + ); + } + if ((value as any)?._type === 'WeakMap') { + return ( + + {key}: {'WeakMap'} + + ); + } + if ((value as any)?._type === 'Set') { + return ( + + {key}:{' '} + {`Set(${(value as Set).size})`} + + ); + } + if ((value as any)?._type === 'Map') { + return ( + + {key}:{' '} + {`Map(${(value as Map).size})`} + + ); + } + + // object + return ( + + {key}: {'{...}'} + + ); + } + return ( + + {key}: {displayValue(value)} + + ); + })}}`; + } + return data; +} + +export function sendMessage(payload) { + chrome.runtime.sendMessage({ + type: 'INULA_DEV_TOOLS', + payload, + from: DevToolPanel, + }); +} diff --git a/packages/inula-dev-tools/src/utils/injectUtils.ts b/packages/inula-dev-tools/src/utils/injectUtils.ts index 7b25df96..e5614388 100644 --- a/packages/inula-dev-tools/src/utils/injectUtils.ts +++ b/packages/inula-dev-tools/src/utils/injectUtils.ts @@ -38,7 +38,7 @@ export function injectSrc(src) { ).appendChild(script); } -function injectCode(code) { +export function injectCode(code) { const script = document.createElement('script'); script.textContent = code; diff --git a/packages/inula-intl/package.json b/packages/inula-intl/package.json index 00cca8ea..910ca492 100644 --- a/packages/inula-intl/package.json +++ b/packages/inula-intl/package.json @@ -1,6 +1,6 @@ { "name": "inula-intl", - "version": "0.0.2", + "version": "0.0.3", "description": "", "main": "build/intl.umd.js", "type": "commonjs", diff --git a/packages/inula-request/package.json b/packages/inula-request/package.json index 1a8fbe97..b2230d30 100644 --- a/packages/inula-request/package.json +++ b/packages/inula-request/package.json @@ -1,6 +1,6 @@ { "name": "inula-request", - "version": "0.0.5", + "version": "0.0.7", "description": "Inula-request brings you a convenient request experience!", "main": "./dist/inulaRequest.js", "scripts": { diff --git a/packages/inula-request/rollup.config.js b/packages/inula-request/rollup.config.js index 54689cb2..439ea2ab 100644 --- a/packages/inula-request/rollup.config.js +++ b/packages/inula-request/rollup.config.js @@ -41,4 +41,7 @@ export default { presets: ['@babel/preset-env'] }) ], + external:[ + 'openinula' + ], }; diff --git a/packages/inula/README.md b/packages/inula/README.md index 7a1082aa..d362820a 100644 --- a/packages/inula/README.md +++ b/packages/inula/README.md @@ -157,10 +157,6 @@ openinula团队会关注所有Pull Request,我们会review以及合入你的 1. `npm run build` 同时构建openinula UMD的prod版本和dev版本 2. `build-types` 单独构建openinula的类型提示@types目录 -#### 配套开发工具 - -- [openinula-devtool](https://www.XXXX.com): 可视化openinula项目页面的vDom树 - ## 开源许可协议 请查阅 License 获取开源许可协议的更多信息. diff --git a/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx b/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx index 38b7445b..499be389 100644 --- a/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx +++ b/packages/inula/scripts/__tests__/HorizonXTest/adapters/ReduxAdapter.test.tsx @@ -187,7 +187,7 @@ describe('Redux adapter', () => { reduxStore.dispatch({ type: 'toggle' }); reduxStore.dispatch({ type: 'toggle' }); - expect(counter).toBe(3); // NOTE: first action is always store initialization + expect(counter).toBe(2); // execute dispatch two times, applyMiddleware was called same times }); it('Should apply multiple enhancers', async () => { @@ -226,7 +226,7 @@ describe('Redux adapter', () => { reduxStore.dispatch({ type: 'toggle' }); - expect(counter).toBe(2); // NOTE: first action is always store initialization + expect(counter).toBe(1); // execute dispatch two times, applyMiddleware was called same times expect(lastAction).toBe('toggle'); expect(middlewareCallList[0]).toBe('callCounter'); expect(middlewareCallList[1]).toBe('lastFunctionStorage'); diff --git a/packages/inula/src/inulax/CommonUtils.ts b/packages/inula/src/inulax/CommonUtils.ts index 5202a200..ccbb182f 100644 --- a/packages/inula/src/inulax/CommonUtils.ts +++ b/packages/inula/src/inulax/CommonUtils.ts @@ -67,18 +67,50 @@ export function isPromise(obj: any): boolean { return isObject(obj) && typeof obj.then === 'function'; } -export function isSame(x, y) { - if (typeof Object.is !== 'function') { - if (x === y) { - // +0 != -0 - return x !== 0 || 1 / x === 1 / y; - } else { - // NaN == NaN - return x !== x && y !== y; - } - } else { - return Object.is(x, y); +export function isSame(x: unknown, y: unknown): boolean { + // 如果两个对象是同一个引用,直接返回true + if (x === y) { + return true; } + // 如果两个对象类型不同,直接返回false + if (typeof x !== typeof y) { + return false; + } + // 如果两个对象都是null或undefined,直接返回true + if (x == null || y == null) { + return true; + } + // 如果两个对象都是基本类型,比较他们的值是否相等 + if (typeof x !== 'object') { + return x === y; + } + // 如果两个对象都是数组,比较他们的长度是否相等,然后递归比较每个元素是否相等 + if (Array.isArray(x) && Array.isArray(y)) { + if (x.length !== y.length) { + return false; + } + for (let i = 0; i < x.length; i++) { + if (!isSame(x[i], y[i])) { + return false; + } + } + return true; + } + // 两个对象都是普通对象,首先比较他们的属性数量是否相等,然后递归比较每个属性的值是否相等 + if (typeof x === 'object' && typeof y === 'object') { + const keys1 = Object.keys(x!).sort(); + const keys2 = Object.keys(y!).sort(); + if (keys1.length !== keys2.length) { + return false; + } + for (let i = 0; i < keys1.length; i++) { + if (!isSame(x![keys1[i]], y![keys2[i]])) { + return false; + } + } + return true; + } + return false; } export function getDetailedType(val: any) { diff --git a/packages/inula/src/inulax/adapters/redux.ts b/packages/inula/src/inulax/adapters/redux.ts index b996abed..97ee92f1 100644 --- a/packages/inula/src/inulax/adapters/redux.ts +++ b/packages/inula/src/inulax/adapters/redux.ts @@ -27,12 +27,12 @@ export { createDispatchHook, } from './reduxReact'; -export type ReduxStoreHandler = { - reducer: (state: any, action: { type: string }) => any; - dispatch: (action: { type: string }) => void; - getState: () => any; - subscribe: (listener: () => void) => () => void; - replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void; +export type ReduxStoreHandler = { + reducer(state: T, action: { type: string }): any; + dispatch(action: { type: string }): void; + getState(): T; + subscribe(listener: () => void): () => void; + replaceReducer(reducer: (state: T, action: { type: string }) => any): void; }; export type ReduxAction = { @@ -53,6 +53,9 @@ export type ReduxMiddleware = ( type Reducer = (state: any, action: ReduxAction) => any; +type StoreCreator = (reducer: Reducer, preloadedState?: any) => ReduxStoreHandler; +type StoreEnhancer = (next: StoreCreator) => StoreCreator; + function mergeData(state, data) { if (!data) { state.stateWrapper = data; @@ -87,7 +90,7 @@ function mergeData(state, data) { state.stateWrapper = data; } -export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler { +export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: StoreEnhancer): ReduxStoreHandler { const store = createStoreX({ id: 'defaultStore', state: { stateWrapper: preloadedState }, @@ -130,12 +133,14 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): dispatch: store.$a.dispatch, }; - enhancers && enhancers(result); - result.dispatch({ type: 'InulaX' }); store.reduxHandler = result; + if (typeof enhancers === 'function') { + return enhancers(createStore)(reducer, preloadedState); + } + return result as ReduxStoreHandler; } @@ -150,19 +155,23 @@ export function combineReducers(reducers: { [key: string]: Reducer }): Reducer { }; } -function applyMiddlewares(store: ReduxStoreHandler, middlewares: ReduxMiddleware[]): void { - middlewares = middlewares.slice(); - middlewares.reverse(); - let dispatch = store.dispatch; - middlewares.forEach(middleware => { - dispatch = middleware(store)(dispatch); - }); - store.dispatch = dispatch; +function applyMiddlewares(createStore: StoreCreator, middlewares: ReduxMiddleware[]): StoreCreator { + return (reducer, preloadedState) => { + middlewares = middlewares.slice(); + middlewares.reverse(); + const storeObj = createStore(reducer, preloadedState); + let dispatch = storeObj.dispatch; + middlewares.forEach(middleware => { + dispatch = middleware(storeObj)(dispatch); + }); + storeObj.dispatch = dispatch; + return storeObj; + }; } -export function applyMiddleware(...middlewares: ReduxMiddleware[]): (store: ReduxStoreHandler) => void { - return store => { - return applyMiddlewares(store, middlewares); +export function applyMiddleware(...middlewares: ReduxMiddleware[]): (createStore: StoreCreator) => StoreCreator { + return createStore => { + return applyMiddlewares(createStore, middlewares); }; } @@ -170,7 +179,7 @@ type ActionCreator = (...params: any[]) => ReduxAction; type ActionCreators = { [key: string]: ActionCreator }; export type BoundActionCreator = (...params: any[]) => void; type BoundActionCreators = { [key: string]: BoundActionCreator }; -type Dispatch = (action) => any; +type Dispatch = (action: ReduxAction) => any; export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators { const boundActionCreators = {}; @@ -183,12 +192,12 @@ export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dis return boundActionCreators; } -export function compose(...middlewares: ReduxMiddleware[]) { - return (store: ReduxStoreHandler, extraArgument: any) => { - let val; - middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => { +export function compose(...middlewares: ((...args: any[]) => any)[]): (...args: any[]) => T { + return (...args) => { + let val: any; + middlewares.reverse().forEach((middleware, index) => { if (!index) { - val = middleware(store, extraArgument); + val = middleware(...args); return; } val = middleware(val); diff --git a/packages/inula/src/inulax/adapters/reduxReact.ts b/packages/inula/src/inulax/adapters/reduxReact.ts index 51650f98..32eff8d0 100644 --- a/packages/inula/src/inulax/adapters/reduxReact.ts +++ b/packages/inula/src/inulax/adapters/reduxReact.ts @@ -17,6 +17,7 @@ import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/Ho import { createContext } from '../../renderer/components/context/CreateContext'; import { createElement } from '../../external/JSXElement'; import type { ReduxStoreHandler, ReduxAction, BoundActionCreator } from './redux'; +import { forwardRef } from '../../renderer/components/ForwardRef'; const DefaultContext = createContext(null); type Context = typeof DefaultContext; @@ -40,29 +41,27 @@ export function createStoreHook(context: Context): () => ReduxStoreHandler { }; } -export function createSelectorHook(context: Context): (selector?: (any) => any) => any { - const store = createStoreHook(context)() as unknown as ReduxStoreHandler; - return function (selector = state => state) { - const [b, fr] = useState(false); +export function createSelectorHook(context: Context): (selector?: ((state: unknown) => any) | undefined) => any { + const store = createStoreHook(context)(); + return function useSelector(selector = state => state) { + const [state, setState] = useState(() => store.getState()); useEffect(() => { - const unsubscribe = store.subscribe(() => fr(!b)); - return () => { - unsubscribe(); - }; - }); + const unsubscribe = store.subscribe(() => { + setState(store.getState()); + }); + return () => unsubscribe(); + }, []); - return selector(store.getState()); + return selector(state); }; } export function createDispatchHook(context: Context): () => BoundActionCreator { - const store = createStoreHook(context)() as unknown as ReduxStoreHandler; - return function () { - return action => { - store.dispatch(action); - }; - }.bind(store); + const store = createStoreHook(context)(); + return function useDispatch() { + return store.dispatch; + }; } export const useSelector = selector => { @@ -90,6 +89,11 @@ type MergePropsP = ( type WrappedComponent = (props: OwnProps) => ReturnType; type OriginalComponent = (props: MergedProps) => ReturnType; type Connector = (Component: OriginalComponent) => WrappedComponent; +type ConnectOption = { + areStatesEqual?: (oldState: State, newState: State) => boolean; + context?: Context; + forwardRef?: boolean; +} export function connect( mapStateToProps: MapStateToPropsP = () => ({} as StateProps), @@ -99,10 +103,7 @@ export function connect( dispatchProps, ownProps ): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps), - options?: { - areStatesEqual?: (oldState: any, newState: any) => boolean; - context?: Context; - } + options?: ConnectOption ): Connector { if (!options) { options = {}; @@ -114,37 +115,31 @@ export function connect( //this component should mimic original type of component used const Wrapper: WrappedComponent = (props: OwnProps) => { - const [f, forceReload] = useState(true); - - const store = useStore() as unknown as ReduxStoreHandler; + const store = useStore() as ReduxStoreHandler; + const [state, setState] = useState(() => store.getState()); useEffect(() => { - const unsubscribe = store.subscribe(() => forceReload(!f)); - return () => { - unsubscribe(); - }; - }); + const unsubscribe = store.subscribe(() => { + setState(store.getState()); + }); + return () => unsubscribe(); + }, []); - const previous = useRef({ + const previous = useRef<{ state: { [key: string]: any }; mappedState: StateProps }>({ state: {}, - mappedState: {}, - }) as { - current: { - state: { [key: string]: any }; - mappedState: StateProps; - }; - }; + mappedState: {} as StateProps, + }); let mappedState: StateProps; if (options?.areStatesEqual) { - if (options.areStatesEqual(previous.current.state, store.getState())) { + if (options.areStatesEqual(previous.current.state, state)) { mappedState = previous.current.mappedState as StateProps; } else { - mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps); + mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps); previous.current.mappedState = mappedState; } } else { - mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps); + mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps); previous.current.mappedState = mappedState; } let mappedDispatch: DispatchProps = {} as DispatchProps; @@ -153,12 +148,14 @@ export function connect( Object.entries(mapDispatchToProps).forEach(([key, value]) => { mappedDispatch[key] = (...args: ReduxAction[]) => { store.dispatch(value(...args)); + setState(store.getState()); }; }); } else { mappedDispatch = mapDispatchToProps(store.dispatch, props); } } + mappedDispatch = Object.assign({}, mappedDispatch, { dispatch: store.dispatch }); const mergedProps = ( mergeProps || ((state, dispatch, originalProps) => { @@ -166,12 +163,18 @@ export function connect( }) )(mappedState, mappedDispatch, props); - previous.current.state = store.getState(); + previous.current.state = state; - const node = createElement(Component, mergedProps); - return node; + return createElement(Component, mergedProps); }; + if (options?.forwardRef) { + const forwarded = forwardRef((props, ref) => { + return Wrapper({ ...props, ref: ref }); + }); + return forwarded as WrappedComponent; + } + return Wrapper; }; } diff --git a/packages/inula/src/inulax/adapters/reduxThunk.ts b/packages/inula/src/inulax/adapters/reduxThunk.ts index cd14fee6..7eaaf227 100644 --- a/packages/inula/src/inulax/adapters/reduxThunk.ts +++ b/packages/inula/src/inulax/adapters/reduxThunk.ts @@ -35,5 +35,5 @@ function createThunkMiddleware(extraArgument?: any): ReduxMiddleware { } export const thunk = createThunkMiddleware(); -// @ts-ignore -thunk.withExtraArgument = createThunkMiddleware; + +export const withExtraArgument = createThunkMiddleware; diff --git a/packages/inula/src/inulax/devtools/index.ts b/packages/inula/src/inulax/devtools/index.ts index 10c71a9e..01dc5c37 100644 --- a/packages/inula/src/inulax/devtools/index.ts +++ b/packages/inula/src/inulax/devtools/index.ts @@ -16,7 +16,7 @@ import { isMap, isSet, isWeakMap, isWeakSet } from '../CommonUtils'; import { getStore, getAllStores } from '../store/StoreHandler'; import { OBSERVED_COMPONENTS } from './constants'; -import { VNode } from "../../renderer/vnode/VNode"; +import { VNode } from '../../renderer/vnode/VNode'; const sessionId = Date.now(); diff --git a/packages/inula/src/renderer/hooks/UseReducerHook.ts b/packages/inula/src/renderer/hooks/UseReducerHook.ts index fe56820b..e0ae2f3a 100644 --- a/packages/inula/src/renderer/hooks/UseReducerHook.ts +++ b/packages/inula/src/renderer/hooks/UseReducerHook.ts @@ -21,7 +21,7 @@ import { setStateChange } from '../render/FunctionComponent'; import { getHookStage, HookStage } from './HookStage'; import type { VNode } from '../Types'; import { getProcessingVNode } from '../GlobalVar'; -import { markUpdatedInRender } from "./HookMain"; +import { markUpdatedInRender } from './HookMain'; // 构造新的Update数组 function insertUpdate(action: A, hook: Hook): Update { diff --git a/packages/inula/src/renderer/vnode/VNodeCreator.ts b/packages/inula/src/renderer/vnode/VNodeCreator.ts index 142bac33..55ee5247 100644 --- a/packages/inula/src/renderer/vnode/VNodeCreator.ts +++ b/packages/inula/src/renderer/vnode/VNodeCreator.ts @@ -74,7 +74,7 @@ export function getLazyVNodeTag(lazyComp: any): string { } else if (lazyComp !== undefined && lazyComp !== null && typeLazyMap[lazyComp.vtype]) { return typeLazyMap[lazyComp.vtype]; } - throw Error("Inula can't resolve the content of lazy"); + throw Error('Inula can\'t resolve the content of lazy'); } // 创建processing