fix: hook splitting, naming, strict mode
This commit is contained in:
parent
87e9836cca
commit
d2fabd902d
|
@ -5,8 +5,9 @@ export const hooks = {
|
|||
* Note: `req` will be only be defined when server-side rendering.
|
||||
* @param {http.ClientRequest} req The HTTP request object.
|
||||
* @invoke SequentialAsync
|
||||
* @returns {[ReactContextProvider<Props>, Props]} An array where the first element is a React
|
||||
* context provider and the second element is the `props` passed to the context provider.
|
||||
* @returns {ReactContextProvider | [ReactContextProvider<Props>, Props]} A React context
|
||||
* provider or an array where the first element is a React context provider and the second
|
||||
* element is the `props` passed to the provider.
|
||||
*/
|
||||
'@flecks/react.providers': (req) => {
|
||||
return req ? serverSideProvider(req) : [SomeContext.Provider, {value: 'whatever'}];
|
||||
|
@ -19,7 +20,9 @@ export const hooks = {
|
|||
* or an array of two elements where the first element is the component and the second element
|
||||
* is the props passed to the component.
|
||||
* @param {http.ClientRequest} req The HTTP request object.
|
||||
* @invoke Async
|
||||
* @returns {Component | [Component<Props>, Props]} A React component or an array where the first
|
||||
* element is a React component and the second element is the `props` passed to the component.
|
||||
* @invoke SequentialAsync
|
||||
*/
|
||||
'@flecks/react.roots': (req) => {
|
||||
// Note that we're not returning `<Component />`, but `Component`.
|
||||
|
|
|
@ -43,8 +43,8 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-router": "6.2.1",
|
||||
"react-router-dom": "6.2.1",
|
||||
"react-router": "6.22.1",
|
||||
"react-router-dom": "6.22.1",
|
||||
"react-tabs": "^6.0.2",
|
||||
"redux-first-history": "5.1.1"
|
||||
},
|
||||
|
|
|
@ -1,30 +1,21 @@
|
|||
import {D} from '@flecks/core';
|
||||
import React from 'react';
|
||||
import {createRoot, hydrateRoot} from 'react-dom/client';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import FlecksContext from '@flecks/react/context';
|
||||
import root from './root';
|
||||
import {createRootElement} from './create-root-element';
|
||||
|
||||
const debug = D('@flecks/react/client');
|
||||
|
||||
export {FlecksContext};
|
||||
|
||||
export const hooks = {
|
||||
'@flecks/web/client.up': async (container, flecks) => {
|
||||
const {ssr} = flecks.get('@flecks/react');
|
||||
debug('%sing...', ssr ? 'hydrat' : 'render');
|
||||
const RootComponent = React.createElement(
|
||||
React.StrictMode,
|
||||
{},
|
||||
[React.createElement(await root(flecks), {key: 'root'})],
|
||||
);
|
||||
const RootElement = await createRootElement(flecks);
|
||||
// API™.
|
||||
if (ssr) {
|
||||
hydrateRoot(container, RootComponent);
|
||||
hydrateRoot(container, RootElement);
|
||||
}
|
||||
else {
|
||||
createRoot(container).render(RootComponent);
|
||||
createRoot(container).render(RootElement);
|
||||
}
|
||||
debug('rendered');
|
||||
},
|
||||
|
|
71
packages/react/src/create-root-element.js
Normal file
71
packages/react/src/create-root-element.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import {D} from '@flecks/core';
|
||||
import React from 'react';
|
||||
|
||||
const debug = D('@flecks/react/root');
|
||||
const debugSilly = debug.extend('silly');
|
||||
|
||||
export async function createRootElement(flecks, req, res) {
|
||||
const Providers = {};
|
||||
const implementingProvider = flecks.flecksImplementing('@flecks/react.providers');
|
||||
for (let i = 0; i < implementingProvider.length; ++i) {
|
||||
const fleck = implementingProvider[i];
|
||||
const implementation = flecks.fleckImplementation(fleck, '@flecks/react.providers');
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
Providers[fleck] = await implementation(req, res, flecks);
|
||||
}
|
||||
debugSilly('providers: %O', Providers);
|
||||
const Roots = {};
|
||||
const implementingRoot = flecks.flecksImplementing('@flecks/react.roots');
|
||||
for (let i = 0; i < implementingRoot.length; ++i) {
|
||||
const fleck = implementingRoot[i];
|
||||
const implementation = flecks.fleckImplementation(fleck, '@flecks/react.roots');
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
Roots[fleck] = await implementation(req, res, flecks);
|
||||
}
|
||||
debugSilly('roots: %O', Roots);
|
||||
const RootElements = Object.entries(Roots)
|
||||
.filter(([, ComponentOrTuple]) => ComponentOrTuple)
|
||||
.map(([fleck, ComponentOrTuple]) => {
|
||||
let Root;
|
||||
let props = {};
|
||||
if (Array.isArray(ComponentOrTuple)) {
|
||||
[Root, props] = ComponentOrTuple;
|
||||
}
|
||||
else {
|
||||
Root = ComponentOrTuple;
|
||||
}
|
||||
return React.createElement(Root, {key: `@flecks/react/root(${fleck})`, ...props});
|
||||
});
|
||||
let [RootElement] = Object.entries(Providers)
|
||||
.filter(([, ComponentOrTuple]) => ComponentOrTuple)
|
||||
.reduceRight(
|
||||
(children, [fleck, ComponentOrTuple]) => {
|
||||
let Provider;
|
||||
let props = {};
|
||||
if (Array.isArray(ComponentOrTuple)) {
|
||||
[Provider, props] = ComponentOrTuple;
|
||||
}
|
||||
else {
|
||||
Provider = ComponentOrTuple;
|
||||
}
|
||||
return [
|
||||
React.createElement(
|
||||
Provider,
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
{key: `@flecks/react/provider(${fleck})`, ...props},
|
||||
children,
|
||||
),
|
||||
];
|
||||
},
|
||||
RootElements,
|
||||
);
|
||||
const {strictMode} = flecks.get('@flecks/react');
|
||||
if (strictMode) {
|
||||
RootElement = React.createElement(
|
||||
React.StrictMode,
|
||||
{},
|
||||
[React.cloneElement(RootElement, {key: '@flecks/react.strictMode'})],
|
||||
);
|
||||
}
|
||||
return RootElement;
|
||||
}
|
|
@ -1,15 +1,12 @@
|
|||
import FlecksContext from './context';
|
||||
|
||||
export {default as classnames} from 'classnames';
|
||||
export {default as PropTypes} from 'prop-types';
|
||||
export {default as React} from 'react';
|
||||
export {default as ReactDom} from 'react-dom';
|
||||
export * from 'react';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved
|
||||
export {default as FlecksContext} from '@flecks/react/context';
|
||||
export {default as gatherComponents} from './gather-components';
|
||||
export {default as useEvent} from './hooks/use-event';
|
||||
export {default as useFlecks} from './hooks/use-flecks';
|
||||
export {default as usePrevious} from './hooks/use-previous';
|
||||
export * from './react-hooks';
|
||||
|
||||
export const hooks = {
|
||||
'@flecks/core.config': () => ({
|
||||
|
@ -21,6 +18,11 @@ export const hooks = {
|
|||
* Whether to enable server-side rendering.
|
||||
*/
|
||||
ssr: true,
|
||||
/**
|
||||
* Whether to wrap the React root with `React.StrictMode`.
|
||||
*/
|
||||
strictMode: true,
|
||||
}),
|
||||
'@flecks/react.providers': (req, res, flecks) => [FlecksContext.Provider, {value: flecks}],
|
||||
'@flecks/web.config': async (req, flecks) => flecks.get('@flecks/react'),
|
||||
};
|
||||
|
|
3
packages/react/src/react-hooks/index.js
vendored
Normal file
3
packages/react/src/react-hooks/index.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
export {default as useEvent} from './use-event';
|
||||
export {default as useFlecks} from './use-flecks';
|
||||
export {default as usePrevious} from './use-previous';
|
|
@ -1,5 +1,4 @@
|
|||
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved
|
||||
import {FlecksContext} from '@flecks/react/client';
|
||||
import FlecksContext from '@flecks/react/context';
|
||||
import {useContext, useEffect, useState} from 'react';
|
||||
|
||||
export default () => {
|
|
@ -1,37 +0,0 @@
|
|||
import {D} from '@flecks/core';
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import FlecksContext from '@flecks/react/context';
|
||||
|
||||
import gatherComponents from './gather-components';
|
||||
|
||||
const debug = D('@flecks/react/root');
|
||||
const debugSilly = debug.extend('silly');
|
||||
|
||||
export default async (flecks, req) => {
|
||||
const Roots = await flecks.invokeAsync('@flecks/react.roots', req);
|
||||
debugSilly('roots: %O', Roots);
|
||||
const Providers = await flecks.invokeSequentialAsync('@flecks/react.providers', req);
|
||||
const FlattenedProviders = [];
|
||||
for (let i = 0; i < Providers.length; i++) {
|
||||
const Provider = Providers[i];
|
||||
if (Provider.length > 0) {
|
||||
FlattenedProviders.push(...(Array.isArray(Provider[0]) ? Provider : [Provider]));
|
||||
}
|
||||
}
|
||||
debugSilly('providers: %O', FlattenedProviders);
|
||||
return () => {
|
||||
const RootElements = [[FlecksContext.Provider, {value: flecks}]]
|
||||
.concat(FlattenedProviders)
|
||||
.reduceRight((children, [Provider, props], i) => [
|
||||
React.createElement(
|
||||
Provider,
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
{key: `@flecks/react/provider(${i})`, ...props},
|
||||
children,
|
||||
),
|
||||
], gatherComponents(Roots));
|
||||
return RootElements[0];
|
||||
};
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
import {Flecks} from '@flecks/core';
|
||||
|
||||
import ssr from './ssr';
|
||||
|
||||
export const hooks = {
|
||||
'@flecks/electron/server.extensions': (installer) => [installer.REACT_DEVELOPER_TOOLS],
|
||||
'@flecks/web/server.stream.html': Flecks.priority(
|
||||
(stream, req, res, flecks) => (
|
||||
flecks.get('@flecks/react.ssr') ? ssr(stream, req, flecks) : stream
|
||||
),
|
||||
{after: '@flecks/web/server'},
|
||||
),
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export const hook = (installer) => [installer.REACT_DEVELOPER_TOOLS];
|
56
packages/react/src/server/hooks/@flecks/web/_parse-html.js
Normal file
56
packages/react/src/server/hooks/@flecks/web/_parse-html.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import {WritableStream} from 'htmlparser2/lib/WritableStream';
|
||||
|
||||
export async function parseHtml(stream) {
|
||||
const css = [];
|
||||
let hasVendor = false;
|
||||
let inline = '';
|
||||
let isInScript = 0;
|
||||
let isSkipping = false;
|
||||
const js = [];
|
||||
const parserStream = new WritableStream({
|
||||
onclosetag(tagName) {
|
||||
if ('script' === tagName) {
|
||||
isInScript -= 1;
|
||||
isSkipping = false;
|
||||
}
|
||||
},
|
||||
onopentag(tagName, attribs) {
|
||||
if ('script' === tagName) {
|
||||
isInScript += 1;
|
||||
if ('ignore' === attribs?.['data-flecks']) {
|
||||
isSkipping = true;
|
||||
}
|
||||
else if (attribs.src) {
|
||||
if (attribs.src.match(/web-vendor\.js$/)) {
|
||||
hasVendor = true;
|
||||
}
|
||||
else {
|
||||
js.push(attribs.src);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ('style' === tagName && attribs['data-href']) {
|
||||
css.push(attribs['data-href']);
|
||||
}
|
||||
if ('link' === tagName && 'stylesheet' === attribs.rel && attribs.href) {
|
||||
css.push(attribs.href);
|
||||
}
|
||||
},
|
||||
ontext(text) {
|
||||
if (isInScript > 0 && !isSkipping) {
|
||||
inline += text;
|
||||
}
|
||||
},
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
const piped = stream.pipe(parserStream);
|
||||
piped.on('error', reject);
|
||||
piped.on('finish', resolve);
|
||||
});
|
||||
return {
|
||||
css,
|
||||
hasVendor,
|
||||
inline,
|
||||
js,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import {Readable} from 'stream';
|
||||
|
||||
import React from 'react';
|
||||
import {renderToPipeableStream} from 'react-dom/server';
|
||||
|
||||
import {Flecks} from '@flecks/core';
|
||||
import {pipesink} from '@flecks/core/server';
|
||||
import {configSource} from '@flecks/web/server';
|
||||
import {createRootElement} from '@flecks/react/create-root-element';
|
||||
|
||||
import DocumentComponent from './_document';
|
||||
import {parseHtml} from './_parse-html';
|
||||
|
||||
export const hook = Flecks.priority(
|
||||
async (stream, req, res, flecks) => {
|
||||
if (!flecks.get('@flecks/react.ssr')) {
|
||||
return stream;
|
||||
}
|
||||
const {
|
||||
appMountId,
|
||||
base,
|
||||
icon,
|
||||
meta,
|
||||
title,
|
||||
} = flecks.get('@flecks/web');
|
||||
// Fallback.
|
||||
const fallback = pipesink(stream);
|
||||
// Extract assets from HTML generated up to this point.
|
||||
const {
|
||||
css,
|
||||
hasVendor,
|
||||
inline,
|
||||
js,
|
||||
} = await parseHtml(stream);
|
||||
// Render document.
|
||||
const DocumentElement = React.createElement(
|
||||
DocumentComponent,
|
||||
{
|
||||
appMountId: flecks.interpolate(appMountId),
|
||||
base: flecks.interpolate(base),
|
||||
config: React.createElement(
|
||||
'script',
|
||||
{dangerouslySetInnerHTML: {__html: await configSource(flecks, req)}},
|
||||
),
|
||||
css,
|
||||
hasVendor,
|
||||
icon,
|
||||
meta,
|
||||
root: await createRootElement(flecks, req, res),
|
||||
title: flecks.interpolate(title),
|
||||
},
|
||||
);
|
||||
return new Promise((resolve) => {
|
||||
const rendered = renderToPipeableStream(
|
||||
DocumentElement,
|
||||
{
|
||||
bootstrapScripts: js,
|
||||
bootstrapScriptContent: inline,
|
||||
async onError(error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('SSR error:', error);
|
||||
resolve(Readable.from(await fallback));
|
||||
},
|
||||
async onShellError(error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('SSR shell error:', error);
|
||||
resolve(Readable.from(await fallback));
|
||||
},
|
||||
onShellReady() {
|
||||
resolve(rendered);
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
{after: '@flecks/web/server'},
|
||||
);
|
3
packages/react/src/server/index.js
Normal file
3
packages/react/src/server/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import {Flecks} from '@flecks/core';
|
||||
|
||||
export const hooks = Flecks.hooks(require.context('./hooks'));
|
|
@ -1,110 +0,0 @@
|
|||
import {Readable} from 'stream';
|
||||
|
||||
import {WritableStream} from 'htmlparser2/lib/WritableStream';
|
||||
import React from 'react';
|
||||
import {renderToPipeableStream} from 'react-dom/server';
|
||||
|
||||
import {configSource} from '@flecks/web/server';
|
||||
import Document from './document';
|
||||
import root from './root';
|
||||
|
||||
export default async (stream, req, flecks) => {
|
||||
const {
|
||||
appMountId,
|
||||
base,
|
||||
icon,
|
||||
meta,
|
||||
title,
|
||||
} = flecks.get('@flecks/web');
|
||||
// Extract assets.
|
||||
const css = [];
|
||||
let hasVendor = false;
|
||||
let inline = '';
|
||||
let isInScript = 0;
|
||||
let isSkipping = false;
|
||||
const js = [];
|
||||
const parserStream = new WritableStream({
|
||||
onclosetag(tagName) {
|
||||
if ('script' === tagName) {
|
||||
isInScript -= 1;
|
||||
isSkipping = false;
|
||||
}
|
||||
},
|
||||
onopentag(tagName, attribs) {
|
||||
if ('script' === tagName) {
|
||||
isInScript += 1;
|
||||
if ('ignore' === attribs?.['data-flecks']) {
|
||||
isSkipping = true;
|
||||
}
|
||||
else if (attribs.src) {
|
||||
if (attribs.src.match(/web-vendor\.js$/)) {
|
||||
hasVendor = true;
|
||||
}
|
||||
else {
|
||||
js.push(attribs.src);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ('style' === tagName && attribs['data-href']) {
|
||||
css.push(attribs['data-href']);
|
||||
}
|
||||
if ('link' === tagName && 'stylesheet' === attribs.rel && attribs.href) {
|
||||
css.push(attribs.href);
|
||||
}
|
||||
},
|
||||
ontext(text) {
|
||||
if (isInScript > 0 && !isSkipping) {
|
||||
inline += text;
|
||||
}
|
||||
},
|
||||
});
|
||||
const chunks = [];
|
||||
stream.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
const piped = stream.pipe(parserStream);
|
||||
piped.on('error', reject);
|
||||
piped.on('finish', resolve);
|
||||
});
|
||||
// Render document.
|
||||
const DocumentElement = React.createElement(
|
||||
Document,
|
||||
{
|
||||
appMountId: flecks.interpolate(appMountId),
|
||||
base: flecks.interpolate(base),
|
||||
config: React.createElement(
|
||||
'script',
|
||||
{dangerouslySetInnerHTML: {__html: await configSource(flecks, req)}},
|
||||
),
|
||||
css,
|
||||
hasVendor,
|
||||
icon,
|
||||
meta,
|
||||
root: React.createElement(await root(flecks, req)),
|
||||
title: flecks.interpolate(title),
|
||||
},
|
||||
);
|
||||
return new Promise((resolve) => {
|
||||
const rendered = renderToPipeableStream(
|
||||
DocumentElement,
|
||||
{
|
||||
bootstrapScripts: js,
|
||||
bootstrapScriptContent: inline,
|
||||
onError(error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('SSR error:', error);
|
||||
resolve(Readable.from(Buffer.concat(chunks)));
|
||||
},
|
||||
onShellError(error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('SSR shell error:', error);
|
||||
resolve(Readable.from(Buffer.concat(chunks)));
|
||||
},
|
||||
onShellReady() {
|
||||
resolve(rendered);
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user