react-streaming
Advanced tools
Comparing version 0.1.4 to 0.1.5
@@ -5,9 +5,4 @@ export { renderToStream }; | ||
import { Pipe } from './renderToStream/createPipeWrapper'; | ||
declare type Return = { | ||
pipe: null | Pipe; | ||
readable: null | ReadableStream; | ||
injectToStream: (chunk: string) => void; | ||
}; | ||
declare type SeoStrategy = 'conservative' | 'google-speed'; | ||
declare function renderToStream(element: React.ReactNode, options?: { | ||
import { SeoStrategy } from './renderToStream/resolveSeoStrategy'; | ||
declare type Options = { | ||
debug?: boolean; | ||
@@ -19,2 +14,8 @@ webStream?: boolean; | ||
renderToReadableStream?: typeof renderToReadableStream; | ||
}): Promise<Return>; | ||
}; | ||
declare type Result = { | ||
pipe: null | Pipe; | ||
readable: null | ReadableStream; | ||
injectToStream: (chunk: string) => void; | ||
}; | ||
declare function renderToStream(element: React.ReactNode, options?: Options): Promise<Result>; |
@@ -11,86 +11,86 @@ "use strict"; | ||
const useStream_1 = require("./useStream"); | ||
const utils_1 = require("./utils"); | ||
const isbot_fast_1 = __importDefault(require("isbot-fast")); | ||
const createPipeWrapper_1 = require("./renderToStream/createPipeWrapper"); | ||
const createReadableWrapper_1 = require("./renderToStream/createReadableWrapper"); | ||
const resolveSeoStrategy_1 = require("./renderToStream/resolveSeoStrategy"); | ||
async function renderToStream(element, options = {}) { | ||
var _a, _b, _c; | ||
let reject; | ||
let resolve; | ||
let resolved = false; | ||
const promise = new Promise((resolve_, reject_) => { | ||
resolve = () => { | ||
if (resolved) | ||
return; | ||
resolved = true; | ||
resolve_({ pipe, readable, injectToStream }); | ||
}; | ||
reject = (err) => { | ||
if (resolved) | ||
return; | ||
resolved = true; | ||
reject_(err); | ||
}; | ||
}); | ||
const seoStrategy = options.seoStrategy || 'conservative'; | ||
const disabled = (_a = options.disabled) !== null && _a !== void 0 ? _a : (() => { | ||
if (!options.userAgent) { | ||
(0, utils_1.assertWarning)(false, 'Streaming disabled. Provide `options.userAgent` to enable streaming. (react-streaming needs the User Agent string in order to be able to disable streaming for bots, e.g. for Google Bot.) Or set `options.disabled` to `true` to get rid of this warning.'); | ||
return true; | ||
} | ||
// https://github.com/omrilotan/isbot | ||
// https://github.com/mahovich/isbot-fast | ||
// https://stackoverflow.com/questions/34647657/how-to-detect-web-crawlers-for-seo-using-express/68869738#68869738 | ||
if (!(0, isbot_fast_1.default)(options.userAgent)) { | ||
return false; | ||
} | ||
const isGoogleBot = options.userAgent.toLowerCase().includes('googlebot'); | ||
if (seoStrategy === 'google-speed' && isGoogleBot) { | ||
return false; | ||
} | ||
return true; | ||
})(); | ||
// options.debug = true | ||
const webStream = (_b = options.webStream) !== null && _b !== void 0 ? _b : !(0, createPipeWrapper_1.nodeStreamModuleIsAvailable)(); | ||
const onError = (err) => { | ||
reject(err); | ||
}; | ||
const streamUtils = { injectToStream: (chunk) => injectToStream(chunk) }; | ||
element = react_1.default.createElement(useStream_1.StreamProvider, { value: streamUtils }, element); | ||
var _a, _b; | ||
element = react_1.default.createElement(useSsrData_1.SsrDataProvider, null, element); | ||
let pipe = null; | ||
let readable = null; | ||
let injectToStream; | ||
element = react_1.default.createElement(useStream_1.StreamProvider, { value: { injectToStream: (chunk) => injectToStream(chunk) } }, element); | ||
const disabled = (_a = options.disabled) !== null && _a !== void 0 ? _a : (0, resolveSeoStrategy_1.resolveSeoStrategy)(options).disableStream; | ||
const webStream = (_b = options.webStream) !== null && _b !== void 0 ? _b : !(0, createPipeWrapper_1.nodeStreamModuleIsAvailable)(); | ||
if (!webStream) { | ||
const { pipe: pipeOriginal } = (0, server_1.renderToPipeableStream)(element, { | ||
onAllReady() { | ||
resolve(); | ||
}, | ||
onShellReady() { | ||
if (!disabled) { | ||
resolve(); | ||
} | ||
}, | ||
onShellError: onError, | ||
onError | ||
}); | ||
const { pipeWrapper, injectToStream: injectToStream_ } = (0, createPipeWrapper_1.createPipeWrapper)(pipeOriginal, options); | ||
pipe = pipeWrapper; | ||
injectToStream = injectToStream_; | ||
pipe.injectToStream = injectToStream; | ||
const result = await renderToNodeStream(element, disabled, options); | ||
injectToStream = result.injectToStream; | ||
return result; | ||
} | ||
else { | ||
const readableOriginal = await ((_c = options.renderToReadableStream) !== null && _c !== void 0 ? _c : server_1.renderToReadableStream)(element, { | ||
onError | ||
}); | ||
if (disabled) { | ||
await readableOriginal.allReady; | ||
} | ||
const { readableWrapper, injectToStream: injectToStream_ } = (0, createReadableWrapper_1.createReadableWrapper)(readableOriginal, options); | ||
readable = readableWrapper; | ||
injectToStream = injectToStream_; | ||
const result = await renderToWebStream(element, disabled, options); | ||
injectToStream = result.injectToStream; | ||
return result; | ||
} | ||
} | ||
exports.renderToStream = renderToStream; | ||
async function renderToNodeStream(element, disabled, options) { | ||
let resolve; | ||
const promise = new Promise((r) => { | ||
resolve = () => r(); | ||
}); | ||
let didError = false; | ||
let firstErr = null; | ||
const onError = (err) => { | ||
didError = true; | ||
firstErr = firstErr || err; | ||
resolve(); | ||
}; | ||
const { pipe: pipeOriginal } = (0, server_1.renderToPipeableStream)(element, { | ||
onAllReady() { | ||
resolve(); | ||
}, | ||
onShellReady() { | ||
if (!disabled) { | ||
resolve(); | ||
} | ||
}, | ||
onError, | ||
onShellError: onError | ||
}); | ||
const { pipeWrapper, injectToStream } = (0, createPipeWrapper_1.createPipeWrapper)(pipeOriginal, { | ||
debug: options.debug, | ||
onError | ||
}); | ||
await promise; | ||
if (didError) { | ||
throw firstErr; | ||
} | ||
return promise; | ||
return { | ||
pipe: pipeWrapper, | ||
readable: null, | ||
injectToStream | ||
}; | ||
} | ||
exports.renderToStream = renderToStream; | ||
async function renderToWebStream(element, disabled, options) { | ||
var _a; | ||
let didError = false; | ||
let firstErr = null; | ||
const onError = (err) => { | ||
didError = true; | ||
firstErr = firstErr || err; | ||
}; | ||
const readableOriginal = await ((_a = options.renderToReadableStream) !== null && _a !== void 0 ? _a : server_1.renderToReadableStream)(element, { onError }); | ||
if (didError) { | ||
throw firstErr; | ||
} | ||
if (disabled) { | ||
await readableOriginal.allReady; | ||
} | ||
if (didError) { | ||
throw firstErr; | ||
} | ||
const { readableWrapper, injectToStream } = (0, createReadableWrapper_1.createReadableWrapper)(readableOriginal, options); | ||
return { | ||
readable: readableWrapper, | ||
pipe: null, | ||
injectToStream | ||
}; | ||
} |
export { createBuffer }; | ||
declare function createBuffer(bufferParams: { | ||
debug: boolean; | ||
debug?: boolean; | ||
writeChunk: null | ((_chunk: string) => void); | ||
@@ -5,0 +5,0 @@ }): { |
@@ -6,4 +6,5 @@ /// <reference types="node" /> | ||
import type { Writable as WritableType } from 'stream'; | ||
declare function createPipeWrapper(pipeOriginal: Pipe, options?: { | ||
declare function createPipeWrapper(pipeOriginal: Pipe, { debug, onError }: { | ||
debug?: boolean; | ||
onError: (err: unknown) => void; | ||
}): { | ||
@@ -10,0 +11,0 @@ pipeWrapper: Pipe; |
@@ -5,6 +5,6 @@ "use strict"; | ||
const createBuffer_1 = require("./createBuffer"); | ||
function createPipeWrapper(pipeOriginal, options = {}) { | ||
function createPipeWrapper(pipeOriginal, { debug, onError }) { | ||
const pipeWrapper = createPipeWrapper(); | ||
const bufferParams = { | ||
debug: !!options.debug, | ||
debug, | ||
writeChunk: null | ||
@@ -26,2 +26,6 @@ }; | ||
callback(); | ||
}, | ||
// If we don't define `destroy()`, then Node.js will `process.exit()` | ||
destroy(err) { | ||
onError(err); | ||
} | ||
@@ -28,0 +32,0 @@ }); |
@@ -5,9 +5,4 @@ export { renderToStream }; | ||
import { Pipe } from './renderToStream/createPipeWrapper'; | ||
declare type Return = { | ||
pipe: null | Pipe; | ||
readable: null | ReadableStream; | ||
injectToStream: (chunk: string) => void; | ||
}; | ||
declare type SeoStrategy = 'conservative' | 'google-speed'; | ||
declare function renderToStream(element: React.ReactNode, options?: { | ||
import { SeoStrategy } from './renderToStream/resolveSeoStrategy'; | ||
declare type Options = { | ||
debug?: boolean; | ||
@@ -19,2 +14,8 @@ webStream?: boolean; | ||
renderToReadableStream?: typeof renderToReadableStream; | ||
}): Promise<Return>; | ||
}; | ||
declare type Result = { | ||
pipe: null | Pipe; | ||
readable: null | ReadableStream; | ||
injectToStream: (chunk: string) => void; | ||
}; | ||
declare function renderToStream(element: React.ReactNode, options?: Options): Promise<Result>; |
@@ -6,85 +6,85 @@ export { renderToStream }; | ||
import { StreamProvider } from './useStream'; | ||
import { assertWarning } from './utils'; | ||
import isBot from 'isbot-fast'; | ||
import { createPipeWrapper, nodeStreamModuleIsAvailable } from './renderToStream/createPipeWrapper'; | ||
import { createReadableWrapper } from './renderToStream/createReadableWrapper'; | ||
import { resolveSeoStrategy } from './renderToStream/resolveSeoStrategy'; | ||
async function renderToStream(element, options = {}) { | ||
var _a, _b, _c; | ||
let reject; | ||
let resolve; | ||
let resolved = false; | ||
const promise = new Promise((resolve_, reject_) => { | ||
resolve = () => { | ||
if (resolved) | ||
return; | ||
resolved = true; | ||
resolve_({ pipe, readable, injectToStream }); | ||
}; | ||
reject = (err) => { | ||
if (resolved) | ||
return; | ||
resolved = true; | ||
reject_(err); | ||
}; | ||
}); | ||
const seoStrategy = options.seoStrategy || 'conservative'; | ||
const disabled = (_a = options.disabled) !== null && _a !== void 0 ? _a : (() => { | ||
if (!options.userAgent) { | ||
assertWarning(false, 'Streaming disabled. Provide `options.userAgent` to enable streaming. (react-streaming needs the User Agent string in order to be able to disable streaming for bots, e.g. for Google Bot.) Or set `options.disabled` to `true` to get rid of this warning.'); | ||
return true; | ||
} | ||
// https://github.com/omrilotan/isbot | ||
// https://github.com/mahovich/isbot-fast | ||
// https://stackoverflow.com/questions/34647657/how-to-detect-web-crawlers-for-seo-using-express/68869738#68869738 | ||
if (!isBot(options.userAgent)) { | ||
return false; | ||
} | ||
const isGoogleBot = options.userAgent.toLowerCase().includes('googlebot'); | ||
if (seoStrategy === 'google-speed' && isGoogleBot) { | ||
return false; | ||
} | ||
return true; | ||
})(); | ||
// options.debug = true | ||
const webStream = (_b = options.webStream) !== null && _b !== void 0 ? _b : !nodeStreamModuleIsAvailable(); | ||
const onError = (err) => { | ||
reject(err); | ||
}; | ||
const streamUtils = { injectToStream: (chunk) => injectToStream(chunk) }; | ||
element = React.createElement(StreamProvider, { value: streamUtils }, element); | ||
var _a, _b; | ||
element = React.createElement(SsrDataProvider, null, element); | ||
let pipe = null; | ||
let readable = null; | ||
let injectToStream; | ||
element = React.createElement(StreamProvider, { value: { injectToStream: (chunk) => injectToStream(chunk) } }, element); | ||
const disabled = (_a = options.disabled) !== null && _a !== void 0 ? _a : resolveSeoStrategy(options).disableStream; | ||
const webStream = (_b = options.webStream) !== null && _b !== void 0 ? _b : !nodeStreamModuleIsAvailable(); | ||
if (!webStream) { | ||
const { pipe: pipeOriginal } = renderToPipeableStream(element, { | ||
onAllReady() { | ||
resolve(); | ||
}, | ||
onShellReady() { | ||
if (!disabled) { | ||
resolve(); | ||
} | ||
}, | ||
onShellError: onError, | ||
onError | ||
}); | ||
const { pipeWrapper, injectToStream: injectToStream_ } = createPipeWrapper(pipeOriginal, options); | ||
pipe = pipeWrapper; | ||
injectToStream = injectToStream_; | ||
pipe.injectToStream = injectToStream; | ||
const result = await renderToNodeStream(element, disabled, options); | ||
injectToStream = result.injectToStream; | ||
return result; | ||
} | ||
else { | ||
const readableOriginal = await ((_c = options.renderToReadableStream) !== null && _c !== void 0 ? _c : renderToReadableStream)(element, { | ||
onError | ||
}); | ||
if (disabled) { | ||
await readableOriginal.allReady; | ||
} | ||
const { readableWrapper, injectToStream: injectToStream_ } = createReadableWrapper(readableOriginal, options); | ||
readable = readableWrapper; | ||
injectToStream = injectToStream_; | ||
const result = await renderToWebStream(element, disabled, options); | ||
injectToStream = result.injectToStream; | ||
return result; | ||
} | ||
} | ||
async function renderToNodeStream(element, disabled, options) { | ||
let resolve; | ||
const promise = new Promise((r) => { | ||
resolve = () => r(); | ||
}); | ||
let didError = false; | ||
let firstErr = null; | ||
const onError = (err) => { | ||
didError = true; | ||
firstErr = firstErr || err; | ||
resolve(); | ||
}; | ||
const { pipe: pipeOriginal } = renderToPipeableStream(element, { | ||
onAllReady() { | ||
resolve(); | ||
}, | ||
onShellReady() { | ||
if (!disabled) { | ||
resolve(); | ||
} | ||
}, | ||
onError, | ||
onShellError: onError | ||
}); | ||
const { pipeWrapper, injectToStream } = createPipeWrapper(pipeOriginal, { | ||
debug: options.debug, | ||
onError | ||
}); | ||
await promise; | ||
if (didError) { | ||
throw firstErr; | ||
} | ||
return promise; | ||
return { | ||
pipe: pipeWrapper, | ||
readable: null, | ||
injectToStream | ||
}; | ||
} | ||
async function renderToWebStream(element, disabled, options) { | ||
var _a; | ||
let didError = false; | ||
let firstErr = null; | ||
const onError = (err) => { | ||
didError = true; | ||
firstErr = firstErr || err; | ||
}; | ||
const readableOriginal = await ((_a = options.renderToReadableStream) !== null && _a !== void 0 ? _a : renderToReadableStream)(element, { onError }); | ||
if (didError) { | ||
throw firstErr; | ||
} | ||
if (disabled) { | ||
await readableOriginal.allReady; | ||
} | ||
if (didError) { | ||
throw firstErr; | ||
} | ||
const { readableWrapper, injectToStream } = createReadableWrapper(readableOriginal, options); | ||
return { | ||
readable: readableWrapper, | ||
pipe: null, | ||
injectToStream | ||
}; | ||
} |
export { createBuffer }; | ||
declare function createBuffer(bufferParams: { | ||
debug: boolean; | ||
debug?: boolean; | ||
writeChunk: null | ((_chunk: string) => void); | ||
@@ -5,0 +5,0 @@ }): { |
@@ -6,4 +6,5 @@ /// <reference types="node" /> | ||
import type { Writable as WritableType } from 'stream'; | ||
declare function createPipeWrapper(pipeOriginal: Pipe, options?: { | ||
declare function createPipeWrapper(pipeOriginal: Pipe, { debug, onError }: { | ||
debug?: boolean; | ||
onError: (err: unknown) => void; | ||
}): { | ||
@@ -10,0 +11,0 @@ pipeWrapper: Pipe; |
export { createPipeWrapper }; | ||
export { nodeStreamModuleIsAvailable }; | ||
import { createBuffer } from './createBuffer'; | ||
function createPipeWrapper(pipeOriginal, options = {}) { | ||
function createPipeWrapper(pipeOriginal, { debug, onError }) { | ||
const pipeWrapper = createPipeWrapper(); | ||
const bufferParams = { | ||
debug: !!options.debug, | ||
debug, | ||
writeChunk: null | ||
@@ -24,2 +24,6 @@ }; | ||
callback(); | ||
}, | ||
// If we don't define `destroy()`, then Node.js will `process.exit()` | ||
destroy(err) { | ||
onError(err); | ||
} | ||
@@ -26,0 +30,0 @@ }); |
{ | ||
"name": "react-streaming", | ||
"description": "React 18 Streaming. Made fully-fledged & easy.", | ||
"version": "0.1.4", | ||
"version": "0.1.5", | ||
"main": "./dist/cjs/index.js", | ||
"dependencies": { | ||
"@brillout/json-s": "^0.4.5", | ||
"isbot-fast": "^1.2.0" | ||
"isbot-fast": "1.2.0" | ||
}, | ||
@@ -41,4 +41,4 @@ "scripts": { | ||
"@types/react-dom": "^18.0.1", | ||
"react": "^18.0.0", | ||
"react-dom": "^18.0.0", | ||
"react": "18.1.0", | ||
"react-dom": "18.1.0", | ||
"typescript": "^4.6.3", | ||
@@ -45,0 +45,0 @@ "vitest": "^0.9.4" |
@@ -0,4 +1,10 @@ | ||
<p align="center"> | ||
<a href="/../../#readme"> | ||
<img src="/images/logo.svg" height="145" alt="React Streaming"/> | ||
</a> | ||
</p> | ||
# `react-streaming` | ||
> React 18 Streaming. Made fully-fledged & easy. | ||
> React 18 Streaming. Full-fledged & Easy. | ||
@@ -18,4 +24,5 @@ <b>Contents</b> | ||
- Unlocks React libraries of tomorrow. (Such as using [Telefunc](https://telefunc.com/) for data fetching.) | ||
- Supports all platforms (Vercel, Cloudflare Workers, AWS, Netlify Edge, Deno, Deploy, ...). | ||
- Supports all platforms (Vercel, Cloudflare Workers, AWS, Netlify Edge, Deno Deploy, ...). | ||
- Two SEO strategies: `conservative` or `google-speed`. | ||
- Easy error handling. | ||
- **Bonus**: new `useAsync()` hook. | ||
@@ -32,3 +39,2 @@ | ||
import { renderToStream } from 'react-streaming/server' | ||
const { | ||
@@ -45,3 +51,3 @@ pipe, // Node.js (Vercel, AWS) | ||
- Data Fetching: | ||
- Use RPC to fetch data in a seamless way, e.g. with [Telefunc](https://telefunc.com) or [tRPC](https://github.com/trpc/trpc). (Data fetching SSR hooks will be a thing of the past: no more Next.js's `getServerSideProps()` nor [`vite-plugin-ssr`](https://vite-plugin-ssr.com/)'s `onBeforeRender()`.) | ||
- Use RPC to fetch data in a seamless way, e.g. with [Telefunc](https://telefunc.com). (Data fetching SSR hooks will be a thing of the past: no more Next.js's `getServerSideProps()` nor [`vite-plugin-ssr`](https://vite-plugin-ssr.com/)'s `onBeforeRender()`.) | ||
- Expect your GraphQL tools to significantly improve, both performance and DX. (Also expect new tools such as [Vilay](https://github.com/XiNiHa/vilay).) | ||
@@ -69,4 +75,2 @@ - Fundamentally improved mobile performance. (Mobile users can progressively load the page as data is fetched, before even a single line of JavaScript is loaded. Especially important for low-end and/or poorly-connected devices.) | ||
import { renderToStream } from 'react-streaming/server' | ||
// Cross-platform | ||
const { | ||
@@ -92,6 +96,6 @@ pipe, // Defined if running in Node.js, otherwise `null` | ||
- `options.disable?: boolean`: Disable streaming. | ||
> The component is still rendered to a stream, but the promise `const promise = await renderTo...` resolves only after the stream has finished. (This effectively disables streaming from a user perspective, while unlocking React 18 Streaming capabilities such as SSR `<Supsense>`.) | ||
> `<Page>` is still rendered to a stream, but the promise `const promise = renderToStream()` resolves only after the stream has finished. (This effectively disables streaming from a user perspective, while unlocking React 18 Streaming capabilities such as SSR `<Supsense>`.) | ||
- `options.seoStrategy?: 'conservative' | 'google-speed'` | ||
- `conservative` (default): Disable streaming if the HTTP request originates from a bot. (Ensuring the bot always sees the whole HTML.) | ||
- `conservative` (default): Disable streaming if the HTTP request originates from a bot. (Ensuring bots to always see the whole HTML.) | ||
- `google-speed`: Don't disable streaming for the Google Bot. | ||
@@ -111,3 +115,3 @@ - Pro: Google ranks your website higher because the initial HTTP response is faster. (To be researched.) | ||
const stream = await renderToStream(<Page />, { disable }) | ||
await renderToStream(<Page />, { disable }) | ||
``` | ||
@@ -118,2 +122,19 @@ | ||
### Error Handling | ||
If an error occurs at the beginning of the stream then `await renderToStream()` rejects with the error. | ||
```js | ||
try { | ||
await renderToStream(<Page />) | ||
// ✅ page shell succesfully rendered and is ready in the stream buffer. | ||
} catch(err) { | ||
// ❌ something went wrong while rendering the page shell. | ||
} | ||
``` | ||
Technically speaking, the page shell is composed of all components that are outside a `<Suspsense>` boundary. | ||
If an error occurs later in the stream (i.e. a suspense boundary fails), then React swallows the error on the server-side and retries to resolve the suspense boundary on the client-side. If the `<Suspsense>` fails again on the client-side, then the client-side throws the error. | ||
### Bonus: `useAsync()` | ||
@@ -182,7 +203,7 @@ | ||
const someAsyncFunc = async function () { | ||
return 'someData' | ||
const value = 'someData' | ||
return value | ||
} | ||
// `useSsrData()` suspends rendering until the promise returned by `someAsyncFunc()` resolves. | ||
const value = useSsrData(key, someAsyncFunc) | ||
// `value` is the value returned by `someAsyncFunc()`. | ||
assert(value === 'someData') | ||
@@ -189,0 +210,0 @@ } |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
52963
55
1128
265
Updatedisbot-fast@1.2.0