-
-
Notifications
You must be signed in to change notification settings - Fork 96
Introduce renderToNodeStream #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c380f59
c96b201
dc2b3c7
8db721b
d60794b
7f59e21
d8c9986
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,11 +8,12 @@ | |
"module": "dist/index.module.js", | ||
"jsnext:main": "dist/index.module.js", | ||
"scripts": { | ||
"build": "npm run -s transpile && npm run -s transpile:jsx && npm run -s copy-typescript-definition", | ||
"build": "npm run -s transpile && npm run -s transpile:jsx && npm run -s transpile:stream && npm run -s copy-typescript-definition", | ||
"transpile": "echo 'export const ENABLE_PRETTY = false;'>env.js && microbundle src/index.js -f es,umd --target web --external preact", | ||
"transpile:jsx": "echo 'export const ENABLE_PRETTY = true;'>env.js && microbundle src/jsx.js -o dist/jsx.js --target web --external none && microbundle dist/jsx.js -o dist/jsx.js -f cjs", | ||
"transpile:stream": "echo 'export const ENABLE_PRETTY = true;'>env.js && microbundle dist/stream.js --external preact,stream -o dist/stream.js -f cjs", | ||
"copy-typescript-definition": "copyfiles -f src/index.d.ts dist", | ||
"test": "eslint src test && mocha --compilers js:babel-register test/**/*.js", | ||
"test": "eslint src test && mocha --compilers js:@babel/register test/**/*.js", | ||
"prepublish": "npm run build", | ||
"release": "npm run build && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" | ||
}, | ||
|
@@ -40,16 +41,23 @@ | |
}, | ||
"babel": { | ||
"presets": [ | ||
"env" | ||
[ | ||
"@babel/env", | ||
{ | ||
"exclude": [ | ||
"@babel/plugin-transform-regenerator" | ||
] | ||
} | ||
] | ||
], | ||
"plugins": [ | ||
[ | ||
"transform-react-jsx", | ||
"@babel/transform-react-jsx", | ||
{ | ||
"pragma": "h" | ||
} | ||
], | ||
"transform-object-rest-spread" | ||
"@babel/proposal-object-rest-spread" | ||
] | ||
}, | ||
"author": "Jason Miller <[email protected]>", | ||
|
@@ -62,10 +70,11 @@ | |
"preact": ">=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0" | ||
}, | ||
"devDependencies": { | ||
"babel-plugin-transform-object-rest-spread": "^6.26.0", | ||
"babel-plugin-transform-react-jsx": "^6.24.1", | ||
"babel-preset-env": "^1.7.0", | ||
"babel-register": "^6.26.0", | ||
"@babel/core": "^7.4.4", | ||
"@babel/plugin-proposal-object-rest-spread": "^7.4.4", | ||
"@babel/plugin-transform-react-jsx": "^7.3.0", | ||
"@babel/preset-env": "^7.4.4", | ||
"@babel/register": "^7.4.4", | ||
"chai": "^3.5.0", | ||
"copyfiles": "^1.2.0", | ||
"eslint": "^4.19.1", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
import { encodeEntities, styleObjToCss, assign, getChildren } from './util'; | ||
import { options, Fragment } from 'preact'; | ||
import stream from 'stream'; | ||
|
||
// components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names. | ||
const UNNAMED = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to self: Seems like a memory leak when keeping references to components around in a global variable |
||
|
||
const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; | ||
|
||
|
||
function PreactReadableStream(vnode, context, opts) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's safe to use a class here. The current |
||
if (opts && opts.pretty) { | ||
throw new Error('pretty is not supported in renderToNodeStream!'); | ||
} | ||
|
||
stream.Readable.call(this, opts && opts.readable); | ||
|
||
this.vnode = vnode; | ||
this.context = context || {}; | ||
this.opts = opts || {}; | ||
} | ||
|
||
PreactReadableStream.prototype = new stream.Readable(); | ||
PreactReadableStream.prototype.constructor = PreactReadableStream; | ||
PreactReadableStream.prototype._read = function _read() { | ||
try { | ||
if (!this._generator) { | ||
this._generator = this._generate(this.vnode, this.context, this.opts); | ||
} | ||
else if (this.reading) { | ||
console.warn(new Error('You should not call PreactReadableStream#_read when a read is in progress').stack); | ||
} | ||
|
||
this.reading = true; | ||
|
||
for (const chunk of this._generator) { | ||
if (!this.push(chunk)) { | ||
this.reading = false; | ||
// high water mark reached, pause the stream until _read is called again... | ||
return; | ||
} | ||
} | ||
} | ||
catch (e) { | ||
this.emit('error', e); | ||
this.push(null); | ||
return; | ||
} | ||
|
||
// end the stream | ||
this.push(null); | ||
}; | ||
|
||
PreactReadableStream.prototype._generate = function *_generate(vnode, context, opts, inner, isSvgMode, selectValue) { | ||
if (vnode==null || typeof vnode==='boolean') { | ||
yield ''; | ||
return; | ||
} | ||
|
||
let nodeName = vnode.type, | ||
props = vnode.props, | ||
isComponent = false; | ||
context = context || {}; | ||
opts = opts || {}; | ||
|
||
// #text nodes | ||
if (typeof vnode!=='object' && !nodeName) { | ||
yield encodeEntities(vnode); | ||
return; | ||
} | ||
|
||
// components | ||
if (typeof nodeName==='function') { | ||
isComponent = true; | ||
if (opts.shallow && (inner || opts.renderRootComponent===false)) { | ||
nodeName = getComponentName(nodeName); | ||
} | ||
else if (nodeName===Fragment) { | ||
let children = []; | ||
getChildren(children, vnode.props.children); | ||
|
||
for (let i = 0; i < children.length; i++) { | ||
for (const chunk of this._generate(children[i], context, opts, opts.shallowHighOrder!==false, isSvgMode, selectValue)) { | ||
yield chunk; | ||
} | ||
} | ||
|
||
return; | ||
} | ||
else { | ||
let c = vnode.__c = { __v: vnode, context, props: vnode.props }; | ||
if (options.render) options.render(vnode); | ||
|
||
let renderedVNode; | ||
|
||
if (!nodeName.prototype || typeof nodeName.prototype.render!=='function') { | ||
// Necessary for createContext api. Setting this property will pass | ||
// the context value as `this.context` just for this component. | ||
let cxType = nodeName.contextType; | ||
let provider = cxType && context[cxType.__c]; | ||
let cctx = cxType != null ? (provider ? provider.props.value : cxType._defaultValue) : context; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hello! Looks like the use of |
||
|
||
// stateless functional components | ||
renderedVNode = nodeName.call(vnode.__c, props, cctx); | ||
} | ||
else { | ||
// class-based components | ||
// c = new nodeName(props, context); | ||
c = vnode.__c = new nodeName(props, context); | ||
c.__v = vnode; | ||
// turn off stateful re-rendering: | ||
c._dirty = c.__d = true; | ||
c.props = props; | ||
c.context = context; | ||
if (nodeName.getDerivedStateFromProps) c.state = assign(assign({}, c.state), nodeName.getDerivedStateFromProps(c.props, c.state)); | ||
else if (c.componentWillMount) c.componentWillMount(); | ||
|
||
renderedVNode = c.render(c.props, c.state || {}, c.context); | ||
} | ||
|
||
if (c.getChildContext) { | ||
context = assign(assign({}, context), c.getChildContext()); | ||
} | ||
|
||
for (const chunk of this._generate(renderedVNode, context, opts, opts.shallowHighOrder!==false, isSvgMode, selectValue)) { | ||
yield chunk; | ||
} | ||
|
||
return; | ||
} | ||
} | ||
|
||
// render JSX to HTML | ||
let s = '', html; | ||
|
||
if (props) { | ||
let attrs = Object.keys(props); | ||
|
||
// allow sorting lexicographically for more determinism (useful for tests, such as via preact-jsx-chai) | ||
if (opts && opts.sortAttributes===true) attrs.sort(); | ||
|
||
for (let i=0; i<attrs.length; i++) { | ||
let name = attrs[i], | ||
v = props[name]; | ||
if (name==='children') continue; | ||
|
||
if (name.match(/[\s\n\\/='"\0<>]/)) continue; | ||
|
||
if (!(opts && opts.allAttributes) && (name==='key' || name==='ref')) continue; | ||
|
||
if (name==='className') { | ||
if (props.class) continue; | ||
name = 'class'; | ||
} | ||
else if (isSvgMode && name.match(/^xlink:?./)) { | ||
name = name.toLowerCase().replace(/^xlink:?/, 'xlink:'); | ||
} | ||
|
||
if (name==='style' && v && typeof v==='object') { | ||
v = styleObjToCss(v); | ||
} | ||
|
||
let hooked = opts.attributeHook && opts.attributeHook(name, v, context, opts, isComponent); | ||
if (hooked || hooked==='') { | ||
s += hooked; | ||
continue; | ||
} | ||
|
||
if (name==='dangerouslySetInnerHTML') { | ||
html = v && v.__html; | ||
} | ||
else if ((v || v===0 || v==='') && typeof v!=='function') { | ||
if (v===true || v==='') { | ||
v = name; | ||
// in non-xml mode, allow boolean attributes | ||
if (!opts || !opts.xml) { | ||
s += ' ' + name; | ||
continue; | ||
} | ||
} | ||
|
||
if (name==='value') { | ||
if (nodeName==='select') { | ||
selectValue = v; | ||
continue; | ||
} | ||
else if (nodeName==='option' && selectValue==v) { | ||
s += ` selected`; | ||
} | ||
} | ||
s += ` ${name}="${encodeEntities(v)}"`; | ||
} | ||
} | ||
} | ||
|
||
let isVoid = String(nodeName).match(VOID_ELEMENTS); | ||
|
||
s = `<${nodeName}${s}`; | ||
if (String(nodeName).match(/[\s\n\\/='"\0<>]/)) throw s; | ||
|
||
yield s; | ||
|
||
if (html) { | ||
yield `>${html}</${nodeName}>`; | ||
return; | ||
} | ||
|
||
let didCloseOpeningTag = false; | ||
|
||
let children = []; | ||
if (props && getChildren(children, props.children).length) { | ||
for (let i=0; i<children.length; i++) { | ||
let child = children[i]; | ||
if (child!=null && child!==false) { | ||
let childSvgMode = nodeName==='svg' ? true : nodeName==='foreignObject' ? false : isSvgMode; | ||
|
||
for (const chunk of this._generate(child, context, opts, true, childSvgMode, selectValue)) { | ||
if (chunk) { | ||
if (!didCloseOpeningTag) { | ||
didCloseOpeningTag = true; | ||
yield '>'; | ||
} | ||
|
||
yield chunk; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
yield didCloseOpeningTag | ||
? `</${nodeName}>` | ||
: `${isVoid || opts.xml ? '/>' : `></${nodeName}>`}`; | ||
}; | ||
|
||
function getComponentName(component) { | ||
return component.displayName || component!==Function && component.name || getFallbackComponentName(component); | ||
} | ||
|
||
function getFallbackComponentName(component) { | ||
let str = Function.prototype.toString.call(component), | ||
name = (str.match(/^\s*function\s+([^( ]+)/) || '')[1]; | ||
if (!name) { | ||
// search for an existing indexed name for the given component: | ||
let index = -1; | ||
for (let i=UNNAMED.length; i--; ) { | ||
if (UNNAMED[i]===component) { | ||
index = i; | ||
break; | ||
} | ||
} | ||
// not found, create a new indexed name: | ||
if (index<0) { | ||
index = UNNAMED.push(component) - 1; | ||
} | ||
name = `UnnamedComponent${index}`; | ||
} | ||
return name; | ||
} | ||
|
||
|
||
export default function renderToNodeStream(vnode, context, opts) { | ||
return new PreactReadableStream(vnode, context, opts); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should probably set the target to the minimum node version here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well
renderToNodeStream
requires node 10. The other parts of this package work fine with 9 and more importantly in the browserThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a bit concerned about the async generator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get that point. The
renderToNodeStream
should only be used in node anyways, thus an async generator feels good to me :)I'd propose to disallow async generators in eslint as bundling regenerator would result in a massive bundle bloat. That will make sure we don't use it in modules targeted to browsers