From 3038536c7525c0a8ba639c4f9ea6c9184ef605f4 Mon Sep 17 00:00:00 2001
From: yuche
Date: Tue, 24 Oct 2017 20:56:30 +0800
Subject: [PATCH] refactor: move nerv-server into packages
---
.eslintrc.js | 3 +-
packages/nerv-server/__tests__/render.js | 267 +++++++++++++++++++++++
packages/nerv-server/package.json | 6 +-
packages/nerv-server/rollup.config.js | 17 ++
packages/nerv-server/src/index.ts | 166 ++++++++++++++
packages/nerv-server/src/is.ts | 49 +++++
packages/nerv-server/src/utils.ts | 125 +++++++++++
7 files changed, 629 insertions(+), 4 deletions(-)
create mode 100644 packages/nerv-server/__tests__/render.js
create mode 100644 packages/nerv-server/rollup.config.js
create mode 100644 packages/nerv-server/src/index.ts
create mode 100644 packages/nerv-server/src/is.ts
create mode 100644 packages/nerv-server/src/utils.ts
diff --git a/.eslintrc.js b/.eslintrc.js
index 2182aeba..52c39c47 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -20,7 +20,8 @@ module.exports = {
afterEach: true,
afterAll: true,
expect: true,
- jasmine: true
+ jasmine: true,
+ jest: true
},
parser: 'babel-eslint'
}
diff --git a/packages/nerv-server/__tests__/render.js b/packages/nerv-server/__tests__/render.js
new file mode 100644
index 00000000..78dfea37
--- /dev/null
+++ b/packages/nerv-server/__tests__/render.js
@@ -0,0 +1,267 @@
+/** @jsx createElement */
+import { Component, createElement } from 'nervjs'
+import { renderToString } from '../src'
+const render = renderToString
+describe('render', () => {
+ describe('Basic JSX', () => {
+ it('should render JSX', () => {
+ const rendered = render(
bar
)
+ const expected = `bar
`
+
+ expect(rendered).toEqual(expected)
+ })
+
+ it('should omit falsey attributes', () => {
+ const rendered = render()
+ const expected = ``
+
+ expect(rendered).toEqual(expected)
+
+ expect(render()).toEqual(``)
+ })
+
+ it('should collapse collapsible attributes', () => {
+ const rendered = render()
+ const expected = ``
+
+ expect(rendered).toEqual(expected)
+ })
+
+ it('should encode entities', () => {
+ const rendered = render(&'}>{'"<>&'}
)
+ const expected = `"<>&
`
+
+ expect(rendered).toEqual(expected)
+ })
+
+ it('should self-close void elements', () => {
+ const rendered = render(
+
+
+
+
+ )
+ const expected = `
`
+
+ expect(rendered).toEqual(expected)
+ })
+
+ it('does not close void elements with closing tags', () => {
+ const rendered = render(
+
+ Hello World
+
+ )
+ const expected = ``
+
+ expect(rendered).toEqual(expected)
+ })
+
+ it('should serialize object styles', () => {
+ const rendered = render()
+ const expected = ``
+
+ expect(rendered).toEqual(expected)
+ })
+
+ it('should ignore empty object styles', () => {
+ const rendered = render()
+ const expected = ``
+
+ expect(rendered).toEqual(expected)
+ })
+
+ // FIX: SVG problem
+ // it('should render SVG elements', () => {
+ // const rendered = render((
+ //
+ // ))
+
+ // tslint:disable-next-line:max-line-length
+ // expect(rendered).toEqual(``)
+ // })
+ })
+
+ describe('Functional component', () => {
+ it('should render functional components', () => {
+ const Test = jest.fn(({ foo, children }) => (
+ {children}
+ ))
+ const rendered = render(content)
+ expect(rendered).toEqual(`content
`)
+ expect(Test).toBeCalled()
+ expect(Test).toBeCalledWith({ children: ['content'], foo: 'test' }, {})
+ })
+
+ it('should render functional components within JSX', () => {
+ const Test = jest.fn(({ foo, children }) => (
+ {children}
+ ))
+ const rendered = render(
+
+ )
+ expect(rendered).toEqual(
+ ``
+ )
+ expect(Test).toHaveBeenCalled()
+ expect(Test).toHaveBeenCalledWith(
+ {
+ foo: 1,
+ children: [asdf]
+ },
+ {}
+ )
+ })
+ })
+
+ describe('Classical Component', () => {
+ it('should render classical components', () => {
+ class Test extends Component {
+ render () {
+ const { foo, children } = this.props
+ return {children}
+ }
+ }
+
+ const spy = jest.spyOn(Test.prototype, 'render')
+ const widget = content
+ const rendered = render(widget)
+ expect(rendered).toEqual(`content
`)
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
+
+ it('should render classical components within JSX', () => {
+ class Test extends Component {
+ render () {
+ const { foo, children } = this.props
+ return {children}
+ }
+ }
+ const spy = jest.spyOn(Test.prototype, 'render')
+ const rendered = render(
+
+ )
+ expect(rendered).toEqual(
+ ``
+ )
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
+
+ it('should apply defaultProps', () => {
+ class Test extends Component {
+ render () {
+ return
+ }
+ }
+ Test.defaultProps = {
+ foo: 'default foo',
+ bar: 'default bar'
+ }
+
+ expect(render()).toEqual(
+ ''
+ )
+ expect(render()).toEqual(
+ ''
+ )
+ expect(render()).toEqual(
+ ''
+ )
+ })
+
+ it('should invoke componentWillMount', () => {
+ class Test extends Component {
+ // tslint:disable-next-line:no-empty
+ componentWillMount () {}
+ render () {
+ return
+ }
+ }
+ const spy = jest.spyOn(Test.prototype, 'componentWillMount')
+ render()
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
+
+ it('should pass context to direct child', () => {
+ const CONTEXT = { a: 'a' }
+ class Outer extends Component {
+ getChildContext () {
+ return CONTEXT
+ }
+ render () {
+ return (
+
+
+
+ )
+ }
+ }
+ const outerContextSpy = jest.spyOn(Outer.prototype, 'getChildContext')
+
+ class Inner extends Component {
+ render () {
+ return {this.context && this.context.a}
+ }
+ }
+
+ const rendered = render()
+
+ expect(rendered).toEqual(``)
+ expect(outerContextSpy).toHaveBeenCalledTimes(1)
+ })
+
+ it('should pass context to grandchildren', () => {
+ const CONTEXT = { a: 'a' }
+ class Outer extends Component {
+ getChildContext () {
+ return CONTEXT
+ }
+ render () {
+ return (
+
+
+
+ )
+ }
+ }
+ const outerContextSpy = jest.spyOn(Outer.prototype, 'getChildContext')
+
+ class Inner extends Component {
+ render () {
+ return (
+
+
+
+ )
+ }
+ }
+
+ class Child extends Component {
+ render () {
+ return {this.context && this.context.a}
+ }
+ }
+
+ const rendered = render()
+
+ expect(rendered).toEqual(``)
+ expect(outerContextSpy).toHaveBeenCalledTimes(1)
+ })
+ })
+})
diff --git a/packages/nerv-server/package.json b/packages/nerv-server/package.json
index 2f1b82d8..844a926e 100644
--- a/packages/nerv-server/package.json
+++ b/packages/nerv-server/package.json
@@ -4,9 +4,9 @@
"description": "Server-side rendering for Nerv.js",
"author": "yuche",
"license": "MIT",
- "main": "dist/nerv.ssr.js",
- "module": "dist/nerv.ssr.esm.js",
- "jsnext:main": "dist/nerv.ssr.esm.js",
+ "main": "dist/index.js",
+ "module": "dist/nerv.ssr.js",
+ "jsnext:main": "dist/nerv.ssr.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
diff --git a/packages/nerv-server/rollup.config.js b/packages/nerv-server/rollup.config.js
new file mode 100644
index 00000000..a5b21dcc
--- /dev/null
+++ b/packages/nerv-server/rollup.config.js
@@ -0,0 +1,17 @@
+const typescript = require('rollup-plugin-typescript2')
+const pkg = require('./package.json')
+
+module.exports = {
+ input: 'src/index.ts',
+ plugins: [typescript()],
+ output: [
+ {
+ format: 'cjs',
+ file: pkg.main
+ },
+ {
+ format: 'es',
+ file: pkg.module
+ }
+ ]
+}
diff --git a/packages/nerv-server/src/index.ts b/packages/nerv-server/src/index.ts
new file mode 100644
index 00000000..7bf93dcd
--- /dev/null
+++ b/packages/nerv-server/src/index.ts
@@ -0,0 +1,166 @@
+// tslint:disable-next-line:max-line-length
+import {
+ isVNode,
+ isVText,
+ isWidget,
+ isStateLess,
+ isString,
+ isNumber,
+ isFunction,
+ isNullOrUndef,
+ isArray,
+ isInvalid
+} from './is'
+import {
+ encodeEntities,
+ isVoidElements,
+ escapeText,
+ getCssPropertyName,
+ isUnitlessNumber,
+ assign
+} from './utils'
+
+const skipAttributes = {
+ ref: true,
+ key: true,
+ children: true
+}
+
+function hashToClassName (obj) {
+ const arr: string[] = []
+ for (const i in obj) {
+ if (obj[i]) {
+ arr.push(i)
+ }
+ }
+ return arr.join(' ')
+}
+
+function renderStylesToString (styles: string | object): string {
+ if (isString(styles)) {
+ return styles
+ } else {
+ let renderedString = ''
+ for (const styleName in styles) {
+ const value = styles[styleName]
+
+ if (isString(value)) {
+ renderedString += `${getCssPropertyName(styleName)}${value};`
+ } else if (isNumber(value)) {
+ renderedString += `${getCssPropertyName(
+ styleName
+ )}${value}${isUnitlessNumber[styleName] ? '' : 'px'};`
+ }
+ }
+ return renderedString
+ }
+}
+
+function renderVNodeToString (vnode, parent, context, firstChild) {
+ const { tagName, props, children } = vnode
+ if (isVText(vnode)) {
+ return encodeEntities(vnode.text)
+ } else if (isVNode(vnode)) {
+ let renderedString = `<${tagName}`
+ let html
+ if (!isNullOrUndef(props)) {
+ for (const prop in props) {
+ const value = props[prop]
+ if (skipAttributes[prop]) {
+ continue
+ }
+ if (prop === 'dangerouslySetInnerHTML') {
+ html = value.__html
+ } else if (prop === 'style') {
+ renderedString += ` style="${renderStylesToString(value)}"`
+ } else if (prop === 'class' || prop === 'className') {
+ renderedString += ` class="${isString(value) ? value : hashToClassName(value)}"`
+ } else if (prop === 'defaultValue') {
+ if (!props.value) {
+ renderedString += ` value="${escapeText(value)}"`
+ }
+ } else if (prop === 'defaultChecked') {
+ if (!props.checked) {
+ renderedString += ` checked="${value}"`
+ }
+ } else {
+ if (isString(value)) {
+ renderedString += ` ${prop}="${escapeText(value)}"`
+ } else if (isNumber(value)) {
+ renderedString += ` ${prop}="${value}"`
+ } else if (value === true) {
+ renderedString += ` ${prop}`
+ }
+ }
+ }
+ }
+ if (isVoidElements[tagName]) {
+ renderedString += `/>`
+ } else {
+ renderedString += `>`
+ if (!isInvalid(children)) {
+ if (isString(children)) {
+ renderedString += children === '' ? ' ' : escapeText(children)
+ } else if (isNumber(children)) {
+ renderedString += children + ''
+ } else if (isArray(children)) {
+ for (let i = 0, len = children.length; i < len; i++) {
+ const child = children[i]
+ if (isString(child)) {
+ renderedString += child === '' ? ' ' : escapeText(child)
+ } else if (isNumber(child)) {
+ renderedString += child
+ } else if (!isInvalid(child)) {
+ renderedString += renderVNodeToString(
+ child,
+ vnode,
+ context,
+ i === 0
+ )
+ }
+ }
+ } else {
+ renderedString += renderVNodeToString(children, vnode, context, true)
+ }
+ } else if (html) {
+ renderedString += html
+ }
+ if (!isVoidElements[tagName]) {
+ renderedString += `${tagName}>`
+ }
+ }
+ return renderedString
+ } else if (isWidget(vnode)) {
+ const { ComponentType: type } = vnode
+ const instance = new type(props, context)
+ instance._disable = true
+ if (isFunction(instance.getChildContext)) {
+ context = assign(assign({}, context), instance.getChildContext())
+ }
+ instance.context = context
+ if (isFunction(instance.componentWillMount)) {
+ instance.componentWillMount()
+ }
+ const nextVnode = instance.render(props, instance.state, context)
+
+ if (isInvalid(nextVnode)) {
+ return ''
+ }
+ return renderVNodeToString(nextVnode, vnode, context, true)
+ } else if (isStateLess(vnode)) {
+ const nextVnode = tagName(props, context)
+
+ if (isInvalid(nextVnode)) {
+ return ''
+ }
+ return renderVNodeToString(nextVnode, vnode, context, true)
+ }
+}
+
+export function renderToString (input: any): string {
+ return renderVNodeToString(input, {}, {}, true) as string
+}
+
+export function renderToStaticMarkup (input: any): string {
+ return renderVNodeToString(input, {}, {}, true) as string
+}
diff --git a/packages/nerv-server/src/is.ts b/packages/nerv-server/src/is.ts
new file mode 100644
index 00000000..022cb793
--- /dev/null
+++ b/packages/nerv-server/src/is.ts
@@ -0,0 +1,49 @@
+export function isVNode (node) {
+ return node && node.type === 'VirtualNode'
+}
+
+export function isVText (node) {
+ return node && node.type === 'VirtualText'
+}
+
+export function isWidget (node) {
+ return node && node.type === 'Widget'
+}
+
+export function isStateLess (node) {
+ return node && node.type === 'StateLess'
+}
+
+export function isNumber (arg): arg is number {
+ return typeof arg === 'number'
+}
+
+export function isString (arg): arg is string {
+ return typeof arg === 'string'
+}
+
+export function isFunction (arg): arg is Function {
+ return typeof arg === 'function'
+}
+
+export function isBoolean (arg): arg is true | false {
+ return arg === true || arg === false
+}
+
+export function isNullOrUndef (o: any): o is undefined | null {
+ return isUndefined(o) || isNull(o)
+}
+
+export function isUndefined (o: any): o is undefined {
+ return o === void 0
+}
+
+export function isNull (o: any): o is null {
+ return o === null
+}
+
+export function isInvalid (o: any): o is null | false | true | undefined {
+ return isNull(o) || o === false || o === true || isUndefined(o)
+}
+
+export const isArray = Array.isArray
diff --git a/packages/nerv-server/src/utils.ts b/packages/nerv-server/src/utils.ts
new file mode 100644
index 00000000..267416dc
--- /dev/null
+++ b/packages/nerv-server/src/utils.ts
@@ -0,0 +1,125 @@
+export function escapeText (text: string): string {
+ let result = ''
+ let escape = ''
+ let start = 0
+ let i
+ for (i = 0; i < text.length; i++) {
+ switch (text.charCodeAt(i)) {
+ case 34: // "
+ escape = '"'
+ break
+ case 39: // \
+ escape = '''
+ break
+ case 38: // &
+ escape = '&'
+ break
+ case 60: // <
+ escape = '<'
+ break
+ case 62: // >
+ escape = '>'
+ break
+ default:
+ continue
+ }
+ if (i > start) {
+ if (start) {
+ result += text.slice(start, i)
+ } else {
+ result = text.slice(start, i)
+ }
+ }
+ result += escape
+ start = i + 1
+ }
+ return result + text.slice(start, i)
+}
+
+const uppercasePattern = /[A-Z]/g
+
+const CssPropCache = {}
+
+export function getCssPropertyName (str): string {
+ if (CssPropCache.hasOwnProperty(str)) {
+ return CssPropCache[str]
+ }
+ return (CssPropCache[str] =
+ str.replace(uppercasePattern, '-$&').toLowerCase() + ':')
+}
+
+export const isVoidElements = {
+ 'area': true,
+ 'base': true,
+ 'br': true,
+ 'col': true,
+ 'command': true,
+ 'embed': true,
+ 'hr': true,
+ 'img': true,
+ 'input': true,
+ 'keygen': true,
+ 'link': true,
+ 'meta': true,
+ 'param': true,
+ 'source': true,
+ 'track': true,
+ 'wbr': true
+}
+
+/**
+ * CSS properties which accept numbers but are not in units of "px".
+ */
+export const isUnitlessNumber = {
+ animationIterationCount: true,
+ borderImageOutset: true,
+ borderImageSlice: true,
+ borderImageWidth: true,
+ boxFlex: true,
+ boxFlexGroup: true,
+ boxOrdinalGroup: true,
+ columnCount: true,
+ flex: true,
+ flexGrow: true,
+ flexPositive: true,
+ flexShrink: true,
+ flexNegative: true,
+ flexOrder: true,
+ gridRow: true,
+ gridColumn: true,
+ fontWeight: true,
+ lineClamp: true,
+ lineHeight: true,
+ opacity: true,
+ order: true,
+ orphans: true,
+ tabSize: true,
+ widows: true,
+ zIndex: true,
+ zoom: true,
+
+ // SVG-related properties
+ fillOpacity: true,
+ floodOpacity: true,
+ stopOpacity: true,
+ strokeDasharray: true,
+ strokeDashoffset: true,
+ strokeMiterlimit: true,
+ strokeOpacity: true,
+ strokeWidth: true
+}
+
+export function encodeEntities (text): string {
+ if (typeof text === 'boolean' || typeof text === 'number') {
+ return '' + text
+ }
+ return escapeText(text)
+}
+
+// TODO: use extend from nerv.js module
+export function assign (obj, props) {
+ for (const i in props) {
+ obj[i] = props[i]
+ }
+ return obj
+}