Match-id-7133f2734eacafd1263023fbe3921097f915d497

This commit is contained in:
* 2022-03-31 10:25:52 +08:00 committed by *
parent 5cde85df95
commit e13f3245bd
21 changed files with 155 additions and 122 deletions

View File

@ -35,7 +35,7 @@ function ComponentAttr({ name, attr }: { name: string, attr: IAttr[] }) {
newSet.add(index); newSet.add(index);
} }
setCollapsedNode(newSet); setCollapsedNode(newSet);
} };
const showAttr = []; const showAttr = [];
let currentIndentation = null; let currentIndentation = null;
@ -76,7 +76,7 @@ function ComponentAttr({ name, attr }: { name: string, attr: IAttr[] }) {
{showAttr} {showAttr}
</div> </div>
</div> </div>
) );
} }
export default function ComponentInfo({ name, attrs }: IComponentInfo) { export default function ComponentInfo({ name, attrs }: IComponentInfo) {
@ -104,5 +104,5 @@ export default function ComponentInfo({ name, attrs }: IComponentInfo) {
</div> </div>
</div> </div>
</div> </div>
) );
} }

View File

@ -68,6 +68,7 @@
.attrDetail { .attrDetail {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
.attrArrow { .attrArrow {
color: @arrow-color; color: @arrow-color;
width: 12px; width: 12px;

View File

@ -1,3 +1,3 @@
.search { .search {
width: 100%; width: 100%;
} }

View File

@ -8,12 +8,12 @@ export default function Search(props: SearchProps) {
const { onChange } = props; const { onChange } = props;
const handleChange = (event) => { const handleChange = (event) => {
onChange(event.target.value); onChange(event.target.value);
} };
return ( return (
<input <input
onChange={handleChange} onChange={handleChange}
className={styles.search} className={styles.search}
placeholder={'Search (text or /regex/)'} placeholder={'Search (text or /regex/)'}
/> />
) );
} }

View File

@ -5,10 +5,12 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
.treeItem { .treeItem {
width: 100%; width: 100%;
position: absolute; position: absolute;
line-height: 18px; line-height: 18px;
&:hover { &:hover {
background-color: @select-color; background-color: @select-color;
} }
@ -19,15 +21,15 @@
width: 12px; width: 12px;
padding-left: 0.5rem; padding-left: 0.5rem;
} }
.componentName { .componentName {
color: @component-name-color; color: @component-name-color;
} }
.componentKeyName { .componentKeyName {
color: @component-key-color; color: @component-key-color;
} }
.componentKeyValue { .componentKeyValue {
color: @componentKeyValue-color; color: @componentKeyValue-color;
} }
@ -37,4 +39,3 @@
background-color: rgb(141 199 248 / 60%); background-color: rgb(141 199 248 / 60%);
} }
} }

View File

@ -26,14 +26,14 @@ const lineHeight = 18;
const indentationLength = 20; const indentationLength = 20;
function Item(props: IItem) { function Item(props: IItem) {
const { const {
name, name,
style, style,
userKey, userKey,
hasChild, hasChild,
onCollapse, onCollapse,
isCollapsed, isCollapsed,
id, id,
indentation, indentation,
onClick, onClick,
isSelect, isSelect,
@ -43,14 +43,14 @@ function Item(props: IItem) {
const showIcon = hasChild ? <Arrow director={isCollapsed ? 'right' : 'down'} /> : ''; const showIcon = hasChild ? <Arrow director={isCollapsed ? 'right' : 'down'} /> : '';
const handleClickCollapse = () => { const handleClickCollapse = () => {
onCollapse(id); onCollapse(id);
} };
const handleClick = () => { const handleClick = () => {
onClick(id); onClick(id);
} };
const itemAttr: any = {style, className: styles.treeItem, onClick: handleClick}; const itemAttr: any = { style, className: styles.treeItem, onClick: handleClick };
if (isSelect) { if (isSelect) {
itemAttr.tabIndex = 0; itemAttr.tabIndex = 0;
itemAttr.className = styles.treeItem + ' ' + styles.select itemAttr.className = styles.treeItem + ' ' + styles.select;
} }
const reg = createRegExp(highlightValue); const reg = createRegExp(highlightValue);
const heightCharacters = name.match(reg); const heightCharacters = name.match(reg);
@ -60,7 +60,7 @@ function Item(props: IItem) {
showName = []; showName = [];
// 高亮第一次匹配即可 // 高亮第一次匹配即可
const char = heightCharacters[0]; const char = heightCharacters[0];
let index = name.search(char); const index = name.search(char);
const notHighlightStr = cutName.slice(0, index); const notHighlightStr = cutName.slice(0, index);
showName.push(notHighlightStr); showName.push(notHighlightStr);
showName.push(<mark>{char}</mark>); showName.push(<mark>{char}</mark>);
@ -90,7 +90,7 @@ function Item(props: IItem) {
</> </>
)} )}
</div> </div>
) );
} }
function VTree({ data, highlightValue }: { data: IData[], highlightValue: string }) { function VTree({ data, highlightValue }: { data: IData[], highlightValue: string }) {
@ -128,10 +128,11 @@ function VTree({ data, highlightValue }: { data: IData[], highlightValue: string
currentCollapseIndentation = null; currentCollapseIndentation = null;
} }
} }
let id = item.id; const id = item.id;
const isCollapsed = collapseNode.has(id); const isCollapsed = collapseNode.has(id);
if (totalHeight >= scrollTop && showList.length <= showNum) { if (totalHeight >= scrollTop && showList.length <= showNum) {
const nextItem = data[index + 1]; const nextItem = data[index + 1];
// 如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
const hasChild = nextItem ? nextItem.indentation > item.indentation : false; const hasChild = nextItem ? nextItem.indentation > item.indentation : false;
showList.push( showList.push(
<Item <Item
@ -146,7 +147,7 @@ function VTree({ data, highlightValue }: { data: IData[], highlightValue: string
isSelect={id === selectItem} isSelect={id === selectItem}
highlightValue={highlightValue} highlightValue={highlightValue}
{...item} /> {...item} />
) );
} }
totalHeight = totalHeight + lineHeight; totalHeight = totalHeight + lineHeight;
if (isCollapsed) { if (isCollapsed) {
@ -155,19 +156,19 @@ function VTree({ data, highlightValue }: { data: IData[], highlightValue: string
} }
}); });
const scroll = (event: any) => { const handleScroll = (event: any) => {
const scrollTop = event.target.scrollTop; const scrollTop = event.target.scrollTop;
// 顶部留 100px 冗余高度 // 顶部留 100px 冗余高度
setScrollTop(Math.max(scrollTop - 100, 0)); setScrollTop(Math.max(scrollTop - 100, 0));
} };
return ( return (
<div className={styles.treeContainer} onScroll={scroll}> <div className={styles.treeContainer} onScroll={handleScroll}>
{showList} {showList}
{/* 确保有足够的高度 */} {/* 确保有足够的高度 */}
<div style={{ marginTop: totalHeight }} /> <div style={{ marginTop: totalHeight }} />
</div> </div>
) );
} }
export default VTree; export default VTree;

View File

@ -1,5 +1,5 @@
@arrow-color:rgb(95, 99, 104); @arrow-color: rgb(95, 99, 104);
@divider-color:rgb(202, 205, 209); @divider-color: rgb(202, 205, 209);
@attr-name-color: rgb(200, 0, 0); @attr-name-color: rgb(200, 0, 0);
@component-name-color: rgb(136, 18, 128); @component-name-color: rgb(136, 18, 128);
@component-key-color: rgb(153, 69, 0); @component-key-color: rgb(153, 69, 0);
@ -11,4 +11,4 @@
@divider-width: 0.2px; @divider-width: 0.2px;
@common-font-size: 12px; @common-font-size: 12px;
@divider-style: @divider-color solid @divider-width; @divider-style: @divider-color solid @divider-width;

View File

@ -1,3 +1,7 @@
/**
* tree VNode
* getMockVNodeTree
*/
import { parseAttr } from '../parser/parseAttr'; import { parseAttr } from '../parser/parseAttr';
import parseTreeRoot from '../parser/parseVNode'; import parseTreeRoot from '../parser/parseVNode';
@ -9,7 +13,7 @@ const mockComponentNames = ['Apple', 'Pear', 'Banana', 'Orange', 'Jenny', 'Kiwi'
function MockVNode(tag: string, props = {}, key = null, realNode = {}) { function MockVNode(tag: string, props = {}, key = null, realNode = {}) {
const vNode = new VNode(tag, props, key, realNode); const vNode = new VNode(tag, props, key, realNode);
const name = mockComponentNames.shift() || 'MockComponent'; const name = mockComponentNames.shift() || 'MockComponent';
vNode.type = {name}; vNode.type = { name };
return vNode; return vNode;
} }
@ -18,41 +22,49 @@ interface IMockTree {
children?: IMockTree[], children?: IMockTree[],
} }
// 模拟树
const tree: IMockTree = { const tree: IMockTree = {
tag: ClassComponent, tag: ClassComponent,
children: [ children: [
{tag: FunctionComponent}, { tag: FunctionComponent },
{tag: ClassComponent}, { tag: ClassComponent },
{tag: FunctionComponent}, { tag: FunctionComponent },
{ {
tag: FunctionComponent, tag: FunctionComponent,
children: [ children: [
{tag: ClassComponent} { tag: ClassComponent }
] ]
} }
] ]
} };
function addOneThousandNode(node: IMockTree) { function addOneThousandNode(node: IMockTree) {
const nodes = []; const nodes = [];
for(let i = 0; i < 1000; i++) { for (let i = 0; i < 1000; i++) {
nodes.push({tag: FunctionComponent}); nodes.push({ tag: FunctionComponent });
} }
node?.children.push({tag: ClassComponent,children: nodes}); node?.children.push({ tag: ClassComponent, children: nodes });
}; }
addOneThousandNode(tree); addOneThousandNode(tree);
function getMockVNodeTree(tree: IMockTree, vNode: VNode) { /**
const children = tree.children; * mock数据转变为 VNode
*
* @param node
* @param vNode VNode节点
*/
function getMockVNodeTree(node: IMockTree, vNode: VNode) {
const children = node.children;
if (children && children.length !== 0) { if (children && children.length !== 0) {
const childNode = children[0]; const childNode = children[0];
let childVNode = MockVNode(childNode.tag); let childVNode = MockVNode(childNode.tag);
childVNode.key = '0'; childVNode.key = '0';
getMockVNodeTree(childNode, childVNode); getMockVNodeTree(childNode, childVNode);
// 需要建立双链
vNode.child = childVNode; vNode.child = childVNode;
childVNode.parent = vNode; childVNode.parent = vNode;
for(let i = 1; i < children.length; i++) { for (let i = 1; i < children.length; i++) {
const nextNode = children[i]; const nextNode = children[i];
const nextVNode = MockVNode(nextNode.tag); const nextVNode = MockVNode(nextNode.tag);
nextVNode.key = String(i); nextVNode.key = String(i);
@ -68,20 +80,19 @@ getMockVNodeTree(tree, rootVNode);
export const mockParsedVNodeData = parseTreeRoot(rootVNode); export const mockParsedVNodeData = parseTreeRoot(rootVNode);
const mockState = { const mockState = {
str: 'jenny', str: 'jenny',
num: 3, num: 3,
boolean: true, boolean: true,
und: undefined, und: undefined,
fun: () => {}, fun: () => ({}),
symbol: Symbol('sym'), symbol: Symbol('sym'),
map: new Map([['a', 'a']]), map: new Map([['a', 'a']]),
set: new Set(['a', 1, 2, Symbol('bambi')]), set: new Set(['a', 1, 2, Symbol('bambi')]),
arr: [1,2,3,4], arr: [1, 2, 3, 4],
obj: { obj: {
niko: {jenny: 'jenny'} niko: { jenny: 'jenny' }
} }
}; };
export const parsedMockState = parseAttr(mockState); export const parsedMockState = parseAttr(mockState);

View File

@ -1,48 +1,53 @@
@import '../components/assets.less'; @import '../components/assets.less';
.app{ .app {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 100%; height: 100%;
font-size: @common-font-size; font-size: @common-font-size;
} }
.left{ .left {
flex: 7; flex: 7;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.left_top { .left_top {
border-bottom: @divider-style; border-bottom: @divider-style;
flex: 0 0 @top-height; flex: 0 0 @top-height;
display: flex; display: flex;
align-items: center; align-items: center;
.select { .select {
padding: 0 0.25rem 0 0.25rem; padding: 0 0.25rem 0 0.25rem;
flex: 0 0; flex: 0 0;
} }
.divider { .divider {
flex: 0 0 1px; flex: 0 0 1px;
margin: 0 0.25rem 0 0.25rem; margin: 0 0.25rem 0 0.25rem;
border-left: @divider-style; border-left: @divider-style;
height: calc(100% - 1rem); height: calc(100% - 1rem);
} }
.search { .search {
flex: 1 1 0; flex: 1 1 0;
} }
} }
.left_bottom { .left_bottom {
flex: 1; flex: 1;
height: 0; height: 0;
} }
} }
.right{ .right {
flex: 3; flex: 3;
border-left: @divider-style; border-left: @divider-style;
} }
input{ input {
outline: none; outline: none;
border-width: 0; border-width: 0;
padding: 0; padding: 0;
} }

View File

@ -1,4 +1,4 @@
import {useState, useEffect} from 'horizon'; import { useState, useEffect } from 'horizon';
import VTree, { IData } from '../components/VTree'; import VTree, { IData } from '../components/VTree';
import Search from '../components/Search'; import Search from '../components/Search';
import ComponentInfo from '../components/ComponentInfo'; import ComponentInfo from '../components/ComponentInfo';
@ -8,7 +8,7 @@ import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
function App() { function App() {
const [parsedVNodeData, setParsedVNodeData] = useState([]); const [parsedVNodeData, setParsedVNodeData] = useState([]);
const [componentInfo, setComponentInfo] = useState({name: null, attrs: {}}); const [componentInfo, setComponentInfo] = useState({ name: null, attrs: {} });
const [filterValue, setFilterValue] = useState(''); const [filterValue, setFilterValue] = useState('');
useEffect(() => { useEffect(() => {
if (isDev) { if (isDev) {
@ -19,7 +19,7 @@ function App() {
state: parsedMockState, state: parsedMockState,
props: parsedMockState, props: parsedMockState,
}, },
}) });
} }
}, []); }, []);
const idIndentationMap: { const idIndentationMap: {
@ -27,7 +27,7 @@ function App() {
} = {}; } = {};
const data: IData[] = []; const data: IData[] = [];
let i = 0; let i = 0;
while(i < parsedVNodeData.length) { while (i < parsedVNodeData.length) {
const id = parsedVNodeData[i] as string; const id = parsedVNodeData[i] as string;
i++; i++;
const name = parsedVNodeData[i] as string; const name = parsedVNodeData[i] as string;
@ -46,7 +46,7 @@ function App() {
const handleSearchChange = (str: string) => { const handleSearchChange = (str: string) => {
setFilterValue(str); setFilterValue(str);
} };
return ( return (
<div className={styles.app}> <div className={styles.app}>
@ -57,15 +57,15 @@ function App() {
</div> </div>
<div className={styles.divider} /> <div className={styles.divider} />
<div className={styles.search}> <div className={styles.search}>
<Search onChange={handleSearchChange}/> <Search onChange={handleSearchChange} />
</div> </div>
</div> </div>
<div className={styles.left_bottom}> <div className={styles.left_bottom}>
<VTree data={data} highlightValue={filterValue}/> <VTree data={data} highlightValue={filterValue} />
</div> </div>
</div> </div>
<div className={styles.right}> <div className={styles.right}>
<ComponentInfo name={componentInfo.name} attrs={componentInfo.attrs}/> <ComponentInfo name={componentInfo.name} attrs={componentInfo.attrs} />
</div> </div>
</div> </div>
); );

View File

@ -1,7 +1,7 @@
import {render} from 'horizon'; import { render } from 'horizon';
import App from './App'; import App from './App';
render( render(
<App />, <App />,
document.getElementById('root') document.getElementById('root')
); );

View File

@ -1,27 +1,34 @@
<!doctype html> <!doctype html>
<html> <html>
<head>
<meta charset="utf8">
<script src='horizon.production.js'></script>
<style>
html {
width: 100%;
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#root {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="root"></div> <head>
</body> <meta charset="utf8">
<title>Horizon</title>
<script src='horizon.production.js'></script>
<style>
html {
width: 100%;
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#root {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html> </html>

View File

@ -1,6 +1,13 @@
// 将状态的值解析成固定格式
export function parseAttr(rootAttr: any) { export function parseAttr(rootAttr: any) {
const result = []; const result: {
let indentation = 0; name: string,
type: string,
value: string,
indentation: number
}[] = [];
const indentation = 0;
const parseSubAttr = (attr: any, parentIndentation: number, attrName: string) => { const parseSubAttr = (attr: any, parentIndentation: number, attrName: string) => {
const stateType = typeof attr; const stateType = typeof attr;
let value: any; let value: any;
@ -20,7 +27,7 @@ export function parseAttr(rootAttr: any) {
} else if (stateType === 'object') { } else if (stateType === 'object') {
if (attr === null) { if (attr === null) {
showType = 'null'; showType = 'null';
}else if (attr instanceof Map) { } else if (attr instanceof Map) {
showType = 'map'; showType = 'map';
const size = attr.size; const size = attr.size;
value = `Map(${size})`; value = `Map(${size})`;
@ -28,7 +35,7 @@ export function parseAttr(rootAttr: any) {
attr.forEach((value, key) => { attr.forEach((value, key) => {
parseSubAttr(value, parentIndentation + 2, key); parseSubAttr(value, parentIndentation + 2, key);
}); });
} };
} else if (attr instanceof Set) { } else if (attr instanceof Set) {
showType = 'set'; showType = 'set';
const size = attr.size; const size = attr.size;
@ -46,8 +53,8 @@ export function parseAttr(rootAttr: any) {
addSubState = () => { addSubState = () => {
attr.forEach((value, index) => { attr.forEach((value, index) => {
parseSubAttr(value, parentIndentation + 2, String(index)); parseSubAttr(value, parentIndentation + 2, String(index));
}) });
} };
} else { } else {
showType = stateType; showType = stateType;
value = '{...}'; value = '{...}';

View File

@ -1,7 +1,8 @@
import { travelVNodeTree } from "../../../../libs/horizon/src/renderer/vnode/VNodeUtils"; import { travelVNodeTree } from '../../../../libs/horizon/src/renderer/vnode/VNodeUtils';
import { VNode } from "../../../../libs/horizon/src/renderer/Types"; import { VNode } from '../../../../libs/horizon/src/renderer/Types';
import { ClassComponent, FunctionComponent } from "../../../../libs/horizon/src/renderer/vnode/VNodeTags"; import { ClassComponent, FunctionComponent } from '../../../../libs/horizon/src/renderer/vnode/VNodeTags';
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
const VNodeToIdMap = new Map<VNode, number>(); const VNodeToIdMap = new Map<VNode, number>();
const IdToVNodeMap = new Map<number, VNode>(); const IdToVNodeMap = new Map<number, VNode>();
@ -34,7 +35,7 @@ function parseTreeRoot(treeRoot: VNode) {
if (isUserComponent(tag)) { if (isUserComponent(tag)) {
const id = generateUid(); const id = generateUid();
result.push(id); result.push(id);
const name = (node.type as Function).name; const name = node.type.name;
result.push(name); result.push(name);
const parent = getParentUserComponent(node); const parent = getParentUserComponent(node);
if (parent) { if (parent) {

View File

@ -2,16 +2,16 @@ interface IArrow {
director: 'right' | 'down' director: 'right' | 'down'
} }
export default function Arrow({director}: IArrow) { export default function Arrow({ director }: IArrow) {
let d: string; let d: string;
if (director === 'right') { if (director === 'right') {
d = 'm2 0l12 8l-12 8 z' d = 'm2 0l12 8l-12 8 z';
} else if (director === 'down') { } else if (director === 'down') {
d = 'm0 2h16 l-8 12 z'; d = 'm0 2h16 l-8 12 z';
} }
return ( return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='8px' height='8px'> <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='8px' height='8px'>
<path d={d} fill='currentColor'/> <path d={d} fill='currentColor' />
</svg> </svg>
) );
} }

View File

@ -2,7 +2,7 @@
export default function Copy() { export default function Copy() {
return ( return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'> <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d='M0 0 H16 V16 H0 z M2 2 H8 V8 H2 V2z' fill='currentColor' fill-rule='evenodd'/> <path d='M0 0 H16 V16 H0 z M2 2 H8 V8 H2 V2z' fill='currentColor' fill-rule='evenodd' />
</svg> </svg>
) );
} }

View File

@ -4,5 +4,5 @@ export default function Debug() {
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'> <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d='m2 0l12 8l-12 8 z' fill='#000'/> <path d='m2 0l12 8l-12 8 z' fill='#000'/>
</svg> </svg>
) );
} }

View File

@ -3,8 +3,8 @@ export default function Eye() {
return ( return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'> <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<ellipse cx="8" cy="8" rx="8" ry="6" /> <ellipse cx="8" cy="8" rx="8" ry="6" />
<circle cx="8" cy="8" r= "4" fill="rgb(255, 255, 255)"/> <circle cx="8" cy="8" r="4" fill="rgb(255, 255, 255)" />
<circle cx="8" cy="8" r= "2" fill="#000000"/> <circle cx="8" cy="8" r="2" fill="#000000" />
</svg> </svg>
) );
} }

View File

@ -1,9 +1,8 @@
export default function Select() { export default function Select() {
return ( return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'> <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d='M14 6 V3 C14 2.5 13.5 2 13 2 H3 C2.5 2 2 2.5 2 3 V13 C2 13.5 2.5 14 3 14H6 V13 H3 V3 H13 V6z M7 7 L9 15 L11 12 L14 15 L15 14 L12 11 L15 9z' fill='#000' /> <path d='M14 6 V3 C14 2.5 13.5 2 13 2 H3 C2.5 2 2 2.5 2 3 V13 C2 13.5 2.5 14 3 14H6 V13 H3 V3 H13 V6z M7 7 L9 15 L11 12 L14 15 L15 14 L12 11 L15 9z' fill='#000' />
</svg> </svg>
) );
} }

View File

@ -1,5 +1,5 @@
export function createRegExp(expression: string){ export function createRegExp(expression: string) {
let str = expression; let str = expression;
if (str[0] === '/') { if (str[0] === '/') {
str = str.slice(1); str = str.slice(1);
@ -9,7 +9,7 @@ export function createRegExp(expression: string){
} }
try { try {
return new RegExp(str, 'i'); return new RegExp(str, 'i');
}catch(err) { } catch (err) {
return null; return null;
} }
} }

View File

@ -30,8 +30,8 @@ module.exports = {
'@babel/preset-typescript', '@babel/preset-typescript',
['@babel/preset-react', { ['@babel/preset-react', {
runtime: 'classic', runtime: 'classic',
"pragma": "Horizon.createElement", 'pragma': 'Horizon.createElement',
"pragmaFrag": "Horizon.Fragment", 'pragmaFrag': 'Horizon.Fragment',
}]], }]],
plugins: ['@babel/plugin-proposal-class-properties'], plugins: ['@babel/plugin-proposal-class-properties'],
} }
@ -41,14 +41,14 @@ module.exports = {
{ {
test: /\.less/i, test: /\.less/i,
use: [ use: [
"style-loader", 'style-loader',
{ {
loader: "css-loader", loader: 'css-loader',
options: { options: {
modules: true, modules: true,
} }
}, },
'less-loader'], 'less-loader'],
}] }]
}, },