diff --git a/.cloudbuild/release.sh b/.cloudbuild/release.sh index 55565e19..11171071 100644 --- a/.cloudbuild/release.sh +++ b/.cloudbuild/release.sh @@ -20,7 +20,7 @@ if [ -n "${releaseVersion}" ] ; then cd umd # umd生产包多暴露全局名HorizonDOM # 以解决webpack的externals react-dom和react都指向Horizon时,webpack随机使用key名造成源码交付问题 - sed -i '$a window.HorizonDOM = window.Horizon;' horizon.production.js + sed -i '$a window.HorizonDOM = window.Horizon;' horizon.production.min.js cd - # 写入新版本号 diff --git a/.cloudbuild/test.yml b/.cloudbuild/test.yml index 0593afd8..a5c7242f 100644 --- a/.cloudbuild/test.yml +++ b/.cloudbuild/test.yml @@ -22,8 +22,8 @@ steps: - checkout: path: horizon-core - gitlab: - url: https://szv-open.codehub.huawei.com/innersource/shanhai/wutong/react/horizon-test.git - branch: one_tree_dev + url: https://szv-open.codehub.huawei.com/innersource/fenghuang/horizon/horizon-test.git + branch: master path: horizon-test BUILD: - build_execute: diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b9cfbc..12d81d5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 0.0.38 (2023-02-01) +- **core**: 增加flushSync接口 + +## 0.0.37 (2023-01-31) +- **core**: 增加jsxs方法 + +## 0.0.36 (2023-01-30) +- **core**: #100 horizon从上层页面透传到iframe页面里使用,创建的dom元素instanceof HTMLElement为false + +## 0.0.35 (2023-01-28) +- **core**: 在 cloneDeep JSXElement 的时候会出现死循环 + +## 0.0.34 (2023-01-19) +- **core**: #95 新增jsx接口 +- **core**: #96 #97 fix testing-library 的UT错误 + +## 0.0.33 (2023-01-11) +- **horizonX-devtool**: 修复IE中报错 + +## 0.0.32 (2023-01-04) +- **CI**: 生成态输出文件改为horiozn.producion.min.js + ## 0.0.26 (2022-11-09) - **CI**: 包信息同步CMC diff --git a/libs/horizon/global.d.ts b/libs/horizon/global.d.ts index 34c32508..9da3e079 100644 --- a/libs/horizon/global.d.ts +++ b/libs/horizon/global.d.ts @@ -19,3 +19,4 @@ declare var isDev: boolean; declare var isTest: boolean; declare const __VERSION__: string; +declare var setImmediate: Function; diff --git a/libs/horizon/index.ts b/libs/horizon/index.ts index 59045fbb..5fffa7c9 100644 --- a/libs/horizon/index.ts +++ b/libs/horizon/index.ts @@ -42,9 +42,6 @@ import { useState, useDebugValue, } from './src/renderer/hooks/HookExternal'; -import { asyncUpdates } from './src/renderer/TreeBuilder'; -import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue'; -import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler'; import { isContextProvider, isContextConsumer, @@ -59,13 +56,7 @@ import { import { createStore, useStore, clearStore } from './src/horizonx/store/StoreHandler'; import * as reduxAdapter from './src/horizonx/adapters/redux'; import { watch } from './src/horizonx/proxy/watch'; - -// act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 -const act = fun => { - asyncUpdates(fun); - callRenderQueueImmediate(); - runAsyncEffects(); -}; +import { act } from './src/external/TestUtil'; import { render, @@ -75,6 +66,8 @@ import { unmountComponentAtNode, } from './src/dom/DOMExternal'; +import { syncUpdates as flushSync } from './src/renderer/TreeBuilder'; + const Horizon = { Children, createRef, @@ -107,6 +100,7 @@ const Horizon = { findDOMNode, unmountComponentAtNode, act, + flushSync, createStore, useStore, clearStore, @@ -156,6 +150,7 @@ export { findDOMNode, unmountComponentAtNode, act, + flushSync, // 状态管理器HorizonX接口 createStore, useStore, diff --git a/libs/horizon/jsx-runtime.ts b/libs/horizon/jsx-runtime.ts new file mode 100644 index 00000000..f130dc06 --- /dev/null +++ b/libs/horizon/jsx-runtime.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss 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 { + TYPE_FRAGMENT as Fragment, +} from './src/external/JSXElementType'; +import { jsx, jsx as jsxs } from './src/external/JSXElement'; + +export { + jsx, + jsxs, + Fragment +}; diff --git a/libs/horizon/npm/index.js b/libs/horizon/npm/index.js index c2cf8335..5e212be2 100644 --- a/libs/horizon/npm/index.js +++ b/libs/horizon/npm/index.js @@ -16,7 +16,7 @@ 'use strict'; if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/horizon.production.js'); + module.exports = require('./cjs/horizon.production.min.js'); } else { module.exports = require('./cjs/horizon.development.js'); } diff --git a/libs/horizon/package.json b/libs/horizon/package.json index 0658232f..c9a179bc 100644 --- a/libs/horizon/package.json +++ b/libs/horizon/package.json @@ -4,7 +4,7 @@ "keywords": [ "horizon" ], - "version": "0.0.26", + "version": "0.0.38", "homepage": "", "bugs": "", "main": "index.js", diff --git a/libs/horizon/src/dom/DOMOperator.ts b/libs/horizon/src/dom/DOMOperator.ts index ed3047b8..a7abe22d 100644 --- a/libs/horizon/src/dom/DOMOperator.ts +++ b/libs/horizon/src/dom/DOMOperator.ts @@ -16,7 +16,7 @@ import { saveVNode, updateVNodeProps } from './DOMInternalKeys'; import { createDom } from './utils/DomCreator'; import { getSelectionInfo, resetSelectionRange, SelectionData } from './SelectionRangeHandler'; -import { shouldAutoFocus } from './utils/Common'; +import { isDocument, shouldAutoFocus } from './utils/Common'; import { NSS } from './utils/DomCreator'; import { adjustStyleValue } from './DOMPropertiesHandler/StyleHandler'; import type { VNode } from '../renderer/Types'; @@ -26,6 +26,7 @@ import { isNativeElement, validateProps } from './validators/ValidateProps'; import { watchValueChange } from './valueHandler/ValueChangeHandler'; import { DomComponent, DomText } from '../renderer/vnode/VNodeTags'; import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp'; +import {getCurrentRoot} from '../renderer/RootStack'; export type Props = Record & { autoFocus?: boolean; @@ -70,7 +71,12 @@ export function resetAfterSubmit(): void { // 创建 DOM 对象 export function newDom(tagName: string, props: Props, parentNamespace: string, vNode: VNode): Element { - const dom: Element = createDom(tagName, parentNamespace); + // document取值于treeRoot对应的DOM的ownerDocument。 + // 解决:在iframe中使用top的horizon时,horizon在创建DOM时用到的document并不是iframe的document,而是top中的document的问题。 + const rootDom = getCurrentRoot().realNode; + const doc = isDocument(rootDom) ? rootDom : rootDom.ownerDocument; + + const dom: Element = createDom(tagName, parentNamespace, doc); // 将 vNode 节点挂到 DOM 对象上 saveVNode(vNode, dom); // 将属性挂到 DOM 对象上 diff --git a/libs/horizon/src/dom/utils/DomCreator.ts b/libs/horizon/src/dom/utils/DomCreator.ts index 3eb2d1ca..26fe2840 100644 --- a/libs/horizon/src/dom/utils/DomCreator.ts +++ b/libs/horizon/src/dom/utils/DomCreator.ts @@ -20,15 +20,15 @@ export const NSS = { }; // 创建DOM元素 -export function createDom(tagName: string, parentNamespace: string): Element { +export function createDom(tagName: string, parentNamespace: string, doc: Document): Element { let dom: Element; const selfNamespace = NSS[tagName] || NSS.html; const ns = parentNamespace !== NSS.html ? parentNamespace : selfNamespace; if (ns !== NSS.html) { - dom = document.createElementNS(ns, tagName); + dom = doc.createElementNS(ns, tagName); } else { - dom = document.createElement(tagName); + dom = doc.createElement(tagName); } return dom; } diff --git a/libs/horizon/src/external/JSXElement.ts b/libs/horizon/src/external/JSXElement.ts index ebb43d18..3de212b3 100644 --- a/libs/horizon/src/external/JSXElement.ts +++ b/libs/horizon/src/external/JSXElement.ts @@ -25,10 +25,10 @@ import { Source } from '../renderer/Types'; * props 其他常规属性 */ export function JSXElement(type, key, ref, vNode, props, source: Source | null) { - return { + const ele = { // 元素标识符 vtype: TYPE_COMMON_ELEMENT, - src: isDev ? source : null, + src: null, // 属于元素的内置属性 type: type, @@ -37,8 +37,27 @@ export function JSXElement(type, key, ref, vNode, props, source: Source | null) props: props, // 所属的class组件 - belongClassVNode: vNode, + belongClassVNode: null, }; + + // 在 cloneDeep JSXElement 的时候会出现死循环,需要设置belongClassVNode的enumerable为false + Object.defineProperty(ele, 'belongClassVNode', { + configurable: false, + enumerable: false, + value: vNode, + }); + + if (isDev) { + // 为了test判断两个 JSXElement 对象是否相等时忽略src属性,需要设置src的enumerable为false + Object.defineProperty(ele, 'src', { + configurable: false, + enumerable: false, + writable: false, + value: source, + }); + } + + return ele; } function isValidKey(key) { @@ -107,3 +126,12 @@ export function cloneElement(element, setting, ...children) { export function isValidElement(element) { return !!(element && element.vtype === TYPE_COMMON_ELEMENT); } + +// 兼容高版本的babel编译方式 +export function jsx(type, setting, key) { + if (setting.key === undefined && key !== undefined) { + setting.key = key; + } + + return buildElement(false, type, setting, []); +} diff --git a/libs/horizon/src/external/TestUtil.ts b/libs/horizon/src/external/TestUtil.ts new file mode 100644 index 00000000..26d87660 --- /dev/null +++ b/libs/horizon/src/external/TestUtil.ts @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss 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 {asyncUpdates} from '../renderer/TreeBuilder'; +import {callRenderQueueImmediate} from '../renderer/taskExecutor/RenderQueue'; +import {runAsyncEffects} from '../renderer/submit/HookEffectHandler'; +import {isPromise} from '../renderer/ErrorHandler'; + +interface Thenable { + then(resolve: (val?: any) => void, reject: (err: any) => void): void; +} + +// act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。 +function act(fun: () => void | Thenable): Thenable { + const funRet = asyncUpdates(fun); + + callRenderQueue(); + + // 如果fun返回的是Promise + if (isPromise(funRet)) { + // testing-library会返回Promise + return { + then(resolve, reject) { + funRet.then( + () => { + if (typeof setImmediate === 'function') { + // 通过setImmediate回调,用于等待业务的setTimeout完成 + setImmediate(() => { + callRenderQueue(); + resolve(); + }); + } else { + callRenderQueue(); + resolve(); + } + }, + err => { + reject(err); + }, + ); + }, + }; + } else { + return { + then(resolve) { + resolve(); + }, + }; + } +} + +function callRenderQueue() { + callRenderQueueImmediate(); + runAsyncEffects(); + // effects可能产生刷新任务,这里再执行一次 + callRenderQueueImmediate(); +} + +export { + act +}; diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index a838d45f..03ad253b 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -53,6 +53,43 @@ export type ReduxMiddleware = ( type Reducer = (state: any, action: ReduxAction) => any; +function mergeData(state, data) { + console.log('merging data', { state, data }); + if (!data) { + console.log('!data'); + state.stateWrapper = data; + return; + } + + if (Array.isArray(data) && Array.isArray(state?.stateWrapper)) { + console.log('data is array'); + state.stateWrapper.length = data.length; + data.forEach((item, idx) => { + if (item != state.stateWrapper[idx]) { + state.stateWrapper[idx] = item; + } + }); + return; + } + + if (typeof data === 'object' && typeof state?.stateWrapper === 'object') { + console.log('data is object'); + Object.keys(state.stateWrapper).forEach(key => { + if (!data.hasOwnProperty(key)) delete state.stateWrapper[key]; + }); + + Object.entries(data).forEach(([key, value]) => { + if (state.stateWrapper[key] !== value) { + state.stateWrapper[key] = value; + } + }); + return; + } + + console.log('data is primitive or type mismatch'); + state.stateWrapper = data; +} + export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): ReduxStoreHandler { const store = createStoreX({ id: 'defaultStore', @@ -69,6 +106,7 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): if (result === undefined) { return; } // NOTE: reducer should never return undefined, in this case, do not change state + // mergeData(state,result); state.stateWrapper = result; }, }, @@ -77,6 +115,10 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): }, })(); + // store.$subscribe(()=>{ + // console.log('changed'); + // }); + const result = { reducer, getState: function () { diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 73ab8b45..25428623 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -175,7 +175,11 @@ window.addEventListener('message', messageEvent => { } // executes store action +<<<<<<< HEAD if (messageEvent.data.payload.type === 'horizonx executue action') { +======= + if (messageEvent.data?.payload?.type === 'horizonx executue action') { +>>>>>>> master const data = messageEvent.data.payload.data; const store = getStore(data.storeId); if (!store?.[data.action]) return; diff --git a/libs/horizon/src/renderer/ErrorHandler.ts b/libs/horizon/src/renderer/ErrorHandler.ts index af115325..7ba23cec 100644 --- a/libs/horizon/src/renderer/ErrorHandler.ts +++ b/libs/horizon/src/renderer/ErrorHandler.ts @@ -72,7 +72,7 @@ function createClassErrorUpdate(vNode: VNode, error: any): Update { } return update; } -function isPromise(error: any): error is PromiseType { +export function isPromise(error: any): error is PromiseType { return error !== null && typeof error === 'object' && typeof error.then === 'function'; } // 处理capture和bubble阶段抛出的错误 diff --git a/libs/horizon/src/renderer/RootStack.ts b/libs/horizon/src/renderer/RootStack.ts index 110cad72..6a793a0f 100644 --- a/libs/horizon/src/renderer/RootStack.ts +++ b/libs/horizon/src/renderer/RootStack.ts @@ -14,6 +14,7 @@ */ import { VNode } from './vnode/VNode'; + const currentRootStack: VNode[] = []; export function getCurrentRoot() { return currentRootStack[currentRootStack.length - 1]; diff --git a/libs/horizon/src/renderer/Types.ts b/libs/horizon/src/renderer/Types.ts index 1503fee9..91b6bfbd 100644 --- a/libs/horizon/src/renderer/Types.ts +++ b/libs/horizon/src/renderer/Types.ts @@ -77,3 +77,5 @@ export type Source = { fileName: string; lineNumber: number; }; + +export type Callback = () => void; diff --git a/libs/horizon/src/renderer/UpdateHandler.ts b/libs/horizon/src/renderer/UpdateHandler.ts index c906bd1c..5fd17bb1 100644 --- a/libs/horizon/src/renderer/UpdateHandler.ts +++ b/libs/horizon/src/renderer/UpdateHandler.ts @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -import type { VNode } from './Types'; +import type { VNode, Callback } from './Types'; import { FlagUtils, ShouldCapture } from './vnode/VNodeFlags'; export type Update = { @@ -22,8 +22,6 @@ export type Update = { callback: Callback | null; }; -export type Callback = () => any; - export type Updates = Array | null; export enum UpdateState { diff --git a/libs/horizon/src/renderer/components/BaseClassComponent.ts b/libs/horizon/src/renderer/components/BaseClassComponent.ts index fff07c8d..de4ff351 100644 --- a/libs/horizon/src/renderer/components/BaseClassComponent.ts +++ b/libs/horizon/src/renderer/components/BaseClassComponent.ts @@ -13,6 +13,8 @@ * See the Mulan PSL v2 for more details. */ +import {Callback} from '../Types'; + /** * Component的api setState和forceUpdate在实例生成阶段实现 */ @@ -29,7 +31,7 @@ class Component { this.context = context; } - setState(state: S) { + setState(state: S, callback?: Callback) { if (isDev) { console.error('Cant not call `this.setState` in the constructor of class component, it will do nothing'); } diff --git a/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts b/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts index 28e17a84..6da28613 100644 --- a/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts +++ b/libs/horizon/src/renderer/taskExecutor/BrowserAsync.ts @@ -19,7 +19,9 @@ let isMessageLoopRunning = false; let browserCallback = null; -const { port1, port2 } = new MessageChannel(); +let port1 = null; +let port2 = null; +let isTestRuntime = false; export function isOverTime() { return false; @@ -41,21 +43,38 @@ const callRenderTasks = () => { browserCallback = null; } else { // 还有task,继续调用 - port2.postMessage(null); + asyncCall(); } } catch (error) { - port2.postMessage(null); + asyncCall(); throw error; } }; -port1.onmessage = callRenderTasks; +if (typeof MessageChannel === 'function') { + const mc = new MessageChannel(); + port1 = mc.port1; + port1.onmessage = callRenderTasks; + port2 = mc.port2; +} else { + // 测试环境没有 MessageChannel + isTestRuntime = true; +} + +function asyncCall() { + if (isTestRuntime) { + setTimeout(callRenderTasks, 0); + } else { + port2.postMessage(null); + } +} export function requestBrowserCallback(callback) { browserCallback = callback; if (!isMessageLoopRunning) { isMessageLoopRunning = true; - port2.postMessage(null); + asyncCall(); } } + diff --git a/package.json b/package.json index f2d728f2..b5e07ca2 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,11 @@ "@babel/plugin-transform-object-super": "7.16.7", "@babel/plugin-transform-parameters": "7.16.7", "@babel/plugin-transform-react-jsx": "7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", "@babel/plugin-transform-runtime": "7.16.7", "@babel/plugin-transform-shorthand-properties": "7.16.7", "@babel/plugin-transform-spread": "7.16.7", "@babel/plugin-transform-template-literals": "7.16.7", - "@babel/plugin-transform-react-jsx-source": "^7.16.7", "@babel/preset-env": "7.16.7", "@babel/preset-typescript": "7.16.7", "@rollup/plugin-babel": "^5.3.1", @@ -67,5 +67,8 @@ "engines": { "node": ">=10.x", "npm": ">=7.x" + }, + "dependencies": { + "ejs": "^3.1.8" } } diff --git a/scripts/rollup/rollup.config.js b/scripts/rollup/rollup.config.js index efe2aabd..4e2e7cfd 100644 --- a/scripts/rollup/rollup.config.js +++ b/scripts/rollup/rollup.config.js @@ -38,44 +38,58 @@ if (!fs.existsSync(outDir)) { const outputResolve = (...p) => path.resolve(outDir, ...p); +const isDev = (mode) => { + return mode === 'development'; +} + +const getBasicPlugins = (mode) => { + return [ + nodeResolve({ + extensions, + modulesOnly: true, + }), + babel({ + exclude: 'node_modules/**', + configFile: path.join(__dirname, '../../babel.config.js'), + babelHelpers: 'runtime', + extensions, + }), + replace({ + values: { + 'process.env.NODE_ENV': `"${mode}"`, + isDev: isDev(mode).toString(), + isTest: false, + __VERSION__: `"${horizonVersion}"`, + }, + preventAssignment: true, + }), + ]; +} + + +function getOutputName(mode) { + return mode === 'production' ? `horizon.${mode}.min.js` : `horizon.${mode}.js`; +} + function genConfig(mode) { - const isDev = mode === 'development'; - const sourcemap = isDev ? 'inline' : false; + const sourcemap = isDev(mode) ? 'inline' : false; return { input: path.resolve(libDir, 'index.ts'), output: [ { - file: outputResolve('cjs', `horizon.${mode}.js`), + file: outputResolve('cjs', getOutputName(mode)), sourcemap, format: 'cjs', }, { - file: outputResolve('umd', `horizon.${mode}.js`), + file: outputResolve('umd', getOutputName(mode)), sourcemap, name: 'Horizon', format: 'umd', }, ], plugins: [ - nodeResolve({ - extensions, - modulesOnly: true, - }), - babel({ - exclude: 'node_modules/**', - configFile: path.join(__dirname, '../../babel.config.js'), - babelHelpers: 'runtime', - extensions, - }), - replace({ - values: { - 'process.env.NODE_ENV': `"${mode}"`, - isDev: isDev.toString(), - isTest: false, - __VERSION__: `"${horizonVersion}"`, - }, - preventAssignment: true, - }), + ...getBasicPlugins(mode), execute('npm run build-types'), mode === 'production' && terser(), copy([ @@ -92,4 +106,18 @@ function genConfig(mode) { }; } -export default [genConfig('development'), genConfig('production')]; +function genJSXRuntimeConfig(mode) { + return { + input: path.resolve(libDir, 'jsx-runtime.ts'), + output: [ + { + file: outputResolve('jsx-runtime.js'), + format: 'cjs', + } + ], + plugins: [ + ...getBasicPlugins(mode) + ] + }; +} +export default [genConfig('development'), genConfig('production'), genJSXRuntimeConfig('')];