@kitajs/html
Advanced tools
Comparing version 4.1.0 to 4.2.0
// No JS code is needed in this file. Its only here to allow direct import of alpine.js | ||
process.emitWarning( | ||
'The `@kitajs/html/alpine` import will be removed in the next major version. See https://github.com/kitajs/html/tree/master/packages/html#deprecating-importing-type-extensions', | ||
'DeprecationWarning', | ||
'KTHTML_DEP' | ||
); |
@@ -7,3 +7,6 @@ import type { Children } from '.'; | ||
/** An error thrown by the ErrorBoundary's `timeout` property. */ | ||
export class HtmlTimeout extends Error {} | ||
export class HtmlTimeout extends Error { | ||
/** Throws the error. */ | ||
static reject(): never; | ||
} | ||
@@ -34,2 +37,8 @@ /** | ||
timeout?: number; | ||
/** | ||
* The error class we should throw if the timeout gets triggered. Defaults to | ||
* {@linkcode HtmlTimeout} | ||
*/ | ||
error?: { reject(): never }; | ||
} |
@@ -18,3 +18,3 @@ const { contentToString } = require('./index'); | ||
children, | ||
setTimeout(props.timeout).then(HtmlTimeout.reject) | ||
setTimeout(props.timeout).then((props.error || HtmlTimeout).reject) | ||
]); | ||
@@ -34,4 +34,5 @@ } | ||
/** @type {import('./error-boundary').HtmlTimeout} */ | ||
class HtmlTimeout extends Error { | ||
/** @returns {string} */ | ||
/** @returns {never} */ | ||
static reject() { | ||
@@ -42,3 +43,3 @@ throw new HtmlTimeout('Children timed out.'); | ||
module.exports.ErrorBoundary = ErrorBoundary; | ||
module.exports.HtmlTimeout = HtmlTimeout; | ||
exports.ErrorBoundary = ErrorBoundary; | ||
exports.HtmlTimeout = HtmlTimeout; |
// No JS code is needed in this file. Its only here to allow direct import of hotwire-turbo.js | ||
process.emitWarning( | ||
'The `@kitajs/html/hotwire-turbo` import will be removed in the next major version. See https://github.com/kitajs/html/tree/master/packages/html#deprecating-importing-type-extensions', | ||
'DeprecationWarning', | ||
'KTHTML_DEP' | ||
); |
// No JS code is needed in this file. Its only here to allow direct import of htmx.js | ||
process.emitWarning( | ||
'The `@kitajs/html/htmx` import will be removed in the next major version. See https://github.com/kitajs/html/tree/master/packages/html#deprecating-importing-type-extensions', | ||
'DeprecationWarning', | ||
'KTHTML_DEP' | ||
); |
@@ -121,41 +121,2 @@ /// <reference path="./jsx.d.ts" /> | ||
/** | ||
* Compiles a **clean component** into a super fast component. This does not support | ||
* unclean components / props processing. | ||
* | ||
* A **clean component** is a component that does not process props before applying them | ||
* to the element. This means that the props are applied to the element as is, and you | ||
* need to process them before passing them to the component. | ||
* | ||
* @example | ||
* | ||
* ```tsx | ||
* // Clean component, render as is | ||
* function Clean(props: PropsWithChildren<{ repeated: string }>) { return <div>{props.repeated}</div> } | ||
* | ||
* // Calculation is done before passing to the component | ||
* html = <Clean name={'a'.repeat(5)} /> | ||
* | ||
* // Unclean component, process before render | ||
* function Unclean(props: { repeat: string; n: number }) { return <div>{props.repeat.repeat(props.n)}</div> } | ||
* | ||
* // Calculation is done inside the component, thus cannot be used with .compile() html = | ||
* <Unclean repeat="a" n={5} /> | ||
* ``` | ||
* | ||
* @param htmlComponent The _clean_ component to compile. | ||
* @param strict If it should throw an error when a property is not found. Default is | ||
* `true` | ||
* @param separator The string used to interpolate and separate parameters | ||
* @returns The compiled template function | ||
*/ | ||
export function compile< | ||
P extends { [K in keyof P]: K extends 'children' ? Children : string } | ||
>( | ||
this: void, | ||
cleanComponent: Component<P>, | ||
strict?: boolean, | ||
separator?: string | ||
): Component<P>; | ||
/** Here for interop with `preact` and many build systems. */ | ||
@@ -215,11 +176,1 @@ export const h: typeof createElement; | ||
export const Html: Omit<typeof import('.'), 'Html'>; | ||
/** | ||
* Fast and type safe HTML templates using JSX syntax. | ||
* | ||
* @module Html | ||
* @license Apache License Version 2.0 | ||
* @link https://github.com/kitajs/html | ||
* @link https://www.npmjs.com/package/@kitajs/html | ||
*/ | ||
export as namespace Html; |
188
index.js
@@ -389,6 +389,9 @@ /// <reference path="./jsx.d.ts" /> | ||
case 'number': | ||
case 'boolean': | ||
// Bigint is the only case where it differs from React. | ||
// where React renders a empty string and we render the whole number. | ||
case 'bigint': | ||
result += content; | ||
continue; | ||
case 'boolean': | ||
continue; | ||
} | ||
@@ -406,7 +409,11 @@ | ||
// @ts-ignore - Type instantiation is excessively deep and possibly infinite. | ||
return Promise.all(contents.slice(index)).then(function resolveContents(resolved) { | ||
resolved.unshift(result); | ||
return contentsToString(resolved, escape); | ||
}); | ||
if (typeof content.then === 'function') { | ||
// @ts-ignore - Type instantiation is excessively deep and possibly infinite. | ||
return Promise.all(contents.slice(index)).then(function resolveContents(resolved) { | ||
resolved.unshift(result); | ||
return contentsToString(resolved, escape); | ||
}); | ||
} | ||
throw new Error('Objects are not valid as a KitaJSX child'); | ||
} | ||
@@ -432,7 +439,9 @@ | ||
return safe ? escapeHtml(content) : content; | ||
case 'number': | ||
case 'boolean': | ||
// Bigint is the only case where it differs from React. | ||
// where React renders a empty string and we render the whole number. | ||
case 'bigint': | ||
return content.toString(); | ||
case 'boolean': | ||
return ''; | ||
} | ||
@@ -448,5 +457,9 @@ | ||
return content.then(function resolveContent(resolved) { | ||
return contentToString(resolved, safe); | ||
}); | ||
if (typeof content.then === 'function') { | ||
return content.then(function resolveContent(resolved) { | ||
return contentToString(resolved, safe); | ||
}); | ||
} | ||
throw new Error('Objects are not valid as a KitaJSX child'); | ||
} | ||
@@ -469,7 +482,6 @@ | ||
if (!hasAttrs) { | ||
attrs = { children: children.length > 1 ? children : children[0] }; | ||
} else if (attrs.children === undefined) { | ||
attrs.children = children.length > 1 ? children : children[0]; | ||
return name({ children: children.length > 1 ? children : children[0] }); | ||
} | ||
attrs.children = children.length > 1 ? children : children[0]; | ||
return name(attrs); | ||
@@ -480,3 +492,3 @@ } | ||
if (hasAttrs && name === 'tag') { | ||
name = String(attrs.of); | ||
name = /** @type {string} */ (attrs.of); | ||
} | ||
@@ -494,9 +506,9 @@ | ||
if (contents instanceof Promise) { | ||
return contents.then(function resolveContents(child) { | ||
return '<' + name + attributes + '>' + child + '</' + name + '>'; | ||
}); | ||
if (typeof contents === 'string') { | ||
return '<' + name + attributes + '>' + contents + '</' + name + '>'; | ||
} | ||
return '<' + name + attributes + '>' + contents + '</' + name + '>'; | ||
return contents.then(function resolveContents(contents) { | ||
return '<' + name + attributes + '>' + contents + '</' + name + '>'; | ||
}); | ||
} | ||
@@ -506,125 +518,17 @@ | ||
function Fragment(props) { | ||
return Html.contentsToString([props.children]); | ||
return contentsToString([props.children]); | ||
} | ||
/** | ||
* Just to stop TS from complaining about the type. | ||
* | ||
* @type {import('.').compile} | ||
* @returns {Function} | ||
*/ | ||
function compile(htmlFn, strict = true, separator = '/*\x00*/') { | ||
if (typeof htmlFn !== 'function') { | ||
throw new Error('The first argument must be a function.'); | ||
} | ||
const properties = new Set(); | ||
const html = htmlFn( | ||
// @ts-expect-error - this proxy will meet the props with children requirements. | ||
new Proxy( | ||
{}, | ||
{ | ||
get(_, name) { | ||
// Adds the property to the set of known properties. | ||
properties.add(name); | ||
const isChildren = name === 'children'; | ||
let access = `args[${separator}\`${name.toString()}\`${separator}]`; | ||
// Adds support to render multiple children | ||
if (isChildren) { | ||
access = `Array.isArray(${access}) ? ${access}.join(${separator}\`\`${separator}) : ${access}`; | ||
} | ||
// Uses ` to avoid content being escaped. | ||
return `\`${separator} + (${access} || ${ | ||
strict && !isChildren | ||
? `throwPropertyNotFound(${separator}\`${name.toString()}\`${separator})` | ||
: `${separator}\`\`${separator}` | ||
}) + ${separator}\``; | ||
} | ||
} | ||
) | ||
); | ||
if (typeof html !== 'string') { | ||
throw new Error('You cannot use compile() with async components.'); | ||
} | ||
const sepLength = separator.length; | ||
const length = html.length; | ||
// Adds the throwPropertyNotFound function if strict | ||
let body = ''; | ||
let nextStart = 0; | ||
let index = 0; | ||
// Escapes every ` without separator | ||
for (; index < length; index++) { | ||
// Escapes the backtick character because it will be used to wrap the string | ||
// in a template literal. | ||
if ( | ||
html[index] === '`' && | ||
html.slice(index - sepLength, index) !== separator && | ||
html.slice(index + 1, index + sepLength + 1) !== separator | ||
) { | ||
body += html.slice(nextStart, index) + '\\`'; | ||
nextStart = index + 1; | ||
} | ||
} | ||
// Adds the remaining string | ||
body += html.slice(nextStart); | ||
if (strict) { | ||
return Function( | ||
'args', | ||
// Checks for args presence | ||
'if (args === undefined) { throw new Error("The arguments object was not provided.") };\n' + | ||
// Function to throw when a property is not found | ||
'function throwPropertyNotFound(name) { throw new Error("Property " + name + " was not provided.") };\n' + | ||
// Concatenates the body | ||
`return \`${body}\`` | ||
); | ||
} | ||
return Function( | ||
'args', | ||
// Adds a empty args object when it is not present | ||
'if (args === undefined) { args = Object.create(null) };\n' + `return \`${body}\`` | ||
); | ||
} | ||
const Html = { | ||
escape, | ||
e: escape, | ||
escapeHtml, | ||
isVoidElement, | ||
attributesToString, | ||
toKebabCase, | ||
isUpper, | ||
styleToString, | ||
createElement, | ||
h: createElement, | ||
contentsToString, | ||
contentToString, | ||
compile, | ||
Fragment | ||
}; | ||
/** | ||
* These export configurations enable JS and TS developers to consumer @kitajs/html in | ||
* whatever way best suits their needs. Some examples of supported import syntax | ||
* includes: | ||
* | ||
* - `const Html = require('@kitajs/html')` | ||
* - `const { Html } = require('@kitajs/html')` | ||
* - `import * as Html from '@kitajs/html'` | ||
* - `import { Html, type ComponentWithChildren } from '@kitajs/html'` | ||
* - `import Html from '@kitajs/html'` | ||
* - `import Html, { type ComponentWithChildren } from '@kitajs/html'` | ||
*/ | ||
module.exports = Html; | ||
module.exports.Html = Html; | ||
module.exports.default = Html; | ||
exports.escape = escape; | ||
exports.e = escape; | ||
exports.escapeHtml = escapeHtml; | ||
exports.isVoidElement = isVoidElement; | ||
exports.attributesToString = attributesToString; | ||
exports.toKebabCase = toKebabCase; | ||
exports.isUpper = isUpper; | ||
exports.styleToString = styleToString; | ||
exports.createElement = createElement; | ||
exports.h = createElement; | ||
exports.contentsToString = contentsToString; | ||
exports.contentToString = contentToString; | ||
exports.Fragment = Fragment; |
/// <reference path="./jsx.d.ts" /> | ||
/// <reference types="./suspense.d.ts" /> | ||
/// <reference types="./error-boundary.d.ts" /> | ||
const { Fragment, jsx, jsxs } = require('./jsx-runtime'); | ||
const JsxRuntime = { | ||
jsxDEV: jsx, | ||
jsxs, | ||
Fragment | ||
}; | ||
module.exports = JsxRuntime; | ||
module.exports.default = JsxRuntime; | ||
exports.jsx = jsx; | ||
exports.jsxs = jsxs; | ||
exports.jsxDEV = jsx; | ||
exports.Fragment = Fragment; |
/// <reference path="./jsx.d.ts" /> | ||
/// <reference types="./suspense.d.ts" /> | ||
/// <reference types="./error-boundary.d.ts" /> | ||
@@ -20,3 +22,3 @@ const { | ||
if (name === 'tag') { | ||
name = String(attrs.of); | ||
name = /** @type {string} */ (attrs.of); | ||
} | ||
@@ -52,3 +54,3 @@ | ||
if (name === 'tag') { | ||
name = String(attrs.of); | ||
name = /** @type {string} */ (attrs.of); | ||
} | ||
@@ -75,11 +77,5 @@ | ||
const JsxRuntime = { | ||
jsx, | ||
jsxs, | ||
// According to the jsx-runtime spec we must export the fragment element also | ||
Fragment | ||
}; | ||
module.exports = JsxRuntime; | ||
module.exports.default = JsxRuntime; | ||
exports.jsx = jsx; | ||
exports.jsxs = jsxs; | ||
// According to the jsx-runtime spec we must export the fragment element also | ||
exports.Fragment = Fragment; |
98
jsx.d.ts
@@ -34,6 +34,18 @@ // This file is a result from many sources, including: RFCs, typescript dom lib, w3schools, and others. | ||
contenteditable?: undefined | string; | ||
inputmode?: | ||
| undefined | ||
| 'none' | ||
| 'text' | ||
| 'decimal' | ||
| 'numeric' | ||
| 'tel' | ||
| 'search' | ||
| 'email' | ||
| 'url' | ||
| AnyString; | ||
dir?: undefined | string; | ||
hidden?: undefined | string | boolean; | ||
id?: undefined | number | string; | ||
role?: undefined | string; | ||
popover?: undefined | boolean | 'auto' | 'manual'; | ||
role?: undefined | AriaRole; | ||
lang?: undefined | string; | ||
@@ -283,5 +295,5 @@ draggable?: undefined | string | boolean; | ||
height?: undefined | number | string; | ||
decoding?: 'sync' | 'async' | 'auto' | AnyString; | ||
loading?: 'eager' | 'lazy' | AnyString; | ||
srcset?: string; | ||
decoding?: undefined | 'sync' | 'async' | 'auto' | AnyString; | ||
loading?: undefined | 'eager' | 'lazy' | AnyString; | ||
srcset?: undefined | string; | ||
} | ||
@@ -464,3 +476,3 @@ | ||
interface HtmlSelectTag extends HtmlTag { | ||
interface HtmlSelectTag extends HtmlTag, FormEvents { | ||
autofocus?: undefined | boolean; | ||
@@ -473,2 +485,3 @@ disabled?: undefined | boolean; | ||
size?: undefined | string; | ||
autocomplete?: undefined | string; | ||
} | ||
@@ -660,3 +673,3 @@ | ||
*/ | ||
key?: never; | ||
key?: undefined | never; | ||
} | ||
@@ -847,1 +860,74 @@ | ||
} | ||
// All the WAI-ARIA 1.1 role attribute values from https://www.w3.org/TR/wai-aria-1.1/#role_definitions | ||
type AriaRole = | ||
| 'alert' | ||
| 'alertdialog' | ||
| 'application' | ||
| 'article' | ||
| 'banner' | ||
| 'button' | ||
| 'cell' | ||
| 'checkbox' | ||
| 'columnheader' | ||
| 'combobox' | ||
| 'complementary' | ||
| 'contentinfo' | ||
| 'definition' | ||
| 'dialog' | ||
| 'directory' | ||
| 'document' | ||
| 'feed' | ||
| 'figure' | ||
| 'form' | ||
| 'grid' | ||
| 'gridcell' | ||
| 'group' | ||
| 'heading' | ||
| 'img' | ||
| 'link' | ||
| 'list' | ||
| 'listbox' | ||
| 'listitem' | ||
| 'log' | ||
| 'main' | ||
| 'marquee' | ||
| 'math' | ||
| 'menu' | ||
| 'menubar' | ||
| 'menuitem' | ||
| 'menuitemcheckbox' | ||
| 'menuitemradio' | ||
| 'navigation' | ||
| 'none' | ||
| 'note' | ||
| 'option' | ||
| 'presentation' | ||
| 'progressbar' | ||
| 'radio' | ||
| 'radiogroup' | ||
| 'region' | ||
| 'row' | ||
| 'rowgroup' | ||
| 'rowheader' | ||
| 'scrollbar' | ||
| 'search' | ||
| 'searchbox' | ||
| 'separator' | ||
| 'slider' | ||
| 'spinbutton' | ||
| 'status' | ||
| 'switch' | ||
| 'tab' | ||
| 'table' | ||
| 'tablist' | ||
| 'tabpanel' | ||
| 'term' | ||
| 'textbox' | ||
| 'timer' | ||
| 'toolbar' | ||
| 'tooltip' | ||
| 'tree' | ||
| 'treegrid' | ||
| 'treeitem' | ||
| (string & {}); |
// No JS code is needed in this file. Its only here to allow direct import of jsx.js | ||
process.emitWarning( | ||
'The `@kitajs/html/jsx` import will be removed in the next major version. See https://github.com/kitajs/html/tree/master/packages/html#deprecating-importing-type-extensions', | ||
'DeprecationWarning', | ||
'KTHTML_DEP' | ||
); |
{ | ||
"name": "@kitajs/html", | ||
"version": "4.1.0", | ||
"version": "4.2.0", | ||
"description": "Fast and type safe HTML templates using TypeScript.", | ||
@@ -18,3 +18,15 @@ "homepage": "https://github.com/kitajs/html/tree/master/packages/html#readme", | ||
], | ||
"type": "commonjs", | ||
"exports": { | ||
".": "./index.js", | ||
"./jsx-runtime": "./jsx-runtime.js", | ||
"./jsx-dev-runtime": "./jsx-dev-runtime.js", | ||
"./package.json": "./package.json", | ||
"./suspense": "./suspense.js", | ||
"./register": "./register.js", | ||
"./error-boundary": "./error-boundary.js", | ||
"./*": "./*" | ||
}, | ||
"main": "index.js", | ||
"module": "index.js", | ||
"types": "index.d.ts", | ||
@@ -25,8 +37,9 @@ "dependencies": { | ||
"devDependencies": { | ||
"@types/jsdom": "^21.1.6", | ||
"@types/node": "^20.12.2", | ||
"c8": "^9.1.0", | ||
"jsdom": "^24.0.0", | ||
"tslib": "^2.6.2", | ||
"typescript": "^5.4.3" | ||
"@types/jsdom": "^21.1.7", | ||
"@types/node": "^20.14.11", | ||
"c8": "^10.1.2", | ||
"jsdom": "^24.1.1", | ||
"react": "^18.3.1", | ||
"react-dom": "^18.3.1", | ||
"typescript": "^5.5.4" | ||
}, | ||
@@ -33,0 +46,0 @@ "engines": { |
121
README.md
@@ -61,4 +61,2 @@ <p align="center"> | ||
- [Base HTML templates](#base-html-templates) | ||
- [Compiling HTML](#compiling-html) | ||
- [Clean Components](#clean-components) | ||
- [Fragments](#fragments) | ||
@@ -75,2 +73,3 @@ - [Supported HTML](#supported-html) | ||
- [Deprecating global register](#deprecating-global-register) | ||
- [Deprecating importing type extensions](#deprecating-importing-type-extensions) | ||
- [Fork credits](#fork-credits) | ||
@@ -748,89 +747,2 @@ | ||
## Compiling HTML | ||
`Html.compile` interface compiles a [clean component](#clean-components) into a super fast | ||
component. This does not support unclean components / props processing. | ||
<br /> | ||
> [!WARNING] | ||
> This feature is a special use case for rendering **entire page templates** like what you | ||
> would do with handlebars or nunjucks. | ||
> | ||
> It does not works with mostly JSX components and, for small components, | ||
> [it will be slower than the normal](benchmark.md) JSX syntax. | ||
<br /> | ||
This mode works just like prepared statements in SQL. Compiled components can give up to | ||
[**2000**](#performance) times faster html generation. This is a opt-in feature that you | ||
may not be able to use everywhere! | ||
Due to the nature of | ||
[`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) | ||
objects, the spread operator (`...`) will not work with compiled components. You need to | ||
manually pass all props to their components. | ||
```tsx | ||
import Html from '@kitajs/html'; | ||
function Component(props: Html.PropsWithChildren<{ name: string }>) { | ||
return <div>Hello {props.name}</div>; | ||
} | ||
const compiled = Html.compile<typeof Component>(Component); | ||
compiled({ name: 'World' }); | ||
// <div>Hello World</div> | ||
const compiled = Html.compile((p) => <div>Hello {p.name}</div>); | ||
compiled({ name: 'World' }); | ||
// <div>Hello World</div> | ||
``` | ||
Properties passed for compiled components **ARE NOT** what will be passed as argument to | ||
the generated function. | ||
```tsx | ||
const compiled = Html.compile((t) => { | ||
// THIS WILL NOT print 123, but a string used by .compile instead | ||
console.log(t.asd); | ||
return <div></div>; | ||
}); | ||
compiled({ asd: 123 }); | ||
``` | ||
That's the reason on why you cannot compile unclean components, as they need to process | ||
the props before rendering. | ||
<br /> | ||
### Clean Components | ||
A **clean component** is a component that does not process props before applying them to | ||
the element. This means that the props are applied to the element as is, and you need to | ||
process them before passing them to the component. | ||
```tsx | ||
// Clean component, render as is | ||
function Clean(props: PropsWithChildren<{ sum: number }>) { | ||
return <div>{props.sum}</div>; | ||
} | ||
// Calculation is done before passing to the component | ||
html = <Clean sum={3 * 2} />; | ||
// Unclean component, process before render | ||
function Unclean(props: { a: number; b: number }) { | ||
return <div>{props.a * props.b}</div>; | ||
} | ||
// Calculation is done inside the component, thus cannot be used with .compile() | ||
html = <Unclean a={3} b={2} />; | ||
``` | ||
<br /> | ||
## Fragments | ||
@@ -1114,4 +1026,4 @@ | ||
The `@kitajs/html/register` import has been deprecated and will be removed in the next | ||
major version. Please change the way you have configured your project to use this library. | ||
The `@kitajs/html/register` in favour of the `react-jsx` target `@kitajs/html` supports, | ||
which automatically registers the JSX runtime globally. | ||
@@ -1143,2 +1055,29 @@ Please update your tsconfig to use the new `jsxImportSource` option and remove all | ||
## Deprecating importing type extensions | ||
Importing type extensions like `import '@kitajs/html/htmx'` and | ||
`import '@kitajs/html/alpine'` have been deprecated and will be removed in the next major | ||
version. | ||
Please change the way you import them to either use `/// <reference types="..." />` | ||
[triple slash directive](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) | ||
or the [`types`](https://www.typescriptlang.org/tsconfig/#types) option in your tsconfig. | ||
```diff | ||
- import '@kitajs/html/htmx'; | ||
+ /// <reference types="@kitajs/html/htmx" /> | ||
``` | ||
**Or** add them in the `types` option present in your tsconfig: | ||
```diff | ||
{ | ||
"compilerOptions": { | ||
+ "types": ["@kitajs/html/htmx"] | ||
} | ||
} | ||
``` | ||
<br /> | ||
## Fork credits | ||
@@ -1145,0 +1084,0 @@ |
@@ -60,4 +60,4 @@ import type { Readable } from 'node:stream'; | ||
* **Warning**: Using `Suspense` without any type of runtime support will _**LEAK | ||
* memory**_. Always use `renderToStream`, `renderToString` or within a specific package | ||
* like `@kitajs/fastify-html-plugin` | ||
* memory**_ and not work. Always use with `renderToStream` or within a framework that | ||
* supports it. | ||
*/ | ||
@@ -73,2 +73,4 @@ export function Suspense(props: SuspenseProps): JSX.Element; | ||
* ```tsx | ||
* import { text} from 'node:stream/consumers'; | ||
* | ||
* // Prints out the rendered stream (2nd example shows with a custom id) | ||
@@ -78,5 +80,9 @@ * const stream = renderToStream(r => <AppWithSuspense rid={r} />) | ||
* | ||
* // You can consume it as a stream | ||
* for await (const html of stream) { | ||
* console.log(html.toString()) | ||
* } | ||
* | ||
* // Or join it all together (Wastes ALL Suspense benefits, but useful for testing) | ||
* console.log(await text(stream)) | ||
* ``` | ||
@@ -95,32 +101,28 @@ * | ||
/** | ||
* Returns a promise that resolves to the entire HTML generated by the component tree. | ||
* **Suspense calls are waited for**. | ||
* Joins the html base template (with possible suspense's fallbacks) with the request data | ||
* and returns the final Readable to be piped into the response stream. | ||
* | ||
* This method is a shorthand to call {@linkcode renderToStream} and collect its result | ||
* into a string. | ||
* **This API is meant to be used by library authors and should not be used directly.** | ||
* | ||
* **Rendering to string will not give any benefits over streaming, it will only be | ||
* slower.** | ||
* | ||
* @example | ||
* | ||
* ```tsx | ||
* // Does not uses suspense benefits! Useful for testing. Prefer to | ||
* // use renderToStream instead. (2nd example shows with a custom id) | ||
* const html = await renderToString(r => <AppWithSuspense rid={r} />) | ||
* const html = await renderToString(<AppWithSuspense rid={myCustomId} />, myCustomId) | ||
* const html = <RootLayout rid={rid} /> | ||
* const requestData = SUSPENSE_ROOT.requests.get(rid); | ||
* | ||
* console.log(html); | ||
* if(!requestData) { | ||
* return html; | ||
* } | ||
* | ||
* // This prepends the html into the stream, handling possible | ||
* // cases where the html resolved after one of its async children | ||
* return writeFallback(html, requestData.stream); | ||
* ``` | ||
* | ||
* @param html The component tree to render or a function that returns the component tree. | ||
* @param rid The request id to identify the request, if not provided, a new incrementing | ||
* id will be used. | ||
* @returns A promise that resolves to the entire HTML generated by the component tree. | ||
* @param fallback The fallback to render while the async children are loading. | ||
* @param stream The stream to write the fallback into. | ||
* @returns The same stream or another one with the fallback prepended. | ||
* @see {@linkcode renderToStream} | ||
*/ | ||
export function renderToString( | ||
html: JSX.Element | ((rid: number | string) => JSX.Element), | ||
rid?: number | string | ||
): Promise<string>; | ||
export function resolveHtmlStream(template: JSX.Element, data: RequestData): Readable; | ||
@@ -133,3 +135,3 @@ /** | ||
*/ | ||
export const SuspenseScript: string; | ||
export declare const SuspenseScript: string; | ||
@@ -136,0 +138,0 @@ /** |
134
suspense.js
const { contentsToString, contentToString } = require('./index'); | ||
const { Readable } = require('node:stream'); | ||
const { Readable, PassThrough } = require('node:stream'); | ||
@@ -116,3 +116,3 @@ // Avoids double initialization in case this file is not cached by | ||
children | ||
void children | ||
.then(writeStreamTemplate) | ||
@@ -144,8 +144,3 @@ .catch(function errorRecover(error) { | ||
.catch(function writeFatalError(error) { | ||
// stream.emit returns true if there's a listener | ||
// Nothing else to do if no catch or listener was found | ||
/* c8 ignore next 3 */ | ||
if (data?.stream.emit('error', error) === false) { | ||
console.error(error); | ||
} | ||
data.stream.emit('error', error); | ||
}) | ||
@@ -156,12 +151,12 @@ .finally(function clearRequestData() { | ||
data.running -= 1; | ||
return; | ||
} | ||
// Last suspense component, runs cleanup | ||
} else { | ||
if (data && !data.stream.closed) { | ||
data.stream.push(null); | ||
} | ||
// Last suspense component, runs cleanup | ||
if (data && !data.stream.closed) { | ||
data.stream.push(null); | ||
} | ||
// Removes the current state | ||
SUSPENSE_ROOT.requests.delete(props.rid); | ||
} | ||
// Removes the current state | ||
SUSPENSE_ROOT.requests.delete(props.rid); | ||
}); | ||
@@ -175,7 +170,7 @@ | ||
if (typeof fallback === 'string') { | ||
return `<div id="B:${run}" data-sf>${fallback}</div>`; | ||
return '<div id="B:' + run + '" data-sf>' + fallback + '</div>'; | ||
} | ||
return fallback.then(function resolveCallback(resolved) { | ||
return `<div id="B:${run}" data-sf>${resolved}</div>`; | ||
return '<div id="B:' + run + '" data-sf>' + resolved + '</div>'; | ||
}); | ||
@@ -221,4 +216,13 @@ | ||
} else if (SUSPENSE_ROOT.requests.has(rid)) { | ||
// Ensures the request id is unique | ||
throw new Error(`The provided Request Id is already in use: ${rid}.`); | ||
// Ensures the request id is unique within the current request | ||
// error here to keep original stack trace | ||
const error = new Error(`The provided Request Id is already in use: ${rid}.`); | ||
// returns errored stream to avoid throws | ||
return new Readable({ | ||
read() { | ||
this.emit('error', error); | ||
this.push(null); | ||
} | ||
}); | ||
} | ||
@@ -232,3 +236,10 @@ | ||
SUSPENSE_ROOT.requests.delete(rid); | ||
throw error; | ||
// returns errored stream to avoid throws | ||
return new Readable({ | ||
read() { | ||
this.emit('error', error); | ||
this.push(null); | ||
} | ||
}); | ||
} | ||
@@ -246,55 +257,46 @@ } | ||
const readable = new Readable({ read: noop }); | ||
html.then( | ||
(result) => { | ||
readable.push(result); | ||
readable.push(null); // self closes | ||
}, | ||
(error) => { | ||
// stream.emit returns true if there's a listener | ||
// Nothing else to do if no catch or listener was found | ||
/* c8 ignore next 3 */ | ||
if (readable.emit('error', error) === false) { | ||
console.error(error); | ||
} | ||
return new Readable({ | ||
read() { | ||
void html | ||
.then((result) => { | ||
this.push(result); | ||
this.push(null); | ||
}) | ||
.catch((error) => { | ||
this.emit('error', error); | ||
}); | ||
} | ||
); | ||
return readable; | ||
}); | ||
} | ||
if (typeof html === 'string') { | ||
requestData.stream.push(html); | ||
} else { | ||
html.then( | ||
(html) => requestData.stream.push(html), | ||
(error) => { | ||
/* c8 ignore next 6 */ | ||
// stream.emit returns true if there's a listener | ||
// Nothing else to do if no catch or listener was found | ||
if (requestData.stream.emit('error', error) === false) { | ||
console.error(error); | ||
} | ||
} | ||
); | ||
return resolveHtmlStream(html, requestData); | ||
} | ||
/** @type {import('./suspense').resolveHtmlStream} */ | ||
function resolveHtmlStream(template, requestData) { | ||
// Impossible to sync templates have their | ||
// streams being written (sent = true) before the fallback | ||
if (typeof template === 'string') { | ||
requestData.stream.push(template); | ||
return requestData.stream; | ||
} | ||
return requestData.stream; | ||
} | ||
const prepended = new PassThrough(); | ||
/** @type {import('./suspense').renderToString} */ | ||
async function renderToString(factory, rid) { | ||
const chunks = []; | ||
void template.then( | ||
(result) => { | ||
prepended.push(result); | ||
requestData.stream.pipe(prepended); | ||
}, | ||
(error) => { | ||
prepended.emit('error', error); | ||
} | ||
); | ||
for await (const chunk of renderToStream(factory, rid)) { | ||
chunks.push(chunk); | ||
} | ||
return chunks.join(''); | ||
return prepended; | ||
} | ||
module.exports.Suspense = Suspense; | ||
module.exports.renderToStream = renderToStream; | ||
module.exports.renderToString = renderToString; | ||
module.exports.SuspenseScript = SuspenseScript; | ||
exports.Suspense = Suspense; | ||
exports.renderToStream = renderToStream; | ||
exports.resolveHtmlStream = resolveHtmlStream; | ||
exports.SuspenseScript = SuspenseScript; |
Sorry, the diff of this file is not supported yet
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
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
0
122809
7
2808
1091