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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 (
+
+
+
+
+
+ {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