Merge branch 'master' of gitee.com:openInula/inula

This commit is contained in:
c00364821 2023-12-01 11:07:17 +08:00
commit 530fb24289
29 changed files with 2345 additions and 94 deletions

View File

@ -1,5 +1,5 @@
{
"name": "openinula",
"name": "inula",
"description": "OpenInula is a JavaScript framework library.",
"version": "0.0.1",
"private": true,

View File

@ -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' });
});

View File

@ -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);
});
}
);
}
);

View File

@ -0,0 +1,30 @@
<!--
~ 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.
-->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src *; style-src 'self' 'unsafe-inline'; srcipt-src 'self' 'unsafe-inline' 'unsafe-eval' ">
<script src="inula.development.js"></script>
</head>
<body>
<div>
<p>Inula dev tools!</p>
</div>
</body>
<script> src="main.js"</script>
</html>

View File

@ -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 (
<>
<span
title={'Run action'}
onClick={() => {
if (attributes.length > 0) {
setData({ modal: false, gatheredAttrs: [], query: false });
} else {
executeAction(storeId, actionName, gatheredAttrs);
}
}}
>
<b
style={{
cursor: 'pointer',
}}
>
<span
title={'Add to action queue'}
onClick={e => {
e.preventDefault();
if (attributes.len > 0) {
setData({ modal: true, gatheredAttrs: [], query: true });
} else {
queryAction(storeId, actionName, gatheredAttrs);
}
}}
>
{' '}
</span>
</b>
<span>
<i>{plainFunction}</i>
{' {...}'}
</span>
</span>
{modalIsOpen ? (
<Modal
closeModal={() => {
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]),
});
}
}}
>
<h3>{data.query ? 'Query action:' : 'Run action:'}</h3>
<p>{highlight(plainFunction, attributes[gatheredAttrs.length])}</p>
</Modal>
) : null}
</>
);
}

View File

@ -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 (
<Tree
data={mutation.to}
indent={indent}
search={search}
expand={expand}
forcedExpand={forcedExpand}
omitAttrs={omitAttrs}
forcedLabel={forcedLabel}
/>
);
}
if (newValue) {
return (
<Tree
data={mutation.to}
indent={indent}
search={search}
expand={expand}
forcedExpand={forcedExpand}
className={styles.added}
omitAttrs={omitAttrs}
forcedLabel={forcedLabel}
/>
);
}
if (deleted) {
return (
<Tree
data={mutation.from}
indent={indent}
search={search}
expand={expand}
forcedExpand={forcedExpand}
className={styles.deleted}
omitAttrs={omitAttrs}
forcedLabel={forcedLabel}
/>
);
}
return (
<div
style={{
fontFamily: 'monospace',
}}
className={`${
expanded
? 'expanded'
: `not-expanded ${
mutated && !isPrimitive && !expanded ? styles.changed : ''
}`
}`}
onClick={e => {
e.stopPropagation();
}}
>
<span
style={{
cursor: 'pointer',
}}
onClick={() => {
setExpanded(!expanded);
}}
>
{new Array(Math.max(indent, 0)).fill(<span>&nbsp;</span>)}
{isPrimitive ? (
// 如果两个 value 是基本变量并且不同,则简单显示不同点
<div
onClick={e => {
e.stopPropagation();
}}
>
<Tree
data={mutation.from}
indent={indent}
search={search}
index={index}
className={styles.deleted}
omitAttrs={omitAttrs}
/>
<Tree
data={mutation.to}
indent={indent}
search={search}
index={index}
className={styles.added}
omitAttrs={omitAttrs}
/>
</div>
) : (
// 如果至少有一个是复杂变量,则需要展开按钮
<>
{forcedExpand ? '' : expanded ? <span></span> : <span></span>}
{index === 0 || index ? (
<b className={styles.purple}>{displayValue(index, search)}: </b>
) : (
''
)}
{isArray ? (
// 如果都是数组进行比较
expanded ? (
[
Array(Math.max(mutation.from.length, mutation.to.length))
.fill(true)
.map((i, index) => {
return (
<div>
{mutation.items[index].mutation ? (
<DiffTree
mutation={{
...mutation.items[index],
to: mutation.to[index],
}}
indent={indent}
search={search}
omitAttrs={omitAttrs}
forcedLabel={index}
/>
) : (
<Tree
data={mutation.to[index]}
indent={indent}
search={search}
index={index}
className={styles.default}
omitAttrs={omitAttrs}
/>
)}
</div>
);
}),
]
) : (
forcedLabel || `Array(${mutation.to?.length})`
)
) : isSet ? (
expanded ? (
<div>
<div>
{forcedLabel || `Set(${mutation.to?.values.length})`}
</div>
{Array(
Math.max(
mutation.from?.values.length,
mutation.to?.values.length
)
)
.fill(true)
.map((i ,index) => (
<div>
{mutation.values[index].mutation ? (
<DiffTree
mutation={{
...mutation.values[index],
}}
indent={indent + 2}
search={search}
omitAttrs={omitAttrs}
/>
) : (
<Tree
data={mutation.to?.values[index]}
indent={indent + 2}
search={search}
className={styles.default}
omitAttrs={omitAttrs}
/>
)}
</div>
))}
</div>
) : (
<span>
{forcedLabel || `Set(${mutation.to?.values.length})`}
</span>
)
) : isMap ? (
expanded ? (
<>
<span>
{forcedLabel || `Map(${mutation.to?.entries.length})`}
</span>
{Array(
Math.max(
mutation.from?.entries.length,
mutation.to?.entries.length
)
)
.fill(true)
.map((i, index) =>
mutation.entries[index].mutation ? (
<div>
<DiffTree
mutation={{
...mutation.entries[index],
}}
indent={indent + 2}
search={search}
omitAttrs={omitAttrs}
forcedLabel={'[map item]'}
/>
</div>
) : (
<div>
<Tree
data={mutation.to?.entries[index]}
indent={indent + 2}
search={search}
className={styles.default}
omitAttrs={omitAttrs}
forcedLabel={'[map item]'}
/>
</div>
)
)}
</>
) : (
<span>
{forcedLabel || `Map(${mutation.to?.entries.length})`}
</span>
)
) : expanded ? (
// 如果都是 object 进行比较
Object.entries(mutation.attributes).map(([key, item]) => {
return item.mutation ? (
<span onClick={e => e.stopPropagation()}>
{
<DiffTree
mutation={item}
index={key}
indent={indent}
search={search}
className={!expanded && mutated ? '' : styles.changed}
omitAttrs={omitAttrs}
/>
}
</span>
) : (
<span onClick={e => e.stopPropagation()}>
{
<Tree
data={mutation.to[key]}
index={key}
indent={indent}
search={search}
className={styles.default}
omitAttrs={omitAttrs}
/>
}
</span>
);
})
) : (
forcedLabel || '{ ... }'
)}
</>
)}
</span>
</div>
);
}

View File

@ -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 (
<div
onClick={e => {
e.stopPropagation();
}}
>
<Tree
data={{
Action: `${message.data.action.action}${
message.data.fromQueue ? ' (queued)' : ''
}`
}}
expand={true}
indent={-4}
forcedExpand={true}
search={search}
omitAttrs={['_inulaObserver']}
/>
</div>
);
}
if (message.type === eventTypes.STATE_CHANGE) {
return (
<div
onClick={e => {
e.stopPropagation();
}}
>
<b>{`${message.data.change.vNodes.length} nodes changed:`}</b>
<Tree
data={message.data.change.vNodes.map(vNode => {
return (
<span>
<i>{vNode.type}</i>()
</span>
);
})}
/>
</div>
);
}
return <span className={styles.grey}>N/A</span>
}
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 ? (
<DiffTree
mutation={event.message.data.change.mutation}
expand={true}
forcedExpand={true}
indent={0}
search={eventFilter['fulltext']}
omitAttrs={['_inulaObserver']}
doNotDisplayIcon={true}
/>
) : (
<Tree
data={event.message.data.store.$s}
expand={true}
search={eventFilter['fulltext']}
forcedExpand={true}
indent={-4}
omitAttrs={['_inulaObserver']}
/>
),
storeClick: (
<span
className={styles.link}
onClick={e => {
e.preventDefault();
setNextStore(event.message.data.store.id);
}}
>
{event.message.data.store.id}
</span>
),
additionalData: extractDataByType(
event.message,
eventFilter['fulltext']
),
storeId: event.message.data.store.id,
event,
};
});
return (
<div>
<div style={{ marginTop: '0px', margin: '5px' }}>
<input
ref={filterField}
type={'text'}
placeholder={'Filter:'}
className={`${styles.compositeInput} ${styles.left}`}
onKeyUp={() => {
if (!filterField.current.value) {
removeFilter('fulltext');
}
addFilter('fulltext', filterField.current.value);
}}
/>
<button
className={`${styles.bold} ${styles.compositeInput} ${styles.right}`}
onClick={() => {
filterField.current.value = '';
removeFilter('fulltext');
}}
>
X
</button>
<span className={styles.grey}>{' | '}</span>
<span
style={{
cursor: 'pointer'
}}
onClick={e => {
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);
}}
>
<Checkbox value={persistent}></Checkbox> Persistent events
</span>
{' | '}
<button
onClick={() => {
// 重置 events
chrome.runtime.sendMessage({
type: 'INULA_DEV_TOOLS',
payload: {
type: otherTypes.RESET_EVENTS,
tabId: chrome.devtools.inspectedWindow.tabId,
},
from: DevToolPanel,
});
}}
>
Reset
</button>
{eventFilter['message.data.store.id'] ? (
<span>
{' | '}
<b
style={{
cursor: 'pointer',
}}
onClick={() => {
setNextStore(eventFilter['message.data.store.id']);
}}
>{` Displaying: [${eventFilter['message.data.store.id']}] `}</b>
<button
onClick={() => {
removeFilter('message.data.store.id');
}}
>
X
</button>
</span>
) : null}
</div>
<div style={{ marginTop: '0px', margin: '5px' }}>
<button
className={`${styles.filterButton} ${log.length ? '' : styles.grey} ${
eventFilter['message.type'] ? '' : styles.active
}`}
onClick={() => {
removeFilter('message.type');
}}
>
All({usedTypes.all})
</button>
{Object.values(eventTypes).map(eventType => {
return (
<button
className={`${styles.filterButton} ${
usedTypes[eventType] ? '' : styles.grey
} ${
eventFilter['message.type'] === eventType ? styles.active : ''
}`}
onClick={() => {
addFilter('message.type', eventType);
}}
>
{`${eventType.replace('inulax ', '')}(${
usedTypes[eventType] || 0
})`}
</button>
);
})}
</div>
<Table
data={processedData}
dataKey={'id'}
displayKeys={[
['type', 'Event type:'],
['storeClick', 'Store:'],
['time', 'Time:'],
['state', 'State:'],
['additionalData', 'Additional data:'],
]}
displayDataProcessor={data => {
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 : ''}
/>
</div>
);
}

View File

@ -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 (
<div
style={{
position: 'fixed',
width: '100vw',
height: '100vh',
top: 0,
left: 0,
backgroundColor: 'rgba(0, 0, 0 , 0.3)',
}}
>
<div
style={{
top: 'calc(50vh - 50px)',
left: 'calc(50vw - 125px)',
width: '250px',
backgroundColor: 'white',
border: '1px solid black',
position: 'fixed',
textAlign: 'center',
}}
>
<p>{children}</p>
<p>
<input
ref={inputRef}
type={'text'}
onKeyPress={({key}) => {
if (key === 'Enter') {
tryGatherData();
}
}}
/>
</p>
{error ? <p>Variable parsing error</p> : null}
<p>
<button
onClick={() => {
tryGatherData();
}}
>
OK
</button>
<button
onClick={() => {
closeModal();
}}
>
Cancel
</button>
</p>
</div>
</div>
);
}

View File

@ -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;
}
}

View File

@ -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: () => (
<Stores
nextStoreId={nextStoreId}
showFilteredEvents={showFilterEvents}
/>
),
},
{
id: 'events',
title: 'Events',
getComponents: () => (
<EventLog
setNextStore={id => {
setNextStoreId(id);
setActive('stores');
}}
setEventFilter={setEventFilter}
eventFilter={eventFilter}
/>
),
},
];
return (
<div>
<div style={{ marginBottom: '10px' }}>
{tabs.map(tab =>
tab.id === active ? (
<button
className={`${styles.tab} ${styles.active}`}
disabled={true}
>
{tab.title}
</button>
) : (
<button
className={styles.tab}
onClick={() => {
setActive(tab.id);
}}
>
{tab.title}
</button>
)
)}
</div>
{tabs.find(item => item.id === active).getComponent()}
</div>
);
}

View File

@ -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);
};
});
}

View File

@ -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 (
<div className={styles.wrapper}>
<div className={`${styles.table} ${styles.half}`}>
<div className={styles.row}>
<div className={`${styles.cell} ${styles.header}`}>{title}</div>
</div>
<div className={styles.scrollable}>
<span></span>
{data.map(row => (
<div
className={`${styles.row} ${
keyToDisplay === row[dataKey] ? styles.active : ''
}`}
onClick={() => {
setManualOverride(true);
setKeyToDisplay(
keyToDisplay === row[dataKey] ? null : row[dataKey]
);
}}
>
<div className={styles.cell}>{row?.[attr] || ''}</div>
</div>
))}
</div>
</div>
<div className={`${styles.table} ${styles.half} ${styles.displayData}`}>
<div className={styles.row}>
<div className={styles.cell}>
<b>Data:</b>
<button
className={styles.floatingButton}
onClick={() => {
setKeyToDisplay(null);
}}
>
X
</button>
</div>
</div>
<div className={styles.scrollable}>
<span></span>
<div className={styles.row}>
<div className={styles.cell}>
<Tree
data={
displayDataProcessor
? displayDataProcessor(displayRow)
: displayRow
}
indent={displayRow[displayKeys[0][0]]}
expand={true}
search={search}
forcedExpand={true}
/>
</div>
</div>
</div>
</div>
</div>
);
} else {
return (
<div className={styles.wrapper}>
<div className={styles.table}>
<div className={`${styles.row} ${styles.header}`}>
{displayKeys.map(([key, title]) => (
<div className={styles.cell}>{title}</div>
))}
</div>
{data.map(item => (
<div
onClick={() => {
setManualOverride(true);
setKeyToDisplay(item[dataKey]);
}}
className={styles.row}
>
{displayKeys.map(([key, title]) => (
<div className={styles.cell}>{item[key]}</div>
))}
</div>
))}
</div>
</div>
);
}
}

View File

@ -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 ? (
<div
style={{ fontFamily: 'monoSpace' }}
className={`${expanded ? 'expanded' : 'not-expanded'} ${className}`}
onClick={e => {
e.stopPropagation();
}}
>
<span
style={{ cursor: 'pointer' }}
onClick={() => {
setExpanded(!expanded);
}}
>
{new Array(Math.max(indent, 0)).fill(<span>&nbsp;</span>)}
{forcedExpand || isVNode ? null : expanded ? (
<span></span>
) : (
<span></span>
)}
{index === 0 || index ? (
<>
<b className={styles.purple}>{displayValue(index, search)}: </b>
</>
) : (
''
)}
{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})`
: '{ ... }'}
</span>
{expanded || isVNode ? (
isArray ? (
<>
{data.map((value, index) => {
return (
<div>
<Tree
data={value}
indent={indent + 4}
index={index}
search={search}
className={className}
onEdit={
onEdit
? (path, val) => {
onEdit(path.concat([index]), val);
}
: null
}
/>
</div>
);
})}
</>
) : isVNode ? (
data
) : isMap ? (
<div>
{data.entries.map(([key, value]) => {
return (
<Tree
data={{key, value}}
indent={indent + 4}
search={search}
className={className}
// TODO: editable sets
/>
);
})}
</div>
) : isSet ? (
data.values.map(item => {
return (
<div>
<Tree
data={item}
indent={indent + 4}
search={search}
className={className}
// TODO: editable sets
/>
</div>
);
})
) : (
Object.entries(data).map(([key, value]) => {
return (
<div>
<Tree
data={value}
indent={indent + 4}
index={key}
search={search}
className={className}
onEdit={
onEdit
? (path, val) => {
onEdit(path.concat([key]), val);
}
: null
}
/>
</div>
);
})
)
) : (
''
)}
</div>
) : (
<div className={'not-expanded'}>
{new Array(indent).fill(<span>&nbsp;</span>)}
<span className={`${className}`}>
{typeof index !== 'undefined' ? (
<>
<b className={styles.purple}>{displayValue(index, search)}: </b>
</>
) : (
''
)}
{displayValue(data, search)}
{onEdit && !isWeakSet && !isWeakMap ? ( // TODO: editable weak set and map
<>
<b
style={{ cursor: 'pointer' }}
onClick={() => {
setModal(true);
}}
>
</b>
{onEdit && modal ? (
<Modal
closeModal={() => {
setModal(false);
}}
then={data => {
onEdit([], data);
setModal(false);
}}
>
<h3>Edit value:</h3> {index}
</Modal>
) : null}
</>
) : null}
</span>
</div>
);
}

View File

@ -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;

View File

@ -0,0 +1,49 @@
<!--
~ 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.
-->
<!doctype html>
<html style="display: flex">
<head>
<meta charset="utf8">
<meta http-equiv="Content-Security-Policy"
content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' ">
<style>
html {
width: 100%;
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#root {
width: 100%;
height: 100%;
}
</style>
<script src="inula.development.js"></script>
</head>
<body>
<div id="root"></div>
<script src="panelX.js"></script>
</body>
</html>

View File

@ -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(<span className={styles.highlighted}>{search}</span>);
} else {
result.push(parts[i / 2]);
}
}
return result;
}
export function displayValue(val: any, search = '') {
if (typeof val === 'boolean') {
return (
<span>
{highlight(val ? 'true' : 'false', search)}
</span>
);
}
if (val === '') {
return <span className={styles.red}>{'""'}</span>;
}
if (typeof val === 'undefined') {
return <span className={styles.grey}>{highlight('undefined', search)}</span>;
}
if (val === 'null') {
return <span className={styles.grey}>{highlight('null', search)}</span>;
}
if (typeof val === 'string') {
if (val.match(/^function\s?\(/)) {
return (
<span>
<i>ƒ</i>
{highlight(
val.match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''),
search
)}
</span>
);
}
return <span className={styles.red}>"{highlight(val, search)}"</span>;
}
if (typeof val === 'number') {
return <span className={styles.blue}>{highlight('' + val, search)}</span>;
}
if (typeof val === 'function') {
const args = val.toString().match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, '');
return (
<span>
<i>ƒ</i>
{highlight(args, search)}
</span>
);
}
if (typeof val === 'object') {
if (val?._type === 'WeakSet') {
return <span>WeakSet()</span>;
}
if (val?._type === 'WeakMap') {
return <span>WeakMap()</span>;
}
}
}
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 (
<span>
<i>ƒ</i>
{data.match(/^function\([\w,]*\)/g)[0].substring(8)}
</span>
);
}
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 (
<span>
<span className={styles.purple}>{key}</span>
<span>
<i>ƒ</i>
{value.match(/^function\([\w,]*\)/g)[0].substring(8)}
</span>
</span>
);
}
if (!value) {
return (
<span>
<span className={styles.purple}>{key}</span>:{displayValue(value)}
</span>
);
}
if (Array.isArray(value)) {
return (
<span>
<span className={styles.purple}>{key}</span>:{' '}
{`Array(${value.length})`}
</span>
);
}
if (typeof value === 'object') {
if ((value as any)?._type === 'WeakSet') {
return (
<span>
<span className={styles.purple}>{key}</span>: {'WeakSet()'}
</span>
);
}
if ((value as any)?._type === 'WeakMap') {
return (
<span>
<span className={styles.purple}>{key}</span>: {'WeakMap'}
</span>
);
}
if ((value as any)?._type === 'Set') {
return (
<span>
<span className={styles.purple}>{key}</span>:{' '}
{`Set(${(value as Set<any>).size})`}
</span>
);
}
if ((value as any)?._type === 'Map') {
return (
<span>
<span className={styles.purple}>{key}</span>:{' '}
{`Map(${(value as Map<any, any>).size})`}
</span>
);
}
// object
return (
<span>
<span className={styles.purple}>{key}</span>: {'{...}'}
</span>
);
}
return (
<span>
<span className={styles.purple}>{key}</span>: {displayValue(value)}
</span>
);
})}}`;
}
return data;
}
export function sendMessage(payload) {
chrome.runtime.sendMessage({
type: 'INULA_DEV_TOOLS',
payload,
from: DevToolPanel,
});
}

View File

@ -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;

View File

@ -1,6 +1,6 @@
{
"name": "inula-intl",
"version": "0.0.2",
"version": "0.0.3",
"description": "",
"main": "build/intl.umd.js",
"type": "commonjs",

View File

@ -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": {

View File

@ -41,4 +41,7 @@ export default {
presets: ['@babel/preset-env']
})
],
external:[
'openinula'
],
};

View File

@ -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 获取开源许可协议的更多信息.

View File

@ -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');

View File

@ -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) {

View File

@ -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<T = any> = {
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<T = StoreCreator>(...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);

View File

@ -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<StateProps, DispatchProps, OwnProps, MergedProps> = (
type WrappedComponent<OwnProps> = (props: OwnProps) => ReturnType<typeof createElement>;
type OriginalComponent<MergedProps> = (props: MergedProps) => ReturnType<typeof createElement>;
type Connector<OwnProps, MergedProps> = (Component: OriginalComponent<MergedProps>) => WrappedComponent<OwnProps>;
type ConnectOption<State = any> = {
areStatesEqual?: (oldState: State, newState: State) => boolean;
context?: Context;
forwardRef?: boolean;
}
export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
mapStateToProps: MapStateToPropsP<StateProps, OwnProps> = () => ({} as StateProps),
@ -99,10 +103,7 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
dispatchProps,
ownProps
): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as unknown as MergedProps),
options?: {
areStatesEqual?: (oldState: any, newState: any) => boolean;
context?: Context;
}
options?: ConnectOption
): Connector<OwnProps, MergedProps> {
if (!options) {
options = {};
@ -114,37 +115,31 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
//this component should mimic original type of component used
const Wrapper: WrappedComponent<OwnProps> = (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<StateProps, DispatchProps, OwnProps, MergedProps>(
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<StateProps, DispatchProps, OwnProps, MergedProps>(
})
)(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<OwnProps>;
}
return Wrapper;
};
}

View File

@ -35,5 +35,5 @@ function createThunkMiddleware(extraArgument?: any): ReduxMiddleware {
}
export const thunk = createThunkMiddleware();
// @ts-ignore
thunk.withExtraArgument = createThunkMiddleware;
export const withExtraArgument = createThunkMiddleware;

View File

@ -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();

View File

@ -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<S, A>(action: A, hook: Hook<S, A>): Update<S, A> {

View File

@ -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