Skip to content

Commit

Permalink
feat(): async rendering (#252)
Browse files Browse the repository at this point in the history
  • Loading branch information
manucorporat committed Mar 10, 2022
1 parent e0e6ed6 commit 7fadd18
Show file tree
Hide file tree
Showing 42 changed files with 1,391 additions and 1,322 deletions.
28 changes: 22 additions & 6 deletions .github/workflows/ci.yml
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
38 changes: 25 additions & 13 deletions src/core/api.md
Expand Up @@ -15,13 +15,13 @@ export function Async<T>(props: AsyncProps<T>): JSXNode<any>;
// @public (undocumented)
export function bubble<PAYLOAD>(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$<PROPS extends {}>(onMount: OnMountFn<PROPS>, options?: ComponentOptions): (props: PROPS & QwikEvents) => JSXNode<PROPS>;
export function component$<PROPS extends {}>(onMount: OnMountFn<PROPS>, options?: ComponentOptions): (props: PROPS & ComponentBaseProps) => JSXNode<PROPS>;

// @public
export function component<PROPS extends {}>(onMount: QRL<OnMountFn<PROPS>>, options?: ComponentOptions): (props: PROPS & QwikEvents) => JSXNode<PROPS>;
export function component<PROPS extends {}>(onMount: QRL<OnMountFn<PROPS>>, options?: ComponentOptions): (props: PROPS & ComponentBaseProps) => JSXNode<PROPS>;

// @public (undocumented)
export type ComponentChild = JSXNode<any> | object | string | number | bigint | boolean | null | undefined;
Expand All @@ -39,8 +39,9 @@ export interface ComponentOptions {
export interface CorePlatform {
chunkForSymbol: (symbolName: string) => string | undefined;
importSymbol: (element: Element, url: string | URL, symbol: string) => Promise<any>;
queueRender: (renderMarked: (doc: Document) => Promise<any>) => Promise<any>;
queueStoreFlush: (flushStore: (doc: Document) => Promise<any>) => Promise<any>;
// (undocumented)
nextTick: (fn: () => any) => Promise<any>;
raf: (fn: () => any) => Promise<any>;
}

// @public
Expand All @@ -50,12 +51,14 @@ export function createStore<STATE extends {}>(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<P = {}> {
// (undocumented)
(props: P): JSXNode | null;
(props: P, key?: string): JSXNode | null;
}

// @public (undocumented)
Expand Down Expand Up @@ -106,7 +109,7 @@ export const Host: FunctionComponent<Record<string, any>>;
export function implicit$FirstArg<FIRST, REST extends any[], RET>(fn: (first: QRL<FIRST>, ...rest: REST) => RET): (first: FIRST, ...rest: REST) => RET;

// @public (undocumented)
function jsx<T extends string | FunctionComponent<PROPS>, PROPS>(type: T, props: PROPS, key?: string): JSXNode<T>;
function jsx<T extends string | FunctionComponent<PROPS>, PROPS>(type: T, props: PROPS, key?: string | number): JSXNode<T>;
export { jsx }
export { jsx as jsxDEV }
export { jsx as jsxs }
Expand All @@ -117,17 +120,23 @@ export type JSXFactory<T, PROPS extends {} = any> = (props: PROPS, state?: any)
// @public (undocumented)
export interface JSXNode<T = any> {
// (undocumented)
children: ComponentChild[];
children: JSXNode[];
// (undocumented)
elm?: Node;
// (undocumented)
key: string | number | any;
key: string | null;
// (undocumented)
props: any;
props: Record<string, any> | 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<void>;
export function notifyRender(hostElement: Element): Promise<RenderContext>;

// @public
export interface Observer {
Expand Down Expand Up @@ -257,7 +266,7 @@ export namespace QwikJSX {
}

// @public
export function render(parent: Element | Document, jsxNode: JSXNode<unknown> | FunctionComponent<any>): Promise<HTMLElement[]>;
export function render(parent: Element | Document, jsxNode: JSXNode<unknown> | FunctionComponent<any>): ValueOrPromise<RenderContext>;

// @public (undocumented)
export type RenderableProps<P, RefType = any> = P & Readonly<{
Expand All @@ -273,6 +282,9 @@ export const Slot: FunctionComponent<{
children?: any;
}>;

// @public (undocumented)
export function useDocument(): Document;

// @public
export function useEvent<EVENT extends {}>(expectEventType?: string): EVENT;

Expand Down
58 changes: 32 additions & 26 deletions 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.
Expand All @@ -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<HTMLElement[]> {
render(ctx: RenderContext): ValueOrPromise<void> {
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<HTMLElement>(renderQueue))];
}
ctx.hostElements.add(hostElement);
this.slots = [];
const newCtx: RenderContext = {
...ctx,
component: this,
};
return visitJsxNode(newCtx, hostElement, processNode(jsxNode), false);
});
}
}

Expand Down
18 changes: 10 additions & 8 deletions src/core/component/component.public.ts
Expand Up @@ -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';

// <docs markdown="https://hackmd.io/c_nNpiLZSYugTU0c5JATJA#onUnmount">
// !!DO NOT EDIT THIS COMMENT DIRECTLY!!!
Expand Down Expand Up @@ -320,18 +321,18 @@ export interface ComponentOptions {
export function component<PROPS extends {}>(
onMount: QRL<OnMountFn<PROPS>>,
options?: ComponentOptions
): (props: PROPS & QwikEvents) => JSXNode<PROPS>;
): (props: PROPS & ComponentBaseProps) => JSXNode<PROPS>;
/**
* @public
*/
export function component<PROPS extends {}>(
onMount: QRL<OnMountFn<PROPS>>,
options: ComponentOptions = {}
): (props: PROPS & QwikEvents) => JSXNode<PROPS> {
): FunctionComponent<PROPS & ComponentBaseProps> {
const tagName = options.tagName ?? 'div';

// Return a QComponent Factory function.
return function QComponent(props: PROPS & QwikEvents): JSXNode<PROPS> {
return function QComponent(props, key): JSXNode<PROPS> {
const onRenderFactory: qrlFactory = async (hostElement: Element): Promise<QRLInternal> => {
// Turn function into QRL
const onMountQrl = toQrlOrError(onMount);
Expand All @@ -342,7 +343,8 @@ export function component<PROPS extends {}>(
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;
};
}

Expand Down Expand Up @@ -409,7 +411,7 @@ export function component<PROPS extends {}>(
export function component$<PROPS extends {}>(
onMount: OnMountFn<PROPS>,
options?: ComponentOptions
): (props: PROPS & QwikEvents) => JSXNode<PROPS> {
): (props: PROPS & ComponentBaseProps) => JSXNode<PROPS> {
return component<PROPS>($(onMount), options);
}

Expand Down Expand Up @@ -441,7 +443,7 @@ function _useStyles(styles: QRL<string>, 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');
Expand Down
8 changes: 6 additions & 2 deletions 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';

// <docs markdown="https://hackmd.io/m5DzCi5MTa26LuUj5t3HpQ#QRL">
// !!DO NOT EDIT THIS COMMENT DIRECTLY!!!
Expand Down Expand Up @@ -148,11 +149,14 @@ export interface QRL<TYPE = any> {
export async function qrlImport<T>(element: Element, qrl: QRL<T>): Promise<T> {
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
));
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/core/import/qrl.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/core/index.ts
Expand Up @@ -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';
Expand Down

0 comments on commit 7fadd18

Please sign in to comment.