fusion-core-beta
v0.2.4-alpha1
Published
Beta release of fusion-core. See fusion-core for official release.
Downloads
3
Readme
fusion-core
Guides
Core concepts
fusion-core
The fusion-core
package provides a generic entry point class for FusionJS applications that is used by the FusionJS runtime.
If you're using React, you should use the fusion-react
package instead.
This package also exposes utilities for developing plugins.
Example
// main.js
import React from 'react';
import {render} from 'react-dom';
import {renderToString} from 'react-dom/server';
import App from 'fusion-core';
const Hello = () => <div>Hello</div>;
const render = el => __NODE__ ? renderToString(el) : render(el, document.getElementById('root'));
export default function() {
return new App(<Hello />, render);
}
API
import App from 'fusion-core';
const app = new App(element, render);
element: T
- Required. The root element of the application. Typically this should be a React/Preact elementrender: T => string|undefined
- A function that can renderelement
Creates an application that can be registered into the Fusion server.
The application is responsible for rendering (both virtual dom and server-side rendering)
An application can receive any number of plugins, which can augment the behavior of the application.
Typically a plugin works the same way as a Koa middleware.
Instance members
app.plugin
const plugin = app.plugin(factory, dependencies);
factory: (dependencies: Object) => Plugin
- Required. The function that is exported by a plugin packagedependencies: Object
- Optional. A map of dependencies and configuration for the pluginplugin
- a plugin
Call this method to register a plugin into a FusionJS application. Returns a plugin, which can be passed as a dependency to other plugins.
Plugin
There are two types of plugins: middleware plugins and service plugins.
When writing a plugin you should always export a function that returns either a middleware or a instance of the Plugin
class.
Middleware plugins
A middleware plugin is a Koa middleware, a function that takes two argument: a ctx
object that has some FusionJS-specific properties, and a next
callback function.
const middleware = (ctx, next) => {
return next();
}
In FusionJS, the next()
call represents the time when virtual dom rendering happens. Typically, you'll want to run all your logic before that, and simply have a return next()
statement at the end of the function. Even in cases where virtual DOM rendering is not applicable, this pattern is still the simplest way to write a middleware.
In a few more advanced cases, however, you might want to do things after virtual dom rendering. In that case, you can call await next()
instead:
export default () => __NODE__ && async (ctx, next) => {
// this happens before virtual dom rendering
const start = new Date();
await next();
// this happens after virtual rendeing, but before the response is sent to the browser
console.log('timing: ', new Date() - start);
}
Services
Often we want to encapsulate some functionality into a single coherent package that exposes a programmatic API that can be consumed by others.
In FusionJS, any class can be a service.
import {Plugin} from 'fusion-core';
export default () => {
return new Plugin({
Service: class SomeService {
/* ... */
},
});
}
Singleton services
In some cases, it's desirable to enforce that only a single instance of a service exists in an application. To do this, simply use the SingletonPlugin
instead of the Plugin
class:
import {SingletonPlugin} from 'fusion-core';
export default () => {
return new SingletonPlugin({
Service: class {
constructor() {
console.log('only gets instantiated once');
}
},
})
}
The singleton service instance can be acquired using the .of
method. Calling .of(ctx)
from a middleware returns the same instance for all requests.
const Thing = app.plugin(MyThing);
const instance = Thing.of();
Examples
Implementing HTTP endpoints
A plugin can be used to implement a RESTful HTTP endpoint. To achieve this, simply run code conditionally based on the url of the request
export default () => async (ctx, next) => {
if (ctx.method === 'GET' && ctx.path === '/api/v1/users') {
ctx.body = await getUsers();
}
return next();
}
Serialization and hydration
A plugin can be atomically responsible for serialization/deserialization of data from the server to the client.
The example below shows a plugin that grabs the project version from package.json and logs it in the browser:
// plugins/version-plugin.js
import util from 'util';
import fs from 'fs';
import {html} from 'fusion-core'; // html sanitization
export default () => {
if (__NODE__) {
const read = util.promisify(fs.readFile);
return async (ctx, next) => {
const data = read('package.json');
const {version} = JSON.parse(data);
ctx.body.head.push(html`<meta id="app-version" content="${version}">`);
return next();
}
}
else {
return async (ctx, next) => {
const version = document.getElementById('app-version').content;
console.log(`Version: ${version}`);
return next();
}
}
}
We can then consume the plugin like this:
// main.js
import React from 'react';
import App from 'fusion-core';
import VersionPlugin from './plugins/version-plugin';
const root = <div>Hello world</div>;
const render = el => __NODE__ ? renderToString(el) : render(el, document.getElementById('root'));
export default function() {
const app = new App(root, render);
app.plugin(VersionPlugin);
return app;
}
API
Middleware plugins
export default () => (ctx, next) => {
return next()
}
ctx: {element: T}
- An object with a property calledelement
. Plugins can compose element in order to add providers into a React tree, for example:ctx.element = <SomeProvider>{ctx.element}</SomeProvider>
The
ctx
object also exposes properties that server-specific properties in the server. See contextnext: () => Promise
- Every plugin must callawait next()
(ornext().then(...)
) exactly once. Code before this call happens before virtual dom rendering, and code after it runs after. In the server, flushing the response to the client happens after
Service plugins
import {Plugin} from 'fusion-core';
export default () => {
const plugin = new Plugin({Service, middleware});
return plugin;
}
Service: class
- Optional. A class that provides a programmatic APImiddleware: (ctx: Object, next: () => Promise) => Promise
- Optional. A Koa middlewareplugin: {Service, middleware, of}
Service: class
middleware: (ctx: Object, next: () => Promise) => Promise
of: (ctx: Object|null|undefined) => Service
- returns an instance of the service, memoized usingctx
as the memoization key.
Context
Middlewares receive a ctx
object as their first argument. This object has a property called element
in both server and client.
ctx: Object
element: Object
In the server, ctx
also exposes the same properties as a Koa context
ctx: Object
header: Object
- alias ofctx.headers
headers: Object
- map of parsed HTTP headersmethod: string
- HTTP methodurl: string
- request URLoriginalUrl: string
- same asurl
, except thaturl
may be modified (e.g. for url rewriting)path: string
- request pathnamequery: Object
- parsed querystring as an objectquerystring: string
- querystring without?
host: string
- host and porthostname: string
origin: string
- request origin, including protocol and hosthref: string
- full URL including protocol, host and urlfresh: boolean
- check for cache negotiationstale: boolean
- inverse offresh
socket: Socket
- request socketprotocol: string
secure: boolean
ip: string
- remote IP addressips: Array<string>
- proxy IPssubdomains: Array<string>
is: (...types: ...string) => boolean
- response type checkaccepts: (...types: ...string) => boolean
- request MIME type checkacceptsEncoding: (...encodings: ...string) => boolean
acceptsCharset: (...charsets: ...string) => boolean
acceptsLanguage: (...languages: ...string) => boolean
get: (name: String) => string
- returns a headerreq: http.IncomingMessage
- Node'srequest
objectres: Response
- Node'sresponse
objectrequest: Request
- Koa'srequest
objectresponse: Response
- Koa'sresponse
objectstate: Object
- A state bag for Koa middlewaresapp: Object
- a reference to the Koa instancecookies: {get, set}
get: (name: string, options: ?Object) => string
- get a cookiename: string
options: {signed: boolean}
set: (name: string, value: string, options: ?Object)
name: string
value: string
options: Object
- OptionalmaxAge: number
- a number representing the milliseconds from Date.now() for expirysigned: boolean
- sign the cookie valueexpires: Date
- a Date for cookie expirationpath: string
- cookie path, /' by defaultdomain: string
- cookie domainsecure: boolean
- secure cookiehttpOnly: boolean
- server-accessible cookie, true by defaultoverwrite: boolean
- a boolean indicating whether to overwrite previously set cookies of the same name (false by default). If this is true, all cookies set during the same request with the same name (regardless of path or domain) are filtered out of the Set-Cookie header when setting this cookie.
throw: (status: number, message: ?string, properties: ?Object) => void
- throws an errorstatus: number
- HTTP status codemessage: string
- error messageproperties: Object
- is merged to the error object
assert: (value: any, status: ?number, message: ?string, properties)
- throws if value is falsyvalue: any
status: number
- HTTP status codemessage: string
- error messageproperties: Object
- is merged to the error object
respond: boolean
- set to true to bypass Koa's built-in response handling. You should not use this flag.
Additionally, when server-side rendering a page, FusionJS sets ctx.body
to an object with the following properties:
ctx: Object
body: Object
htmlAttrs: Object
- attributes for the<html>
tag. For example{lang: 'en-US'}
turns into<html lang="en-US">
. Default: empty objecttitle: string
- The content for the<title>
tag. Default: empty stringhead: Array
- A list of sanitized HTML strings. Default: empty arraybody: Array
- A list of sanitized HTML strings. Default: empty array
When a request does not require a server-side render, ctx.body
follows regular Koa semantics.
HTML sanitization
Default-on HTML sanitization is important for preventing security threats such as XSS attacks.
Fusion automatically sanitizes htmlAttrs
and title
. When pushing HTML strings to head
or body
, you must use the html
template tag to mark your HTML as sanitized:
import {html} from 'fusion-core';
export default () => (ctx, next) => {
if (ctx.element) {
const userData = await getUserData();
// userData can't be trusted, and is automatically escaped
ctx.body.body.push(html`<div>${userData}</div>`)
}
return next();
}
If userData
above was <script>alert(1)</script>
, the string would be automatically turned into <div>\u003Cscript\u003Ealert(1)\u003C/script\u003E</div>
. Note that only userData
is escaped, but the HTML in your code stays intact.
If your HTML is complex and needs to be broken into smaller strings, you can also nest sanitized HTML strings like this:
const notUserData = html`<h1>Hello</h1>`
const body = html`<div>${notUserData}</div>`
Note that you cannot mix sanitized HTML with unsanitized strings:
ctx.body.body.push(html`<h1>Safe</h1>` + 'not safe') // will throw an error when rendered
Also note that only template strings can have template tags (i.e. html`<div></div>`). The following are NOT valid Javascript: html"<div></div>"
and html'<div></div>'
.
If you get an Unsanitized html. You must use html`[your html here]` error, remember to prepend the html
template tag to your template string.
If you have already taken steps to sanitize your input against XSS and don't wish to re-sanitize it, you can use dangerouslySetHTML(string)
to let Fusion render the unescaped dynamic string.
Serialization and deserialization
Here's how to serialize JSON data in the server:
ctx.body.body.push(html`<script id="__MY_DATA__" type="text/plain">${JSON.stringify(data)}</script>`);
Here's how to deserialize it in the browser:
import {unescape} from 'fusion-core';
const data = JSON.parse(unescape(document.getElementById('__MY_DATA__').innerHTML));