diff --git a/packages/compiler/experimental/utils.ts b/packages/compiler/experimental/utils.ts index dde437c702..74ba6ac3db 100644 --- a/packages/compiler/experimental/utils.ts +++ b/packages/compiler/experimental/utils.ts @@ -43,8 +43,8 @@ export const chainOrLogic = ( }; export const createDirtyChecker = (holes: string[]) => { - const oldProps = t.identifier('_'); - const newProps = t.identifier('$'); + const oldProps = t.identifier('a'); + const newProps = t.identifier('b'); return t.arrowFunctionExpression( [oldProps, newProps], chainOrLogic( diff --git a/packages/compiler/react/transform.ts b/packages/compiler/react/transform.ts index e5aba37b69..3e2aa4376d 100644 --- a/packages/compiler/react/transform.ts +++ b/packages/compiler/react/transform.ts @@ -34,6 +34,7 @@ export const transformComponent = ( SHARED: Shared, ) => { const { + isReact, file, callSite, callSitePath, @@ -114,7 +115,7 @@ export const transformComponent = ( const returnStatement = jsxPath.node as t.ReturnStatement; /** - * Turns top-level JSX fragmentsinto a render scope. This is because + * Turns top-level JSX fragments into a render scope. This is because * the runtime API does not currently handle fragments. We will deal with * nested fragments later. * @@ -172,26 +173,16 @@ export const transformComponent = ( cache: new Set(), // cache to check if id already exists to prevent dupes deferred: [], // callback (() => void) functions that run mutations on the JSX unoptimizable: false, + portalInfo: { + index: -1, + id: componentBodyPath.scope.generateUidIdentifier(''), + }, }; if (!t.isJSXElement(returnStatement.argument)) { throw createDeopt(null, file, callSitePath); } - // This function will automatically populate the `dynamics` for us: - transformJSX( - options, - { - jsx: returnStatement.argument, - jsxPath: jsxPath.get('argument') as NodePath, - componentBody, - componentBodyPath, - dynamics, - isRoot: true, - }, - SHARED, - ); - /** * The compiler splits the original Component into two, in order to conform to the runtime API: * @@ -223,10 +214,39 @@ export const transformComponent = ( : 'master$', ); const puppetComponentId = callSitePath.scope.generateUidIdentifier('puppet$'); - const isCallable = statementsInBody === 1; const block = imports.addNamed('block'); + const layers: t.JSXElement[] = extractLayers( + options, + { + returnStatement, + originalComponent, + jsx: returnStatement.argument, + jsxPath: jsxPath.get('argument') as NodePath, + layers: [], + }, + SHARED, + ); + const isCallable = + statementsInBody === 1 && + layers.length === 0 && + dynamics.portalInfo.index === -1; + + // This function will automatically populate the `dynamics` for us: + transformJSX( + options, + { + jsx: returnStatement.argument, + jsxPath: jsxPath.get('argument') as NodePath, + componentBody, + componentBodyPath, + dynamics, + isRoot: true, + }, + SHARED, + ); + /** * ```js * const puppet = block(({ foo }) => { @@ -235,34 +255,6 @@ export const transformComponent = ( * ``` */ - const params = t.isVariableDeclarator(Component) - ? t.isArrowFunctionExpression(Component.init) - ? Component.init.params - : null - : Component.params; - - // We want to add a __props property for the original call props - // TODO: refactor this probably - if ( - options.server && - params?.length && - (t.isIdentifier(params[0]) || t.isObjectPattern(params[0])) - ) { - // turn params[0] object pattern into an object expression - const props = t.isObjectPattern(params[0]) - ? t.objectExpression( - params[0].properties.map((prop) => { - if (t.isObjectProperty(prop) || t.isObjectMethod(prop)) { - return t.objectProperty(prop.key, prop.key as t.Expression); - } - return t.spreadElement(prop.argument as t.Expression); - }) as any, - ) - : params[0]; - - dynamics.props = props; - } - const holes = dynamics.data.map(({ id }) => id.name); const originalObjectExpression = callSite.arguments[1] as @@ -292,10 +284,6 @@ export const transformComponent = ( )), ), ), - t.objectProperty( - t.identifier('original'), - options.server ? (originalComponent.id as t.Identifier) : t.nullLiteral(), - ), t.objectProperty(t.identifier('shouldUpdate'), createDirtyChecker(holes)), ]; @@ -316,6 +304,55 @@ export const transformComponent = ( ]); t.addComment(puppetBlock, 'leading', TRANSFORM_ANNOTATION); + const data: (typeof dynamics)['data'] = []; + + if (dynamics.portalInfo.index !== -1) { + const useState = imports.addNamed( + 'useState', + isReact ? 'react' : 'preact/hooks', + ); + data.push({ + id: dynamics.portalInfo.id, + value: t.memberExpression( + t.callExpression(useState, [ + t.arrowFunctionExpression( + [], + t.objectExpression([ + t.objectProperty( + t.identifier('$'), + t.newExpression(t.identifier('Array'), [ + t.numericLiteral(dynamics.portalInfo.index + 1), + ]), + ), + ]), + ), + ]), + t.numericLiteral(0), + true, + ), + }); + } + + for (const { id, value } of dynamics.data) { + if (!value) continue; + + data.push({ id, value }); + } + + if (data.length) { + jsxPath.insertBefore( + t.variableDeclaration('const', [ + ...data.map(({ id, value }) => { + return t.variableDeclarator(id, value); + }), + ]), + ); + } + + const puppetJsxAttributes = dynamics.data.map(({ id }) => + t.jsxAttribute(t.jsxIdentifier(id.name), t.jsxExpressionContainer(id)), + ); + /** * We want to change the Component's return from our original JSX * to the puppet call: @@ -339,40 +376,7 @@ export const transformComponent = ( * } * ``` */ - - const data: (typeof dynamics)['data'] = []; - - for (const { id, value } of dynamics.data) { - if (!value) continue; - - data.push({ id, value }); - } - - if (data.length) { - jsxPath.insertBefore( - t.variableDeclaration( - 'const', - data.map(({ id, value }) => { - return t.variableDeclarator(id, value); - }), - ), - ); - } - - const puppetJsxAttributes = dynamics.data.map(({ id }) => - t.jsxAttribute(t.jsxIdentifier(id.name), t.jsxExpressionContainer(id)), - ); - - if (dynamics.props) { - puppetJsxAttributes.push( - t.jsxAttribute( - t.jsxIdentifier('__props'), - t.jsxExpressionContainer(dynamics.props), - ), - ); - } - - const puppetCall = t.jsxElement( + let puppetCall: any = t.jsxElement( t.jsxOpeningElement( t.jsxIdentifier(puppetComponentId.name), puppetJsxAttributes, @@ -382,6 +386,43 @@ export const transformComponent = ( [], ); + if (dynamics.portalInfo.index !== -1) { + puppetCall = t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), [ + puppetCall, + t.jsxSpreadChild( + t.callExpression( + t.memberExpression( + t.memberExpression(dynamics.portalInfo.id, t.identifier('$')), + t.identifier('map'), + ), + [ + t.arrowFunctionExpression( + [t.identifier('p')], + t.memberExpression(t.identifier('p'), t.identifier('portal')), + ), + ], + ), + ), + ]); + } + + if (layers.length) { + let parent: t.JSXElement | t.JSXFragment | undefined; + let current: t.JSXElement | t.JSXFragment | undefined; + for (let i = 0; i < layers.length; ++i) { + const layer = layers[i]!; + if (!current) { + current = layer; + parent = current; + continue; + } + current.children = [layer]; + current = layer; + } + current!.children = [puppetCall]; + puppetCall = parent; + } + componentBody.body[data.length ? statementsInBody : statementsInBody - 1] = t.returnStatement(puppetCall); @@ -407,14 +448,6 @@ export const transformComponent = ( callSitePath.replaceWith(masterComponentId); - if (options.server) { - // attach the original component to the master component - globalPath.insertBefore( - t.isVariableDeclarator(originalComponent) - ? t.variableDeclaration('const', [originalComponent]) - : originalComponent, - ); - } globalPath.insertBefore( t.variableDeclaration('const', [ t.variableDeclarator(puppetComponentId, puppetBlock), @@ -458,6 +491,52 @@ export const transformComponent = ( } }; +export const extractLayers = ( + options: Options, + { + returnStatement, + originalComponent, + jsx, + jsxPath, + layers, + }: { + returnStatement: t.ReturnStatement; + originalComponent: t.FunctionDeclaration | t.VariableDeclarator; + jsx: t.JSXElement; + jsxPath: NodePath; + layers: t.JSXElement[]; + }, + SHARED: Shared, +): t.JSXElement[] => { + const type = jsx.openingElement.name; + if ( + (t.isJSXIdentifier(type) && isComponent(type.name)) || + t.isJSXMemberExpression(type) + ) { + trimJsxChildren(jsx); + const firstChild = jsx.children[0]; + if (jsx.children.length === 1 && t.isJSXElement(firstChild)) { + jsxPath.replaceWith(firstChild); + returnStatement.argument = firstChild; + jsx.children = []; + layers.push(jsx); + + return extractLayers( + options, + { + returnStatement, + originalComponent, + jsx: firstChild, + jsxPath, + layers, + }, + SHARED, + ); + } + } + return layers; +}; + export const transformJSX = ( options: Options, { @@ -527,7 +606,7 @@ export const transformJSX = ( *
{_1$}
* ``` */ - const id = identifier || componentBodyPath.scope.generateUidIdentifier('$'); + const id = identifier || componentBodyPath.scope.generateUidIdentifier(''); if (!dynamics.cache.has(id.name)) { dynamics.data.push({ value: expression, id }); @@ -538,6 +617,33 @@ export const transformJSX = ( return id; }; + let renderReactScope: t.Identifier | null = null; + const createPortal = ( + callback: () => void, + _arguments: ( + | t.Expression + | t.ArgumentPlaceholder + | t.JSXNamespacedName + | t.SpreadElement + )[], + ) => { + renderReactScope ??= imports.addNamed('renderReactScope'); + const index = ++dynamics.portalInfo.index; + + const refCurrent = t.memberExpression( + dynamics.portalInfo.id, + t.identifier('$'), + ); + const nestedRender = t.callExpression(renderReactScope, [ + ..._arguments, + refCurrent, + t.numericLiteral(index), + t.booleanLiteral(Boolean(options.server)), + ]); + const id = createDynamic(null, nestedRender, null, callback); + return id; + }; + const type = jsx.openingElement.name; // if (!t.isJSXElement(jsx) && !t.isJSXFragment(jsx) && isRoot) { @@ -581,14 +687,11 @@ export const transformJSX = ( file, resolvePath(spreadPath), ); - const renderReactScope = imports.addNamed('renderReactScope'); - const nestedRender = t.callExpression(renderReactScope, [ - jsx, - t.booleanLiteral(unstable), - ]); - const id = createDynamic(null, nestedRender, null, () => { + + const id = createPortal(() => { jsxPath.replaceWith(t.jsxExpressionContainer(id!)); - }); + }, [jsx, t.booleanLiteral(unstable)]); + return dynamics; } @@ -606,18 +709,14 @@ export const transformJSX = ( if (t.isIdentifier(expression)) { if (attribute.name.name === 'ref') { - const renderReactScope = imports.addNamed('renderReactScope'); - const nestedRender = t.callExpression(renderReactScope, [ - jsx, - t.booleanLiteral(unstable), - ]); - const id = createDynamic(null, nestedRender, null, () => { + const id = createPortal(() => { jsxPath.replaceWith( isRoot ? t.expressionStatement(id!) : t.jsxExpressionContainer(id!), ); - }); + }, [jsx, t.booleanLiteral(unstable)]); + return dynamics; } createDynamic(expression, null, null, null); @@ -649,18 +748,25 @@ export const transformJSX = ( // options.mute, // ); - const renderReactScope = imports.addNamed('renderReactScope'); - const nestedRender = t.callExpression(renderReactScope, [ + const id = createPortal(() => { + jsxPath.replaceWith( + isRoot ? t.expressionStatement(id!) : t.jsxExpressionContainer(id!), + ); + }, [ jsx, type.name === 'For' ? t.booleanLiteral(false) : t.booleanLiteral(unstable), ]); - const id = createDynamic(null, nestedRender, null, () => { + + return dynamics; + } + if (t.isJSXMemberExpression(type)) { + const id = createPortal(() => { jsxPath.replaceWith( isRoot ? t.expressionStatement(id!) : t.jsxExpressionContainer(id!), ); - }); + }, [jsx, t.booleanLiteral(unstable)]); return dynamics; } @@ -679,14 +785,11 @@ export const transformJSX = ( file, resolvePath(spreadPath), ); - const renderReactScope = imports.addNamed('renderReactScope'); - const nestedRender = t.callExpression(renderReactScope, [ - jsx, - t.booleanLiteral(unstable), - ]); - const id = createDynamic(null, nestedRender, null, () => { + + const id = createPortal(() => { jsxPath.replaceWith(t.jsxExpressionContainer(id!)); - }); + }, [jsx, t.booleanLiteral(unstable)]); + return dynamics; } @@ -804,16 +907,12 @@ export const transformJSX = ( ); if (t.isJSXIdentifier(attribute.name) && attribute.name.name === 'css') { - const renderReactScope = imports.addNamed('renderReactScope'); - const nestedRender = t.callExpression(renderReactScope, [ - jsx, - t.booleanLiteral(unstable), - ]); - const id = createDynamic(null, nestedRender, null, () => { + const id = createPortal(() => { jsxPath.replaceWith( isRoot ? t.expressionStatement(id!) : t.jsxExpressionContainer(id!), ); - }); + }, [jsx, t.booleanLiteral(unstable)]); + return dynamics; } @@ -821,18 +920,14 @@ export const transformJSX = ( if (t.isIdentifier(expression)) { if (attribute.name.name === 'ref') { - const renderReactScope = imports.addNamed('renderReactScope'); - const nestedRender = t.callExpression(renderReactScope, [ - jsx, - t.booleanLiteral(unstable), - ]); - const id = createDynamic(null, nestedRender, null, () => { + const id = createPortal(() => { jsxPath.replaceWith( isRoot ? t.expressionStatement(id!) : t.jsxExpressionContainer(id!), ); - }); + }, [jsx, t.booleanLiteral(unstable)]); + return dynamics; } createDynamic(expression, null, null, null); @@ -866,14 +961,10 @@ export const transformJSX = ( file, resolvePath(spreadPath), ); - const renderReactScope = imports.addNamed('renderReactScope'); - const nestedRender = t.callExpression(renderReactScope, [ - jsx, - t.booleanLiteral(unstable), - ]); - const id = createDynamic(null, nestedRender, null, () => { + const id = createPortal(() => { jsxPath.replaceWith(t.jsxExpressionContainer(id!)); - }); + }, [jsx, t.booleanLiteral(unstable)]); + return dynamics; } @@ -933,32 +1024,38 @@ export const transformJSX = ( continue; } - if ( - t.isJSXElement(expression) && - t.isJSXIdentifier(expression.openingElement.name) && - !isComponent(expression.openingElement.name.name) - ) { - /** - * Handles raw JSX elements as expressions: - * - * ```js - *
{}
- * ``` - */ - transformJSX( - options, - { - jsx: expression, - jsxPath: jsxPath.get(`children.${i}`) as NodePath, - componentBody, - componentBodyPath, - dynamics, - isRoot: false, - }, - SHARED, - ); - jsx.children[i] = expression; - continue; + if (t.isJSXElement(expression)) { + const type = expression.openingElement.name; + if (t.isJSXIdentifier(type) && !isComponent(type.name)) { + /** + * Handles raw JSX elements as expressions: + * + * ```js + *
{}
+ * ``` + */ + transformJSX( + options, + { + jsx: expression, + jsxPath: jsxPath.get(`children.${i}`) as NodePath, + componentBody, + componentBodyPath, + dynamics, + isRoot: false, + }, + SHARED, + ); + jsx.children[i] = expression; + continue; + } + if (t.isJSXMemberExpression(type)) { + const id = createPortal(() => { + jsx.children[i] = t.jsxExpressionContainer(id!); + }, [expression, t.booleanLiteral(unstable)]); + + continue; + } } if (t.isExpression(expression)) { @@ -1003,16 +1100,10 @@ export const transformJSX = ( options.mute, ); - const renderReactScope = imports.addNamed('renderReactScope'); - - const nestedRender = t.callExpression(renderReactScope, [ - newJsxArrayIterator, - t.booleanLiteral(unstable), - ]); - - const id = createDynamic(null, nestedRender, null, () => { + const id = createPortal(() => { jsx.children[i] = t.jsxExpressionContainer(id!); - }); + }, [newJsxArrayIterator, t.booleanLiteral(unstable)]); + continue; } diff --git a/packages/compiler/react/types.ts b/packages/compiler/react/types.ts index f02c7dfa9c..9550337dcb 100644 --- a/packages/compiler/react/types.ts +++ b/packages/compiler/react/types.ts @@ -20,11 +20,14 @@ export interface Shared { export interface Dynamics { cache: Set; - props?: t.Expression; data: { id: t.Identifier; value: t.Expression | null; }[]; deferred: (() => void)[]; unoptimizable: boolean; + portalInfo: { + index: number; + id: t.Identifier; + }; } diff --git a/packages/kitchen-sink/README.md b/packages/kitchen-sink/README.md index 88685014cc..d2ae39a573 100644 --- a/packages/kitchen-sink/README.md +++ b/packages/kitchen-sink/README.md @@ -1,6 +1,6 @@ # Million.js Kitchen Sink 🧑‍🍳 -Hey! We're actively recruiting cooks 🧑‍🍳 to help assemble a list of examples of Million + your favorite React library. +Hey! We're actively recruiting cooks 🧑‍🍳 to help assemble a list of examples of Million + your favorite React library. [View it live at sink.million.dev](https://sink.million.dev) ## Getting Started diff --git a/packages/kitchen-sink/src/examples/context.tsx b/packages/kitchen-sink/src/examples/context.tsx new file mode 100644 index 0000000000..5720734900 --- /dev/null +++ b/packages/kitchen-sink/src/examples/context.tsx @@ -0,0 +1,26 @@ +import { useState, createContext, useContext } from 'react'; +import { block } from 'million/react'; + +const ThemeContext = createContext('light'); + +const Context = block(() => { + const theme = useContext(ThemeContext); + + return ( +
{theme}
+ ); +}); + +const App = block(() => { + const [theme, setTheme] = useState('light'); + + return ( + + + + ); +}); + +export default App; diff --git a/packages/million/block.ts b/packages/million/block.ts index ffc1271aa9..42b03a20ad 100644 --- a/packages/million/block.ts +++ b/packages/million/block.ts @@ -142,8 +142,8 @@ export class Block extends AbstractBlock { } if (!el[TEXT_NODE_CACHE]) el[TEXT_NODE_CACHE] = new Array(l); - if (typeof value === 'function') { - const scopeEl = value(null); + if (value && typeof value === 'object' && 'foreign' in value) { + const scopeEl = value.current; el[TEXT_NODE_CACHE][k] = scopeEl; insertBefore$.call(el, scopeEl, childAt(el, edit.i!)); continue; @@ -231,14 +231,18 @@ export class Block extends AbstractBlock { oldValue.p(newChildBlock); continue; } - if (typeof newValue === 'function') { + if ( + newValue && + typeof newValue === 'object' && + 'foreign' in newValue + ) { const scopeEl = el[TEXT_NODE_CACHE][k]; if ('unstable' in newValue && oldValue !== newValue) { - const newScopeEl = newValue(null); + const newScopeEl = newValue.current; el[TEXT_NODE_CACHE][k] = newScopeEl; replaceChild$.call(el, newScopeEl, scopeEl); } else { - newValue(scopeEl); + newValue.current = scopeEl; } continue; diff --git a/packages/react-server/index.ts b/packages/react-server/index.ts index e893e49fb6..30a32f2f9c 100644 --- a/packages/react-server/index.ts +++ b/packages/react-server/index.ts @@ -35,15 +35,12 @@ export const block =

( }; }, []); - if (!ready || !blockFactory || !props.__props) { + if (!ready || !blockFactory) { if (options.ssr === false) return null; return createElement

( RENDER_SCOPE, null, - // During compilation we will attach a .original for the component and - // pass __props as the props to the component. This references - // the original component for SSR. - createElement((options.original as any) || Component, props.__props), + createElement(Component, props as any), ); } diff --git a/packages/react/block.ts b/packages/react/block.ts index ef75059eab..8d7cbb57a0 100644 --- a/packages/react/block.ts +++ b/packages/react/block.ts @@ -9,7 +9,7 @@ import { queueMicrotask$ } from '../million/dom'; import { processProps, unwrap } from './utils'; import { Effect, RENDER_SCOPE, REGISTRY, SVG_RENDER_SCOPE } from './constants'; import type { ComponentType, Ref } from 'react'; -import type { Options, MillionProps } from '../types'; +import type { Options, MillionProps, MillionPortal } from '../types'; export const block =

( fn: ComponentType

| null, @@ -26,8 +26,9 @@ export const block =

( ) => { const ref = useRef(null); const patch = useRef<((props: P) => void) | null>(null); + const portalRef = useRef([]); - props = processProps(props, forwardedRef); + props = processProps(props, forwardedRef, portalRef.current); patch.current?.(props); const effect = useCallback(() => { @@ -53,6 +54,7 @@ export const block =

( null, marker, createElement(Effect, { effect }), + ...portalRef.current.map((p) => p.portal), ); return vnode; diff --git a/packages/react/for.ts b/packages/react/for.ts index bcc65407f7..b67f6806cf 100644 --- a/packages/react/for.ts +++ b/packages/react/for.ts @@ -7,7 +7,12 @@ import { renderReactScope } from './utils'; import { RENDER_SCOPE, REGISTRY, SVG_RENDER_SCOPE } from './constants'; import type { Block } from '../million'; import type { MutableRefObject } from 'react'; -import type { ArrayCache, MillionArrayProps, MillionProps } from '../types'; +import type { + ArrayCache, + MillionArrayProps, + MillionPortal, + MillionProps, +} from '../types'; const MillionArray = ({ each, @@ -70,6 +75,7 @@ const createChildren = ( memo?: boolean, ): Block[] => { const children = Array(each.length); + const portalRef = useRef([]); const currentCache = cache.current; for (let i = 0, l = each.length; i < l; ++i) { if (memo && currentCache.each && currentCache.each[i] === each[i]) { @@ -101,7 +107,13 @@ const createChildren = ( const currentBlock = (props: MillionProps) => { return block( { - scope: renderReactScope(createElement(vnode.type, props)), + scope: renderReactScope( + createElement(vnode.type, props), + false, + portalRef.current, + i, + false, + ), }, vnode.key ? String(vnode.key) : undefined, ); diff --git a/packages/react/utils.ts b/packages/react/utils.ts index a593b20208..41530392e5 100644 --- a/packages/react/utils.ts +++ b/packages/react/utils.ts @@ -1,16 +1,31 @@ -import { Fragment, createElement, isValidElement, version } from 'react'; -import { REACT_ROOT, REGISTRY, RENDER_SCOPE } from './constants'; +import { Fragment, createElement, isValidElement } from 'react'; +import { createPortal } from 'react-dom'; +import { REGISTRY, RENDER_SCOPE } from './constants'; import type { ComponentProps, ReactNode, Ref } from 'react'; import type { VNode } from '../million'; +import type { MillionPortal } from '../types'; // TODO: access perf impact of this -export const processProps = (props: ComponentProps, ref: Ref) => { +export const processProps = ( + props: ComponentProps, + ref: Ref, + portals: MillionPortal[], +) => { const processedProps: ComponentProps = { ref }; + let currentIndex = 0; + for (const key in props) { const value = props[key]; if (isValidElement(value)) { - processedProps[key] = renderReactScope(value); + processedProps[key] = renderReactScope( + value, + false, + portals, + currentIndex++, + false, + ); + continue; } processedProps[key] = props[key]; @@ -19,8 +34,15 @@ export const processProps = (props: ComponentProps, ref: Ref) => { return processedProps; }; -export const renderReactScope = (vnode: ReactNode, unstable?: boolean) => { - if (typeof window === 'undefined') { +export const renderReactScope = ( + vnode: ReactNode, + unstable: boolean, + portals: MillionPortal[], + currentIndex: number, + server: boolean, +) => { + const el = portals[currentIndex]?.current; + if (typeof window === 'undefined' || (server && !el)) { return createElement( RENDER_SCOPE, { suppressHydrationWarning: true }, @@ -42,32 +64,17 @@ export const renderReactScope = (vnode: ReactNode, unstable?: boolean) => { } } - const scope = (el: HTMLElement | null) => { - let root; - const parent = el ?? document.createElement(RENDER_SCOPE); - if (version.startsWith('18')) { - import('react-dom/client') - .then((res) => { - root = - REACT_ROOT in parent - ? parent[REACT_ROOT] - : (parent[REACT_ROOT] = res.createRoot(parent)); - root.render(vnode); - }) - .catch((e) => { - // eslint-disable-next-line no-console - console.error(e); - }); - } else { - root = parent[REACT_ROOT]; - root.render(vnode); - } - return parent; + const current = el ?? document.createElement(RENDER_SCOPE); + const reactPortal = createPortal(vnode, current); + const millionPortal = { + foreign: true as const, + current, + portal: reactPortal, + unstable, }; + portals[currentIndex] = millionPortal; - if (unstable) scope.unstable = true; - - return scope; + return millionPortal; }; export const unwrap = (vnode: JSX.Element | null): VNode => { diff --git a/packages/types/index.ts b/packages/types/index.ts index b1057f02f8..133a6c9c22 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -1,12 +1,11 @@ import type { block as createBlock } from '../million'; -import type { ComponentType } from 'react'; +import type { ReactPortal } from 'react'; export type MillionProps = Record; export interface Options { shouldUpdate?: (oldProps: MillionProps, newProps: MillionProps) => boolean; block?: any; - original?: ComponentType; ssr?: boolean; svg?: boolean; as?: string; @@ -28,3 +27,10 @@ export interface ArrayCache { mounted?: boolean | null; block?: ReturnType; } + +export interface MillionPortal { + foreign: true; + current: HTMLElement; + portal: ReactPortal; + unstable?: boolean; +} diff --git a/website/pages/_meta.json b/website/pages/_meta.json index a968cc9b50..c5958cc55c 100644 --- a/website/pages/_meta.json +++ b/website/pages/_meta.json @@ -15,17 +15,25 @@ "type": "page", "title": "Blog" }, - "foundation": { + "showcase": { "type": "page", - "title": "Foundation", + "title": "Showcase", "theme": { - "typesetting": "article" + "typesetting": "article", + "layout": "full" } }, "more": { "title": "More", "type": "menu", "items": { + "foundation": { + "type": "page", + "title": "Foundation", + "theme": { + "typesetting": "article" + } + }, "research": { "title": "Research", "href": "https://dl.acm.org/doi/10.1145/3555776.3577683", diff --git a/website/pages/showcase.mdx b/website/pages/showcase.mdx new file mode 100644 index 0000000000..f66604b1c1 --- /dev/null +++ b/website/pages/showcase.mdx @@ -0,0 +1,62 @@ +--- +title: Showcase +--- + +import { Card, Cards } from 'nextra/components'; + +{

Showcase

} + +{

Projects powered by Million.js.

} + + + + <>![Wyze preview](./showcase/wyze.png) + + + <>![Dona AI preview](./showcase/dona-ai.jpeg) + + + <>![T4 Stack preview](./showcase/t4stack.png) + + + <>![VeganCheck.me preview](./showcase/vegancheck.png) + + + <>![Windows 11 Web preview](./showcase/windows-11-web.jpeg) + + + <>![jahir.dev preview](./showcase/jahir-dev.png) + + + <>![LogLib preview](./showcase/loglib.png) + + + <>![Comty preview](./showcase/comty.svg) + + + +export const ShowcaseCard = Object.assign( + // Copy card component and add default props + Card.bind(), + { + displayName: 'ShowcaseCard', + defaultProps: { + image: true, + arrow: true, + target: '_blank', + }, + }, +); + + diff --git a/website/pages/showcase/comty.svg b/website/pages/showcase/comty.svg new file mode 100644 index 0000000000..594dadfaa5 --- /dev/null +++ b/website/pages/showcase/comty.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/website/pages/showcase/dona-ai.jpeg b/website/pages/showcase/dona-ai.jpeg new file mode 100644 index 0000000000..c782d90004 Binary files /dev/null and b/website/pages/showcase/dona-ai.jpeg differ diff --git a/website/pages/showcase/jahir-dev.png b/website/pages/showcase/jahir-dev.png new file mode 100644 index 0000000000..faf5f07290 Binary files /dev/null and b/website/pages/showcase/jahir-dev.png differ diff --git a/website/pages/showcase/loglib.png b/website/pages/showcase/loglib.png new file mode 100644 index 0000000000..309d146979 Binary files /dev/null and b/website/pages/showcase/loglib.png differ diff --git a/website/pages/showcase/t4stack.png b/website/pages/showcase/t4stack.png new file mode 100644 index 0000000000..7fb29721fb Binary files /dev/null and b/website/pages/showcase/t4stack.png differ diff --git a/website/pages/showcase/vegancheck.png b/website/pages/showcase/vegancheck.png new file mode 100644 index 0000000000..ddc40747bc Binary files /dev/null and b/website/pages/showcase/vegancheck.png differ diff --git a/website/pages/showcase/windows-11-web.jpeg b/website/pages/showcase/windows-11-web.jpeg new file mode 100644 index 0000000000..542a6acf0b Binary files /dev/null and b/website/pages/showcase/windows-11-web.jpeg differ diff --git a/website/pages/showcase/wyze.png b/website/pages/showcase/wyze.png new file mode 100644 index 0000000000..09bfdf3cf8 Binary files /dev/null and b/website/pages/showcase/wyze.png differ