diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a4a6f5422f..ae5c22581fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: name: Setup runs-on: ubuntu-latest outputs: - fullbuild: ${{ steps.fullbuild.outputs.fullbuild }} + fullbuild: ${{ steps.filter.outputs.fullbuild == 'true' || github.event.inputs.disttag != '' }} steps: - name: Branch @@ -36,14 +36,19 @@ jobs: with: filters: | fullbuild: - - 'src/**/*.(ts|tsx|js|mjs|cjs|jsx|json|toml|rs)' + - 'src/**/*.ts' + - 'src/**/*.tsx' + - 'src/**/*.js' + - 'src/**/*.mjs' + - 'src/**/*.cjs' + - 'src/**/*.jsx' + - 'src/**/*.json' + - 'src/**/*.toml' + - 'src/**/*.rs' - 'yarn.lock' - 'tsconfig.json' - - name: Set fullbuild output - id: fullbuild - run: echo ::set-output name=fullbuild::"${{ steps.filter.outputs.fullbuild == 'true' && github.event.inputs.disttag != '' }}" - name: Print fullbuild output - run: echo ${{ steps.fullbuild.outputs.fullbuild }} + run: echo ${{ steps.filter.outputs.fullbuild == 'true' || github.event.inputs.disttag != '' }} ############ BUILD PACKAGE ############ build-package: @@ -464,25 +469,36 @@ jobs: test-unit: name: Unit Tests runs-on: ubuntu-latest + needs: + - changes + steps: - name: Setup Node + if: ${{ needs.changes.outputs.fullbuild == 'true' }} uses: actions/setup-node@v1 with: node-version: 16.x registry-url: https://registry.npmjs.org/ - name: Checkout + if: ${{ needs.changes.outputs.fullbuild == 'true' }} uses: actions/checkout@v2 - name: Cache NPM Dependencies + if: ${{ needs.changes.outputs.fullbuild == 'true' }} uses: actions/cache@v2 with: path: node_modules key: npm-cache-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - name: Install NPM Dependencies + if: ${{ needs.changes.outputs.fullbuild == 'true' }} run: yarn install --frozen-lockfile --registry https://registry.npmjs.org --network-timeout 300000 + - name: Jest Unit Tests + if: ${{ needs.changes.outputs.fullbuild == 'true' }} + run: yarn test.unit + ########### VALIDATE RUST ############ validate-rust: name: Validate Rust diff --git a/src/core/api.md b/src/core/api.md index ee73ff4aca8..d0743b33409 100644 --- a/src/core/api.md +++ b/src/core/api.md @@ -15,13 +15,13 @@ export function Async(props: AsyncProps): JSXNode; // @public (undocumented) export function bubble(eventType: string, payload?: PAYLOAD): void; -// Warning: (ae-forgotten-export) The symbol "QwikEvents" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ComponentBaseProps" needs to be exported by the entry point index.d.ts // // @public -export function component$(onMount: OnMountFn, options?: ComponentOptions): (props: PROPS & QwikEvents) => JSXNode; +export function component$(onMount: OnMountFn, options?: ComponentOptions): (props: PROPS & ComponentBaseProps) => JSXNode; // @public -export function component(onMount: QRL>, options?: ComponentOptions): (props: PROPS & QwikEvents) => JSXNode; +export function component(onMount: QRL>, options?: ComponentOptions): (props: PROPS & ComponentBaseProps) => JSXNode; // @public (undocumented) export type ComponentChild = JSXNode | object | string | number | bigint | boolean | null | undefined; @@ -39,8 +39,9 @@ export interface ComponentOptions { export interface CorePlatform { chunkForSymbol: (symbolName: string) => string | undefined; importSymbol: (element: Element, url: string | URL, symbol: string) => Promise; - queueRender: (renderMarked: (doc: Document) => Promise) => Promise; - queueStoreFlush: (flushStore: (doc: Document) => Promise) => Promise; + // (undocumented) + nextTick: (fn: () => any) => Promise; + raf: (fn: () => any) => Promise; } // @public @@ -50,12 +51,14 @@ export function createStore(initialState: STATE): STATE; export function dehydrate(document: Document): void; // @public (undocumented) -export const Fragment: any; +export const Fragment: FunctionComponent<{ + children?: any; +}>; // @public (undocumented) export interface FunctionComponent

{ // (undocumented) - (props: P): JSXNode | null; + (props: P, key?: string): JSXNode | null; } // @public (undocumented) @@ -106,7 +109,7 @@ export const Host: FunctionComponent>; export function implicit$FirstArg(fn: (first: QRL, ...rest: REST) => RET): (first: FIRST, ...rest: REST) => RET; // @public (undocumented) -function jsx, PROPS>(type: T, props: PROPS, key?: string): JSXNode; +function jsx, PROPS>(type: T, props: PROPS, key?: string | number): JSXNode; export { jsx } export { jsx as jsxDEV } export { jsx as jsxs } @@ -117,17 +120,23 @@ export type JSXFactory = (props: PROPS, state?: any) // @public (undocumented) export interface JSXNode { // (undocumented) - children: ComponentChild[]; + children: JSXNode[]; + // (undocumented) + elm?: Node; // (undocumented) - key: string | number | any; + key: string | null; // (undocumented) - props: any; + props: Record | null; + // (undocumented) + text?: string; // (undocumented) type: T; } +// Warning: (ae-forgotten-export) The symbol "RenderContext" needs to be exported by the entry point index.d.ts +// // @public -export function notifyRender(hostElement: Element): Promise; +export function notifyRender(hostElement: Element): Promise; // @public export interface Observer { @@ -257,7 +266,7 @@ export namespace QwikJSX { } // @public -export function render(parent: Element | Document, jsxNode: JSXNode | FunctionComponent): Promise; +export function render(parent: Element | Document, jsxNode: JSXNode | FunctionComponent): ValueOrPromise; // @public (undocumented) export type RenderableProps = P & Readonly<{ @@ -273,6 +282,9 @@ export const Slot: FunctionComponent<{ children?: any; }>; +// @public (undocumented) +export function useDocument(): Document; + // @public export function useEvent(expectEventType?: string): EVENT; diff --git a/src/core/component/component-ctx.ts b/src/core/component/component-ctx.ts index 91606ed8d05..5a3bbd220f6 100644 --- a/src/core/component/component-ctx.ts +++ b/src/core/component/component-ctx.ts @@ -1,11 +1,13 @@ import { assertDefined } from '../assert/assert'; -import { cursorForComponent, cursorReconcileEnd } from '../render/cursor'; -import { ComponentRenderQueue, visitJsxNode } from '../render/render'; +import type { RenderContext } from '../render/cursor'; +import { visitJsxNode } from '../render/render'; import { ComponentScopedStyles, OnRenderProp } from '../util/markers'; -import { flattenPromiseTree, then } from '../util/promises'; +import { then } from '../util/promises'; import { styleContent, styleHost } from './qrl-styles'; import { newInvokeContext, useInvoke } from '../use/use-core'; import { getContext, getEvent, QContext } from '../props/props'; +import type { JSXNode, ValueOrPromise } from '..'; +import { processNode } from '../render/jsx/jsx-runtime'; // TODO(misko): Can we get rid of this whole file, and instead teach getProps to know how to render // the advantage will be that the render capability would then be exposed to the outside world as well. @@ -19,37 +21,41 @@ export class QComponentCtx { styleClass: string | null = null; styleHostClass: string | null = null; + slots: JSXNode[] = []; + constructor(hostElement: HTMLElement) { this.hostElement = hostElement; this.ctx = getContext(hostElement); } - async render(): Promise { + render(ctx: RenderContext): ValueOrPromise { const hostElement = this.hostElement; - const onRender = getEvent(this.ctx, OnRenderProp) as any as () => void; + const onRender = getEvent(this.ctx, OnRenderProp) as any as () => JSXNode; assertDefined(onRender); - const renderQueue: ComponentRenderQueue = []; - try { - const event = 'qRender'; - const promise = useInvoke(newInvokeContext(hostElement, hostElement, event), onRender); - await then(promise, (jsxNode) => { - if (this.styleId === undefined) { - const scopedStyleId = (this.styleId = hostElement.getAttribute(ComponentScopedStyles)); - if (scopedStyleId) { - this.styleHostClass = styleHost(scopedStyleId); - this.styleClass = styleContent(scopedStyleId); - } + const event = 'qRender'; + this.ctx.dirty = false; + ctx.globalState.hostsStaging.delete(hostElement); + + const promise = useInvoke(newInvokeContext(hostElement, hostElement, event), onRender); + return then(promise, (jsxNode) => { + // Types are wrong here + jsxNode = (jsxNode as any)[0]; + + if (this.styleId === undefined) { + const scopedStyleId = (this.styleId = hostElement.getAttribute(ComponentScopedStyles)); + if (scopedStyleId) { + this.styleHostClass = styleHost(scopedStyleId); + this.styleClass = styleContent(scopedStyleId); } - const cursor = cursorForComponent(this.hostElement); - visitJsxNode(this, renderQueue, cursor, jsxNode, false); - cursorReconcileEnd(cursor); - }); - } catch (e) { - // TODO(misko): Proper error handling - // eslint-disable-next-line no-console - console.log(e); - } - return [this.hostElement, ...(await flattenPromiseTree(renderQueue))]; + } + ctx.hostElements.add(hostElement); + this.slots = []; + const newCtx: RenderContext = { + ...ctx, + component: this, + }; + return visitJsxNode(newCtx, hostElement, processNode(jsxNode), false); + }); } } diff --git a/src/core/component/component.public.ts b/src/core/component/component.public.ts index 7eca3722c10..b68879b3138 100644 --- a/src/core/component/component.public.ts +++ b/src/core/component/component.public.ts @@ -2,15 +2,16 @@ import { toQrlOrError } from '../import/qrl'; import type { QRLInternal } from '../import/qrl-class'; import { $, implicit$FirstArg, QRL, qrlImport } from '../import/qrl.public'; import type { qrlFactory } from '../props/props-on'; -import { h } from '../render/jsx/factory'; import type { JSXNode } from '../render/jsx/types/jsx-node'; import { newInvokeContext, useInvoke, useWaitOn } from '../use/use-core'; import { useHostElement } from '../use/use-host-element.public'; import { ComponentScopedStyles, OnRenderProp } from '../util/markers'; import { styleKey } from './qrl-styles'; -import type { QwikEvents } from '../render/jsx/types/jsx-qwik-attributes'; +import type { ComponentBaseProps } from '../render/jsx/types/jsx-qwik-attributes'; import type { ValueOrPromise } from '../util/types'; import { getContext, getProps } from '../props/props'; +import { jsx, FunctionComponent } from '../index'; +import { getDocument } from '../util/dom'; // // !!DO NOT EDIT THIS COMMENT DIRECTLY!!! @@ -320,18 +321,18 @@ export interface ComponentOptions { export function component( onMount: QRL>, options?: ComponentOptions -): (props: PROPS & QwikEvents) => JSXNode; +): (props: PROPS & ComponentBaseProps) => JSXNode; /** * @public */ export function component( onMount: QRL>, options: ComponentOptions = {} -): (props: PROPS & QwikEvents) => JSXNode { +): FunctionComponent { const tagName = options.tagName ?? 'div'; // Return a QComponent Factory function. - return function QComponent(props: PROPS & QwikEvents): JSXNode { + return function QComponent(props, key): JSXNode { const onRenderFactory: qrlFactory = async (hostElement: Element): Promise => { // Turn function into QRL const onMountQrl = toQrlOrError(onMount); @@ -342,7 +343,8 @@ export function component( return useInvoke(invokeCtx, onMountFn, props) as QRLInternal; }; onRenderFactory.__brand__ = 'QRLFactory'; - return h(tagName, { [OnRenderProp]: onRenderFactory, ...props }) as any; + + return jsx(tagName, { [OnRenderProp]: onRenderFactory, ...props }, key) as any; }; } @@ -409,7 +411,7 @@ export function component( export function component$( onMount: OnMountFn, options?: ComponentOptions -): (props: PROPS & QwikEvents) => JSXNode { +): (props: PROPS & ComponentBaseProps) => JSXNode { return component($(onMount), options); } @@ -441,7 +443,7 @@ function _useStyles(styles: QRL, scoped: boolean) { useWaitOn( qrlImport(hostElement, styleQrl).then((styleText) => { - const document = hostElement.ownerDocument; + const document = getDocument(hostElement); const head = document.querySelector('head'); if (head && !head.querySelector(`style[q\\:style="${styleId}"]`)) { const style = document.createElement('style'); diff --git a/src/core/import/qrl.public.ts b/src/core/import/qrl.public.ts index 6c5f9bc9a52..9dad04e6655 100644 --- a/src/core/import/qrl.public.ts +++ b/src/core/import/qrl.public.ts @@ -1,5 +1,6 @@ import { runtimeQrl, staticQrl, toInternalQRL } from './qrl'; import { getPlatform } from '../platform/platform'; +import { getDocument } from '../util/dom'; // // !!DO NOT EDIT THIS COMMENT DIRECTLY!!! @@ -148,11 +149,14 @@ export interface QRL { export async function qrlImport(element: Element, qrl: QRL): Promise { const qrl_ = toInternalQRL(qrl); if (qrl_.symbolRef) return qrl_.symbolRef; - const doc = element.ownerDocument!; if (qrl_.symbolFn) { return (qrl_.symbolRef = qrl_.symbolFn().then((module) => module[qrl_.symbol])); } else { - return (qrl_.symbolRef = await getPlatform(doc).importSymbol(element, qrl_.chunk, qrl_.symbol)); + return (qrl_.symbolRef = await getPlatform(getDocument(element)).importSymbol( + element, + qrl_.chunk, + qrl_.symbol + )); } } diff --git a/src/core/import/qrl.ts b/src/core/import/qrl.ts index 1c4cecdb9bc..280c74da1ae 100644 --- a/src/core/import/qrl.ts +++ b/src/core/import/qrl.ts @@ -11,6 +11,7 @@ import type { QRL } from './qrl.public'; import { isQrl, QRLInternal } from './qrl-class'; import { assertEqual } from '../assert/assert'; import type { CorePlatform } from '../index'; +import { getDocument } from '../util/dom'; let runtimeSymbolId = 0; const RUNTIME_QRL = '/runtimeQRL'; @@ -102,7 +103,7 @@ export function stringifyQRL(qrl: QRL, element?: Element, platform?: CorePlatfor } export function qrlToUrl(element: Element, qrl: QRL): URL { - return new URL(stringifyQRL(qrl), element.ownerDocument.baseURI); + return new URL(stringifyQRL(qrl), getDocument(element).baseURI); } /** diff --git a/src/core/index.ts b/src/core/index.ts index e5d5262dd99..e034d4c7722 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -84,6 +84,7 @@ export { render } from './render/render.public'; // use API ////////////////////////////////////////////////////////////////////////////////////////// export { useHostElement } from './use/use-host-element.public'; +export { useDocument } from './use/use-document.public'; export { useEvent } from './use/use-event.public'; export { useLexicalScope } from './use/use-lexical-scope.public'; export { createStore } from './use/use-store.public'; diff --git a/src/core/object/store.ts b/src/core/object/store.ts index b4982940b46..aaccb6470cf 100644 --- a/src/core/object/store.ts +++ b/src/core/object/store.ts @@ -30,10 +30,10 @@ export function QStore_hydrate(doc: Document) { const id = el.getAttribute(ELEMENT_ID)!; elements.set(ELEMENT_ID_PREFIX + id, el); }); - reviveQObjects(meta.objs, meta.subs, elements); for (const obj of meta.objs) { reviveNestedQObjects(obj, meta.objs); } + reviveQObjects(meta.objs, meta.subs, elements); doc.querySelectorAll(QObjSelector).forEach((el) => { const qobj = el.getAttribute(QObjAttr); @@ -124,8 +124,32 @@ export function QStore_dehydrate(doc: Document) { count++; } + const convert = (value: any) => { + if (value && typeof value === 'object') { + value = value[QOjectTargetSymbol] ?? value; + } + const idx = objToId.get(value); + if (idx !== undefined) { + return intToStr(idx); + } + return elementToIndex.get(value) ?? value; + }; + + const convertedObjs = objs.map((obj) => { + if (Array.isArray(obj)) { + return obj.map(convert); + } else if (typeof obj === 'object') { + const output: Record = {}; + Object.entries(obj).forEach(([key, value]) => { + output[key] = convert(value); + }); + return output; + } + return obj; + }); + const data = { - objs, + objs: convertedObjs, subs, }; @@ -161,22 +185,7 @@ export function QStore_dehydrate(doc: Document) { // Serialize const script = doc.createElement('script'); script.setAttribute('type', 'qwik/json'); - script.textContent = JSON.stringify( - data, - function (this: any, key: string, value: any) { - if (key.startsWith('__')) return undefined; - if (value && typeof value === 'object') { - value = value[QOjectTargetSymbol] ?? value; - } - if (this === objs) return value; - const idx = objToId.get(value); - if (idx !== undefined) { - return intToStr(idx); - } - return elementToIndex.get(value) ?? value; - }, - qDev ? ' ' : undefined - ); + script.textContent = JSON.stringify(data, undefined, qDev ? ' ' : undefined); doc.body.appendChild(script); } diff --git a/src/core/platform/platform.ts b/src/core/platform/platform.ts index 8063878702d..c3bce5245f7 100644 --- a/src/core/platform/platform.ts +++ b/src/core/platform/platform.ts @@ -1,14 +1,12 @@ +import { getDocument } from '../util/dom'; import { isDocument } from '../util/element'; import type { CorePlatform } from './types'; export const createPlatform = (doc: Document): CorePlatform => { - let queuePromise: Promise | null; - let storePromise: Promise | null; - const moduleCache = new Map(); return { importSymbol(element, url, symbolName) { - const urlDoc = toUrl(element.ownerDocument, element, url).toString(); + const urlDoc = toUrl(doc, element, url).toString(); const urlCopy = new URL(urlDoc); urlCopy.hash = ''; @@ -23,27 +21,19 @@ export const createPlatform = (doc: Document): CorePlatform => { return mod[symbolName]; }); }, - queueRender: (renderMarked) => { - if (!queuePromise) { - queuePromise = new Promise((resolve, reject) => - doc.defaultView!.requestAnimationFrame(() => { - queuePromise = null; - renderMarked(doc).then(resolve, reject); - }) - ); - } - return queuePromise; + raf: (fn) => { + return new Promise((resolve) => { + requestAnimationFrame(() => { + resolve(fn()); + }); + }); }, - queueStoreFlush: (flushStore) => { - if (!storePromise) { - storePromise = new Promise((resolve, reject) => - doc.defaultView!.requestAnimationFrame(() => { - storePromise = null; - flushStore(doc).then(resolve, reject); - }) - ); - } - return storePromise; + nextTick: (fn) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(fn()); + }); + }); }, chunkForSymbol() { return undefined; @@ -95,7 +85,7 @@ export const setPlatform = (doc: Document, plt: CorePlatform) => * @public */ export const getPlatform = (docOrNode: Document | Node) => { - const doc = (isDocument(docOrNode) ? docOrNode : docOrNode.ownerDocument!) as PlatformDocument; + const doc = (isDocument(docOrNode) ? docOrNode : getDocument(docOrNode)!) as PlatformDocument; return doc[DocumentPlatform] || (doc[DocumentPlatform] = createPlatform(doc)); }; diff --git a/src/core/platform/types.ts b/src/core/platform/types.ts index bf9e3dec14a..6b6486fe4e7 100644 --- a/src/core/platform/types.ts +++ b/src/core/platform/types.ts @@ -10,12 +10,8 @@ export interface CorePlatform { * Platform specific queue, such as process.nextTick() for Node * and requestAnimationFrame() for the browser. */ - queueRender: (renderMarked: (doc: Document) => Promise) => Promise; - /** - * Platform specific queue, such as process.nextTick() for Node - * and requestAnimationFrame() for the browser. - */ - queueStoreFlush: (flushStore: (doc: Document) => Promise) => Promise; + raf: (fn: () => any) => Promise; + nextTick: (fn: () => any) => Promise; /** * Takes a qrl and serializes into a string */ diff --git a/src/core/props/props-on.ts b/src/core/props/props-on.ts index 775bbb2a108..4652ebbbf86 100644 --- a/src/core/props/props-on.ts +++ b/src/core/props/props-on.ts @@ -12,6 +12,7 @@ import { debugStringify } from '../util/stringify'; import type { ValueOrPromise } from '../util/types'; import { invokeWatchFn } from '../watch/watch'; import { getEvents, QContext } from './props'; +import { getDocument } from '../util/dom'; const ON_PROP_REGEX = /on(Document|Window)?:/; const ON$_PROP_REGEX = /on(Document|Window)?\$:/; @@ -168,7 +169,7 @@ function getExistingQRLs(ctx: QContext, prop: string): ValueOrPromise[], ctx: QContext): string { - const platform = getPlatform(ctx.element.ownerDocument); + const platform = getPlatform(getDocument(ctx.element)); const element = ctx.element; return existingQRLs .map((qrl) => (isPromise(qrl) ? '' : stringifyQRL(qrl, element, platform))) diff --git a/src/core/props/props.ts b/src/core/props/props.ts index b9c4a92d718..86c01092057 100644 --- a/src/core/props/props.ts +++ b/src/core/props/props.ts @@ -1,6 +1,7 @@ import { QError, qError } from '../error/error'; import { readWriteProxy } from '../object/q-object'; import { QStore_hydrate } from '../object/store'; +import { getDocument } from '../util/dom'; import { newQObjectMap, QObjectMap } from './props-obj-map'; import { qPropWriteQRL, qPropReadQRL } from './props-on'; @@ -12,7 +13,7 @@ const Q_IS_HYDRATED = '__isHydrated__'; export const Q_CTX = '__ctx__'; export function hydrateIfNeeded(element: Element): void { - const doc = element.ownerDocument!; + const doc = getDocument(element); const isHydrated = (doc as any)[Q_IS_HYDRATED]; if (!isHydrated) { (doc as any)[Q_IS_HYDRATED] = true; @@ -28,9 +29,9 @@ export interface QContext { cache: Map; refMap: QObjectMap; element: Element; - id: string | undefined; + dirty: boolean; props: Record | undefined; - events?: QContextEvents; + events: QContextEvents | undefined; } export function getContext(element: Element): QContext { @@ -43,8 +44,9 @@ export function getContext(element: Element): QContext { element, cache, refMap: newQObjectMap(element), - id: undefined, + dirty: false, props: undefined, + events: undefined, }; } return ctx; diff --git a/src/core/render/cursor.ts b/src/core/render/cursor.ts index 30f4b938dd5..44350ab1321 100644 --- a/src/core/render/cursor.ts +++ b/src/core/render/cursor.ts @@ -1,297 +1,534 @@ -import { - assertDefined, - assertEqual, - assertGreater, - assertGreaterOrEqual, - assertNotEqual, -} from '../assert/assert'; -import type { QComponentCtx } from '../component/component-ctx'; -import { getQComponent } from '../component/component-ctx'; -import { keyValueArrayGet } from '../util/array_map'; -import { isComment, isDocument } from '../util/element'; -import { QHostAttr, OnRenderProp, QSlotAttr } from '../util/markers'; -import { - isComponentElement, - isDomElementWithTagName, - isQSLotTemplateElement, - NodeType, -} from '../util/types'; +import { OnRenderProp, QHostAttr, QSlotAttr } from '../util/markers'; import { getContext, getProps, setEvent } from '../props/props'; -import type { ComponentRenderQueue } from './render'; -import { getSlotMap, isSlotMap, NamedSlot, NamedSlotEnum, SlotMap } from './slots'; import { isOn$Prop, isOnProp } from '../props/props-on'; -import { $ } from '../index'; -import { getScheduled } from './notify-render'; - -/** - * Cursor represents a set of sibling elements at a given level in the DOM. - * - * The cursor is used for reconciling what the JSX expects vs what the DOM has. - * NOTE: Descending to a child involves creating a new cursor. - * - * Cursor allows these operations: - * - `cursorForParent`: creates cursor for a given parent. - * - `cursorForComponent`: creates cursor if parent is component, and we are reconciling component - * view. - * - `cursorReconcileElement`: Ensures that the current DOM node matches a given shape. - * - `cursorReconcileText`: Ensures that the current DOM node matches a given text. - * - `cursorReconcileEnd`: Ensures that there are no more dangling elements. - */ -export interface Cursor { - parent: Node | null; - /** - * `Node`: points to the current node which needs to be reconciled. - * `SlotMap`: points to the current set of projections - * `null': points to the next element after the last sibling. (Reconciliation will insert new - * element.) - * `undefined`: The cursor has been closed with `cursorReconcileEnd`. No further operations are - * allowed. - */ - node: Node | SlotMap | null /** | undefined // not included as it is end state */; - end: Node | null; +export const SVG_NS = 'http://www.w3.org/2000/svg'; +import { $, Host, JSXNode, ValueOrPromise } from '../index'; +import { getQComponent, QComponentCtx } from '../component/component-ctx'; +import { promiseAll, then } from '../util/promises'; +import type { RenderingState } from './notify-render'; +import { assertDefined, assertEqual } from '../assert/assert'; +import { NodeType } from '../util/types'; +import { intToStr } from '../object/store'; +import { EMPTY_ARRAY } from '../util/flyweight'; +import { Skip } from './jsx/host.public'; + +type KeyToIndexMap = { [key: string]: number }; + +type PropHandler = ( + ctx: RenderContext, + el: HTMLElement, + key: string, + newValue: any, + oldValue: any +) => boolean; + +interface RenderOperation { + el: Node; + operation: string; + args: any[]; + fn: () => void; } -export const SVG_NS = 'http://www.w3.org/2000/svg'; +export interface RenderContext { + doc: Document; + roots: Element[]; + hostElements: Set; + operations: RenderOperation[]; + component: QComponentCtx | undefined; + globalState: RenderingState; + perf: PerfEvent[]; +} -/** - * Create a cursor which reconciles logical children. - * - * Here logical means children as defined by JSX. (This will be same as DOM except - * in the case of projection.) In case of projection the cursor will correctly - * deal with the logical children of the View (rather then rendered children.) - * - * See: `cursorForComponent` - * - * @param parent Parent `Element` whose children should be reconciled. - */ -export function cursorForParent(parent: Node): Cursor { - let firstChild = parent.firstChild; - if (firstChild && firstChild.nodeType === NodeType.DOCUMENT_TYPE_NODE) { - firstChild = firstChild.nextSibling; - } - return newCursor(parent, firstChild, null); -} - -function newCursor(parent: Node | null, node: Node | SlotMap | null, end: Node | null): Cursor { - return { parent, node, end }; -} - -function getNode(cursor: Cursor) { - const node = cursor.node; - return cursor.end == node ? null : node; -} - -function setNode(cursor: Cursor, node: Node | null) { - cursor.node = cursor.end == node ? null : node; -} - -export function cursorClone(cursor: Cursor): Cursor { - return newCursor(cursor.parent, cursor.node, cursor.end); -} - -/** - * Reconcile view children of a component. - * - * Use this method to create a cursor when reconciling a component's view. - * - * The main point of this method is to skip the `