GithubHelp home page GithubHelp logo

fusionjs / fusion-core Goto Github PK

View Code? Open in Web Editor NEW
630.0 17.0 45.0 1.09 MB

Migrated to https://github.com/fusionjs/fusionjs

License: MIT License

JavaScript 99.39% Shell 0.50% Dockerfile 0.11%
fusion fusionjs

fusion-core's Introduction

fusion-core

Build status

The fusion-core package provides a generic entry point class for FusionJS applications that is used by the FusionJS runtime. It also provides primitives for implementing server-side code, and utilities for assembling plugins into an application to augment its functionality.

If you're using React, you should use the fusion-react package instead of fusion-core.


Table of contents


Usage

// main.js
import React from 'react';
import ReactDOM from 'react-dom';
import {renderToString} from 'react-dom/server';
import App from 'fusion-core';

const el = <div>Hello</div>;

const render = el =>
  __NODE__
    ? `<div id="root">${renderToString(el)}</div>`
    : ReactDOM.render(el, document.getElementById('root'));

export default function() {
  return new App(el, render);
}

API

App

import App from 'fusion-core';

A class that represents an application. An application is responsible for rendering (both virtual dom and server-side rendering). The functionality of an application is extended via plugins.

Constructor

const app: App = new App(el: any, render: Plugin<Render>|Render);
  • el: any - a template root. In a React application, this would be a React element created via React.createElement or a JSX expression.
  • render: Plugin<Render>|Render - defines how rendering should occur. A Plugin should provide a value of type Render
    • type Render = (el:any) => any

app.register

app.register(plugin: Plugin);
app.register(token: Token, plugin: Plugin);
app.register(token: Token, value: any);

Call this method to register a plugin or configuration value into a Fusion.js application.

You can optionally pass a token as the first argument to associate the plugin/value to the token, so that they can be referenced by other plugins within Fusion.js' dependency injection system.

app.middleware

app.middleware((deps: Object<string, Token>), (deps: Object) => Middleware);
app.middleware((middleware: Middleware));
  • deps: Object<string,Token> - A map of local dependency names to DI tokens
  • middleware: Middleware - a middleware
  • returns undefined

This method is a shortcut for registering middleware plugins. Typically, you should write middlewares as plugins so you can organize different middlewares into different files.

app.enhance

app.enhance(token: Token, value: any => Plugin | Value);

This method is useful for composing / enhancing functionality of existing tokens in the DI system.

app.cleanup

await app.cleanup();

Calls all plugin cleanup methods. Useful for testing.

  • returns Promise

Dependency registration

ElementToken
import App, {ElementToken} from 'fusion-core';
app.register(ElementToken, element);

The element token is used to register the root element with the fusion app. This is typically a react/preact element.

RenderToken
import ReactDOM from 'react-dom';
import {renderToString} from 'react-dom/server';
const render = el =>
  __NODE__
    ? renderToString(el)
    : ReactDOM.render(el, document.getElementById('root'));
import App, {RenderToken} from 'fusion-core';
const app = new App();
app.register(RenderToken, render);

The render token is used to register the render function with the fusion app. This is a function that knows how to render your application on the server/browser, and allows fusion-core to remain agnostic of the virtualdom library.

SSRDeciderToken
import App, {SSRDeciderToken} from 'fusion-core';
app.enhance(SSRDeciderToken, SSRDeciderEnhancer);

Ths SSRDeciderToken can be enhanced to control server rendering logic.

HttpServerToken
import App, {HttpServerToken} from 'fusion-core';
app.register(HttpServerToken, server);

The HttpServerToken is used to register the current server as a dependency that can be utilized from plugins that require access to it. This is normally not required but is available for specific usage cases.


Plugin

A plugin encapsulates some functionality into a single coherent package that exposes a programmatic API and/or installs middlewares into an application.

Plugins can be created via createPlugin

type Plugin {
  deps: Object<string, Token>,
  provides: (deps: Object) => any,
  middleware: (deps: Object, service: any) => Middleware,
  cleanup: ?(service: any) => void
}
createPlugin
import {createPlugin} from 'fusion-core';

Creates a plugin that can be registered via app.register()

const plugin: Plugin = createPlugin({
  deps: Object,
  provides: (deps: Object) => any,
  middleware: (deps: Object, service: any) => Middleware,
  cleanup: ?(service: any) => void
});
  • deps: Object<string, Token> - A map of local dependency names to DI tokens
  • provides: (deps: Object) => any - A function that provides a service
  • middleware: (deps: Object, service: any) => Middleware - A function that provides a middleware
  • cleanup: ?(service: any) => Runs when app.cleanup is called. Useful for tests
  • returns plugin: Plugin - A Fusion.js plugin

Token

A token is a label that can be associated to a plugin or configuration when they are registered to an application. Other plugins can then import them via dependency injection, by mapping a object key in deps to a token

type Token {
  name: string,
  ref: mixed,
  type: number,
  optional: ?Token,
}
createToken
const token:Token = createToken(name: string);
  • name: string - a human-readable name for the token. Used for generating useful error messages.
  • returns token: Token

Memoization

import {memoize} from 'fusion-core';

Sometimes, it's useful to maintain the same instance of a plugin associated with a request lifecycle. For example, session state.

Fusion.js provides a memoize utility function to memoize per-request instances.

const memoized = {from: memoize((fn: (ctx: Context) => any))};
  • fn: (ctx: Context) => any - A function to be memoized
  • returns memoized: (ctx: Context) => any

Idiomatically, Fusion.js plugins provide memoized instances via a from method. This method is meant to be called from a middleware:

createPlugin({
  deps: {Session: SessionToken},
  middleware({Session}) {
    return (ctx, next) => {
      const state = Session.from(ctx);
    }
  }
}

Middleware

type Middleware = (ctx: Context, next: () => Promise) => Promise
  • ctx: Context - a Context
  • next: () => Promise - An asynchronous function call that represents rendering

A middleware function is essentially a Koa middleware, a function that takes two argument: a ctx object that has some FusionJS-specific properties, and a next callback function. However, it has some additional properties on ctx and can run both on the server and the browser.

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:

const middleware = () => 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);
};

Plugins can add dependency injected middlewares.

// fusion-plugin-some-api
const APIPlugin = createPlugin({
  deps: {
    logger: LoggerToken,
  },
  provides: ({logger}) => {
    return new APIClient(logger);
  },
  middleware: ({logger}, apiClient) => {
    return async (ctx, next) => {
      // do middleware things...
      await next();
      // do middleware things...
    };
  },
});
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

Additionally, when server-side rendering a page, FusionJS sets ctx.template to an object with the following properties:

  • ctx: Object
    • template: Object
      • htmlAttrs: Object - attributes for the <html> tag. For example {lang: 'en-US'} turns into <html lang="en-US">. Default: empty object
      • bodyAttrs: Object - attributes for the <body> tag. For example {test: 'test'} turns into <body test="test">. Default: empty object
      • title: string - The content for the <title> tag. Default: empty string
      • head: Array<SanitizedHTML> - A list of sanitized HTML strings. Default: empty array
      • body: Array<SanitizedHTML> - 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.

In the server, ctx also exposes the same properties as a Koa context

  • ctx: Object

    • req: http.IncomingMessage - Node's request object
    • res: Response - Node's response object
    • request: Request - Koa's request object:
      View Koa request details
      • header: Object - alias of request.headers
      • headers: Object - map of parsed HTTP headers
      • method: string - HTTP method
      • url: string - request URL
      • originalUrl: string - same as url, except that url may be modified (e.g. for URL rewriting)
      • path: string - request pathname
      • query: Object - parsed querystring as an object
      • querystring: string - querystring without ?
      • host: string - host and port
      • hostname: string - get hostname when present. Supports X-Forwarded-Host when app.proxy is true, otherwise Host is used
      • length:number - return request Content-Length as a number when present, or undefined.
      • origin: string - request origin, including protocol and host
      • href: string - full URL including protocol, host, and URL
      • fresh: boolean - check for cache negotiation
      • stale: boolean - inverse of fresh
      • socket: Socket - request socket
      • protocol: string - return request protocol, "https" or "http". Supports X-Forwarded-Proto when app.proxy is true
      • secure: boolean - shorthand for ctx.protocol == "https" to check if a request was issued via TLS.
      • ip: string - remote IP address
      • ips: Array<string> - proxy IPs
      • subdomains: Array<string> - return subdomains as an array.For example, if the domain is "tobi.ferrets.example.com": If app.subdomainOffset is not set, ctx.subdomains is ["ferrets", "tobi"]
      • is: (...types: ...string) => boolean - request type check is('json', 'urlencoded')
      • accepts: (...types: ...string) => boolean - request MIME type check
      • acceptsEncodings: (...encodings: ...string) => boolean - check if encodings are acceptable
      • acceptsCharset: (...charsets: ...string) => boolean - check if charsets are acceptable
      • acceptsLanguages: (...languages: ...string) => boolean - check if langs are acceptable
      • get: (name: String) => string - returns a header field
    • response: Response - Koa's response object:

      View Koa response details

      • header: Object - alias of request.headers
      • headers: Object - map of parsed HTTP headers
      • socket: Socket - response socket
      • status: String - response status. By default, response.status is set to 404 unlike node's res.statusCode which defaults to 200.
      • message: String - response status message. By default, response.message is associated with response.status.
      • length: Number - response Content-Length as a number when present, or deduce from ctx.body when possible, or undefined.
      • body: String, Buffer, Stream, Object(JSON), null - get response body
      • get: (name: String) => string - returns a header field
      • set: (field: String, value: String) => undefined - set response header field to value
      • set: (fields: Object) => undefined - set response fields
      • append: (field: String, value: String) => undefined - append response header field with value
      • remove: (field: String) => undefined - remove header field
      • type: String - response Content-Type
      • is: (...types: ...string) => boolean - response type check is('json', 'urlencoded')
      • redirect: (url: String, alt: ?String) => undefined- perform a 302 redirect to url
      • attachment (filename: ?String) => undefined - set Content-Disposition to "attachment" to signal the client to prompt for download. Optionally specify the filename of the download.
      • headerSent: boolean - check if a response header has already been sent
      • lastModified: Date - Last-Modified header as a Date
      • etag: String - set the ETag of a response including the wrapped "s.
      • vary: (field: String) => String - vary on field
      • flushHeaders () => undefined - flush any set headers, and begin the body
    • cookies: {get, set} - cookies based on Cookie Module:

      View Koa cookies details

      • get: (name: string, options: ?Object) => string - get a cookie
        • name: string
        • options: {signed: boolean}
      • set: (name: string, value: string, options: ?Object)
        • name: string
        • value: string
        • options: Object - Optional
          • maxAge: number - a number representing the milliseconds from Date.now() for expiry
          • signed: boolean - sign the cookie value
          • expires: Date - a Date for cookie expiration
          • path: string - cookie path, /' by default
          • domain: string - cookie domain
          • secure: boolean - secure cookie
          • httpOnly: boolean - server-accessible cookie, true by default
          • overwrite: 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.
    • state: Object - recommended namespace for passing information through middleware and to your frontend views ctx.state.user = await User.find(id)
    • throw: (status: ?number, message: ?string, properties: ?Object) => void - throws an error
      • status: number - HTTP status code
      • message: string - error message
      • properties: Object - is merged to the error object
    • assert: (value: any, status: ?number, message: ?string, properties: ?Object) - throws if value is falsy. Uses Assert
      • value: any
      • status: number - HTTP status code
      • message: string - error message
      • properties: 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.
    • app: Object - a reference to the Koa instance

Sanitization

html

import {html} from 'fusion-core';

A template tag that creates safe HTML objects that are compatible with ctx.template.head and ctx.template.body. Template string interpolations are escaped. Use this function to prevent XSS attacks.

const sanitized: SanitizedHTML = html`<meta name="viewport" content="width=device-width, initial-scale=1">`

escape

import {escape} from 'fusion-core';

Escapes HTML

const escaped:string = escape(value: string)
  • value: string - the string to be escaped

unescape

import {unescape} from 'fusion-core';

Unescapes HTML

const unescaped:string = unescape(value: string)
  • value: string - the string to be unescaped

dangerouslySetHTML

import {dangerouslySetHTML} from 'fusion-core';

A function that blindly creates a trusted SanitizedHTML object without sanitizing against XSS. Do not use this function unless you have manually sanitized your input and written tests against XSS attacks.

const trusted:string = dangerouslySetHTML(value: string)
  • value: string - the string to be trusted

Examples

Dependency injection

To use plugins, you need to register them with your Fusion.js application. You do this by calling app.register with the plugin and a token for that plugin. The token is a value used to keep track of what plugins are registered, and to allow plugins to depend on one another.

You can think of Tokens as names of interfaces. There's a list of common tokens in the fusion-tokens package.

Here's how you create a plugin:

import {createPlugin} from 'fusion-core';
// fusion-plugin-console-logger
const ConsoleLoggerPlugin = createPlugin({
  provides: () => {
    return console;
  },
});

And here's how you register it:

// src/main.js
import ConsoleLoggerPlugin from 'fusion-plugin-console-logger';
import {LoggerToken} from 'fusion-tokens';
import App from 'fusion-core';

export default function main() {
  const app = new App(...);
  app.register(LoggerToken, ConsoleLoggerPlugin);
  return app;
}

Now let's say we have a plugin that requires a logger. We can map logger to LoggerToken to inject the logger provided by ConsoleLoggerPlugin to the logger variable.

// fusion-plugin-some-api
import {createPlugin} from 'fusion-core';
import {LoggerToken} from 'fusion-tokens';

const APIPlugin = createPlugin({
  deps: {
    logger: LoggerToken,
  },
  provides: ({logger}) => {
    logger.log('Hello world');
    return new APIClient(logger);
  },
});

The API plugin is declaring that it needs a logger that matches the API documented by the LoggerToken. The user then provides an implementation of that logger by registering the fusion-plugin-console-logger plugin with the LoggerToken.

Implementing HTTP endpoints

You can use a plugin to implement a RESTful HTTP endpoint. To achieve this, run code conditionally based on the URL of the request

app.middleware(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 fs from 'fs';
import {html, unescape, createPlugin} from 'fusion-core'; // html sanitization

export default createPlugin({
  middleware: () => {
    const data = __NODE__ && JSON.parse(fs.readFileSync('package.json').toString());
    return async (ctx, next) => {
      if (__NODE__) {
        ctx.template.head.push(html`<meta id="app-version" content="${data.version}">`);
        return next();
      } else {
        const version = unescape(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.register(VersionPlugin);
  return app;
}

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';

const middleware = (ctx, next) => {
  if (ctx.element) {
    const userData = await getUserData();
    // userData can't be trusted, and is automatically escaped
    ctx.template.body.push(html`<div>${userData}</div>`)
  }
  return next();
}

If userData above was <script>alert(1)</script>, ththe 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.template.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.

Enhancing a dependency

If you wanted to add a header to every request sent using the registered fetch.

app.register(FetchToken, window.fetch);
app.enhance(FetchToken, fetch => {
  return (url, params = {}) => {
    return fetch(url, {
      ...params,
      headers: {
        ...params.headers,
        'x-test': 'test',
      },
    });
  };
});

You can also return a Plugin from the enhancer function, which provides the enhanced value, allowing the enhancer to have dependencies and even middleware.

app.register(FetchToken, window.fetch);
app.enhance(FetchToken, fetch => {
  return createPlugin({
    provides: () => (url, params = {}) => {
      return fetch(url, {
        ...params,
        headers: {
          ...params.headers,
          'x-test': 'test',
        },
      });
    },
  });
});

Controlling SSR behavior

By default we do not perfrom SSR for any paths that match the following extensions: js, gif, jpg, png, pdf and json. You can control SSR behavior by enhancing the SSRDeciderToken. This will give you the ability to apply custom logic around which routes go through the renderer. You may enhance the SSRDeciderToken with either a function, or a plugin if you need dependencies.

import {SSRDeciderToken} from 'fusion-core';
app.enhance(SSRDeciderToken, decide => ctx =>
  decide(ctx) && !ctx.path.match(/ignore-ssr-route/)
);

fusion-core's People

Contributors

renovate[bot] avatar kevingrandon avatar ganemone avatar alexmsmithca avatar lhorie avatar rtsao avatar nadiia avatar dennisgl avatar derekjuber avatar renovate-bot avatar rajeshsegu avatar albertywu avatar ksheedlo avatar matthisk avatar blackswanny avatar epes avatar shannonmoeller avatar sblotner avatar revskill10 avatar uberopensourcebot avatar angus-c avatar

Stargazers

Cat  avatar ใ‚†ใ‚“ใผใ† | yunbow avatar kelvin-guru avatar  avatar  avatar Szymon Nowacki avatar Andy Chen avatar Andrejs Agejevs avatar  avatar  avatar Jaffrey avatar Sathya Ravi avatar Gustavo Pereira avatar oxxd avatar Manoj Madushanka avatar Georvic Tur avatar Roger Lau avatar Souhail RAZZOUK avatar Peter Petrov avatar Kai Hotz avatar Kaid Wong avatar Sebastien Couture avatar Jay Robohn avatar  avatar  avatar Siyuan Wang avatar  avatar Vaughan Rouesnel avatar Gitesh Gupta avatar Daniel Blendea avatar Fahimnur Alam avatar Z avatar Susan Mosby avatar Tristan NGUYEN avatar Rui Afonso Pereira avatar Chentaoyu avatar TbhT avatar  avatar Prakash Senthil Vel avatar Mio Green avatar Scott Boudreaux avatar crapthings avatar gaterking avatar Exequiel Ceasar Navarrete avatar Chee Aun avatar Nikolai Skvortsov avatar  avatar  avatar Georgi Tsaklev avatar ่ƒกๅฐๆ น avatar Xiaoguang Chen avatar Dmitriy (Dima) Rozhkov avatar Manaia Junior avatar Mateusz Stachowicz avatar a.o avatar Azim Kurt avatar Simon Jentsch avatar  avatar Peter Yang avatar Huy Z avatar ๅ—็Ÿฅ avatar Erik Peng avatar Keiichiro Soeda avatar Mohamed Chorfa avatar Hรฉctor Cerรณn Figueroa avatar Bharat Pandya avatar Adrian Carriger avatar Tom Raithel avatar Junior Farias avatar Porramate Lim avatar Guo Jia avatar Eugene Metagnostic avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar Mixail Voronov avatar  avatar  avatar Csaba Palfi avatar Joel Almeida avatar Ander Suรกrez Martรญnez avatar Shinkiro avatar  avatar Janis Rullis avatar Adrien Denat avatar Hiroki Sato avatar yokinist avatar Dave Nunez avatar Bernardo Graรงa avatar Luiz Estรกcio | stacio.eth avatar  avatar Alexandru Furculita avatar Arnau Siches avatar Vicente Canales avatar Urata Daiki avatar

Watchers

 avatar Jandy avatar  avatar  avatar James Cloos avatar  avatar Dooley Nsewolo avatar  avatar Sasha Prentow avatar Sagi Avinash Varma avatar Carsten Jacobsen avatar Derek Ju avatar  avatar  avatar Gregory Tereshko avatar  avatar Mike Van avatar

fusion-core's Issues

Error for missing dependency is not helpful

It says "Missing required value for token: [object Object]."

It should say the name of the token. Ideally, it should also mention what plugin was being resolved when the resolution error occurred

DI error is confusing

When something is registered with no token, the error can be completely cryptic. The error should be more actionable

Overriding RenderToken doesn't work

This works:

import App from 'fusion-react';
import ReactDOM from 'react-dom';

const render = __NODE__
  ? () => '<div id="root"></div>'
  : el => ReactDOM.render(el, document.getElementById('root'));
const app = new App(root, render);

This doesn't

import App from 'fusion-react';
import ReactDOM from 'react-dom';
import RenderToken from 'fusion-core';

const render = __NODE__
  ? () => '<div id="root"></div>'
  : el => ReactDOM.render(el, document.getElementById('root'));
const app = new App(root);
app.register(RenderToken, render)

Add support for async plugin cleanup function

It is useful for testing to allow plugins to register cleanup functions. This will drastically simplify
testing any plugin that depends on side effects such as opening a long lived connection, timers, intervals, etc.

Redirects don't work as expected

Type of issue

Bug

Description

Seeing some odd behavior with respect to server side redirects (using ctx.redirect). It appears to run through the middleware stack twice for a single request / response. Using koa directly, we don't see this behavior, and instead see a 302 response. With fusion, we see a 200 response.

Update browser behavior of ctx.rendered to mimick that of servers

On the server we set ctx.rendered to be the result of

await ctx.render(ctx.element)

If a Promise was returned from render on the client, we were not awaiting it before calling next. We should be consistent with how we handle the render function, and await the result on the client.

Explore missing optional dependency issue

Problem/Rationale

Plugin that has optional tokens with null defaults as dependencies fails to instantiate when they are not overridden in [email protected].

Solution/Change/Deliverable

Explore and fix. Deliverable includes pre-release version increment and additional unit tests, if necessary.

Surface SSR errors in browser UI

Right now when node errors my browser just spins forever while the error has already been outputted to my console. If we get an error during SSR it should just takeover the page.

In webpack, we would just specify the overlay: true option. We could also look at what create-react-app does. E.g., facebook/create-react-app/pull/744.

Intermittently failing unit test

# timing plugin
ok 69 exposes a end resolve function
ok 70 exposes a end reject function
ok 71 timing.end.promise is a promise
ok 72 exposes a upstream resolve function
ok 73 exposes a upstream reject function
ok 74 timing.upstream.promise is a promise
ok 75 exposes a downstream resolve function
ok 76 exposes a downstream reject function
ok 77 timing.downstream.promise is a promise
ok 78 exposes a render resolve function
ok 79 exposes a render reject function
ok 80 timing.render.promise is a promise
ok 81 sets up ctx.timing.start
ok 82 sets up ctx.timing.end to be a promise
ok 83 sets end timing result
not ok 84 result time is at least 10ms
  ---
    operator: ok
    expected: true
    actual:   false
    at: ctx.timing.end.then.result (/fusion-core/dist-tests/node.js:6969:9)
...

We probably need to do a better job of mocking APIs in this test - should not depend on real timers here.

Update check in `isSSR` function to use asset manifest

We are currently using a regular expression to match javascript files as a hack to allow viewing them in the browser. We should instead check against the asset manifest so we know exactly what file is being requested

Improve types for token aliasing

Type checking for token aliasing is not great at the moment, as it allows you to alias tokens of different types. We should keep an eye on the development of flow to see if we can improve this.

Investigate API for composition within DI system

I had some ideas surrounding composition within the DI system that I wanted to get down. This is purely a brain dump and not a final interface.

A classic example of where this could be useful is the fetch token. We already have an example of a plugin which requires a fetch implementation, and provides a new fetch implementation - fusion-plugin-csrf-protection. We solved this problem through token aliasing, which works well enough but I think it can be a bit difficult to understand at first.

Lets say we had three plugins that wanted to register on the FetchToken (polyfill, routePrefix, csrf). Using token aliasing, we would have something like the following:

const BaseFetchToken = createToken('BaseFetch');
const PrefixFetchToken = createToken('PrefixFetch');
app.register(BaseFetchToken, window.fetch);
app.register(PrefixFetchToken, RoutePrefixFetchPlugin).alias(FetchToken, BaseFetchToken);
app.register(FetchToken, CsrfPlugin).alias(FetchToken, PrefixFetchToken);

This is fine, but I think it would be better if we didn't have to keep track of all these different tokens. Using composition it would look like this:

app.register(FetchToken, window.fetch);
app.registerCompose(FetchToken, RoutePrefixComposition);
app.registerCompose(FetchToken, CsrfComposition);

Now with less hand waiving...

app.register(FetchToken, window.fetch);
// add routePrefix
app.registerCompose(FetchToken, (fetch) => {
  return (url, options) => {
    return fetch(window.routePrefix + url, options);
  }
});
// add csrf
app.registerCompose(FetchToken, (fetch) => {
  return createPlugin({...});
});

The first argument to registerCompose is the Token to register and compose with, and the second argument is function that takes the current resolved provides of the composed token, (in this case the implementation of fetch) and returns either a plugin or a new composed value directly. In the case of returning a plugin, the value that will be composed will be the result of the plugin's provides function.

Thoughts?

Make createPlugin asynchronous

Problem/Rationale

Instantiation of a plugin may not always be best served by being synchronous.

Solution/Change/Deliverable

Allow createPlugin and it's associated methods to be asynchronous.

DI API Tweak

I have found myself confusing when to use app.register vs app.configure a few times when working through the DI migration. To give some context, we have two things that can be given to the DI system: plugins and values. A value is given to the DI system via app.configure and a plugin is given to the DI system via app.register. There are a few reasons for this distinction, but suffice to say that it matters.

The problem for me is that app.register intuitively feels like it should work with things that are not plugins. For example:

app.register(FetchToken, window.fetch)
app.configure(FetchToken, window.fetch)

When choosing between the two options above, they both seem equally likely to be correct, when in fact app.configure is correct, and app.register is incorrect.

To fix this, I want to propose a small tweak to the api by renaming app.register to app.plugin.

app.plugin(FetchToken, window.fetch)
app.configure(FetchToken, window.fetch)

This makes it much more clear that only plugins can be registered with the DI system via app.plugin and configuration via app.configure. It also allows us to say "registered with theDI system" without conflating it with app.register, since app.configure is also "registering" something with the DI system.

Along side this change, I think it would make sense to make the following tweaks.

  • rename withDependencies to createPlugin
  • undo the currying of createPlugin (previously withDependencies)
  • remove the need for withMiddleware for pure functional plugins

Lets take a look at some examples of the old vs new apis.

GitHub Logo

// ----------------------------
// plugin with middleware only
// ----------------------------
// old and busted
const SomePlugin = withDependencies({depA: TokenA})(({depA}) => {
   const myThing = new MyThing(depA);
   return myThing;
});
app.register(SomePluginToken, SomePlugin);

// new hotness
const SomePlugin = createPlugin({depA: TokenA}, ({depA}) => {
   const myThing = new MyThing(depA);
   return myThing;  
});
app.plugin(SomePlugin);

// -------------------------------
// plugin with middleware and api
// -------------------------------
// old and busted
const SomePlugin = withDependencies({depA: TokenA})(({depA}) => {
   const myThing = new MyThing(depA);
   return withMiddleware(myThing, (ctx, next) => next());
});
app.register(SomePluginToken, SomePlugin);

// new hotness
const SomePlugin = createPlugin({depA: TokenA}, ({depA}) => {
   const myThing = new MyThing(depA);
   return withMiddleware(myThing, (ctx, next) => next());  
});
app.plugin(SomePlugin);

// -----------------------
// middleware only plugin
// -----------------------
// old and busted
const MiddlewareOnlyPlugin = withMiddleware((ctx, next) => next());
app.register(MiddlewareOnlyPlugin);

// new hotness
const MiddlewareOnlyPlugin = (ctx, next) => next();
app.plugin(MiddlewareOnlyPlugin);

Overall I think this improves the readability and clarity of fusionjs code and reduces some boilerplate.

The only thing that could still use some work is pure functional middleware with dependencies.

// old and busted
const MiddlewareOnlyWithDeps = withDependencies({depA: TokenA})(({depA}) => { 
  return withMiddleware((ctx, next) => next());
});
app.register(MiddlewareOnlyWithDeps);

// new and still busted
const MiddlewareOnlyWithDeps = createPlugin({depA: TokenA}, (depA) => {
  return withMiddleware((ctx, next) => next());
});
app.plugin(MiddlewareOnlyWithDeps);

This is still a little funky, because nothing is going with the middleware. One option is we could support just returning a function directly, but that would impose the restriction that programmatic apis of plugins couldn't be functions. I'm open to ideas on how to make this better, but I don't think it is a dealbreaker for shipping.

Comments?

Pass null for `ctx` rather than an empty object when doing `Plugin.of()`

it is often useful to check if ctx is truthy inside services. We currently pass an empty object to the constructor when a service is instantiated via Plugin.of(). This is confusing and difficult to work with, since you need to do some hack such as:

if (!ctx.headers) {
  // ctx is actually an empty object
}

Instead, we should pass null so you can easily do truthy checks in the constructor.

Investigate alternatives to checking accept header for text/html

Currently we check the accept header for text/html in order to decide if the route should be a ssr route or not. This can be confusing in development when someone wants to see the JSON response of some data endpoint and they load the endpoint via the browser URL bar. This can potentially run SSR logic alongside the intended code, which may affect performance metrics and other SSR-specific side effects.

Move/improve createToken api into fusion-core

We have a few issues with the current createToken api which would be solved by some small changes.

  1. Move createToken into fusion-core

This allows fusion-core to define more about the shape of a token which will in turn lead to better error messages. See #93

  1. Update the API of createToken to the following:
const LoggerToken = createToken('Logger');

// require a logger
createPlugin({
  deps: {
    logger: LoggerToken.required, 
  },
});

// optional logger
createPlugin({
  deps: {
    logger: LoggerToken.optional, 
  },
});

Thoughts?

Expose context to renderer initialization

I'm trying to write a custom renderer for fusion (replacement of fusion-react), and in order to SSR I need to be able to extract information from the context during the request.

Sorry if this is already supported, I haven't been able to figure out the best way to thread context through to the renderer initialization.

Add framework comparison

People often ask how a new framework compares to similar solutions. It would be useful to provide a high level comparison with current popular solutions

Support optional dependencies

Rationale:

Some plugins consume UniversalEvents but only for emission of things like stats. It would make sense for the dependency to be optional, and this ability used to be supported pre-DI.

Deliverable:

Propose a mechanism that would allow plugins to specify dependencies as optional

Yarn link workflows broken

When using yarn link on packages which leverage HTML sanitization[1] in fusion-core Symbols break across packages.

STR:

  • cd fusion-core && yarn link fusion-core
  • cd fusion-plugin-react-router && yarn link fusion-plugin-react-router
  • cd myapp && yarn link fusion-core && yarn link fusion-plugin-react-router
  • Load your app in a browser.

Expected result:

  • Page loads and everything works fine.

Actual result:

  • See an error regarding escaped context HTML not appearing.

[1]

const key = Symbol('sanitized html');

Add support for dependency injection in plugins

NOTE: This issue needs to be updated with much more detail. This is a starting point for reference

// Basic example of main
// src/main.js
import App from 'fusion-core';
import PluginA from 'fusion-plugin-a';
import PluginB from 'fusion-plugin-b';
import {TokenA, TokenB} from 'fusion-types'
export default function main() {
  const app = new App();
  app.register(PluginA, TokenA);
  app.register(PluginB, TokenB);
  return app;
}
// basic example of a plugin with dependencies
// fusion-plugin-a
import {withDependencies} from 'fusion-core';
import {TokenB} from 'fusion-types';

class MyThing {
  // some api here...
}

export default withDependencies({b: TokenB})(deps => {
  const {b} = deps;
  return new MyThing(b);
});
// basic example of a plugin with dependencies and a middleware
// fusion-plugin-a
import {withDependencies, withMiddleware} from 'fusion-core';
import {TokenB} from 'fusion-types';

export default withDependencies({b: TokenB})(deps => {
  const {b} = deps;
  const thing = new MyThing(b);
  return withMiddleware((ctx, next) => {
    // do middleware things
    return next();
  }, thing);
});
// example of main with middleware
import App from 'fusion-core';
import PluginA from 'fusion-plugin-a';
import PluginB from 'fusion-plugin-b';
import {TokenA, TokenB} from 'fusion-types'
export default function main() {
  const app = new App();
  app.register(PluginA, TokenA);
  app.register(PluginB, TokenB);
  
  app.middleware((deps) => (ctx, next) => {
    // use your dependencies
    deps.a.thing()
    return next();
  }, {a: TokenA});
  return app;
}

Error message improvements

Error: Registered token without depending on it: () => { throw new Error(Missing required value for token: ${name}.); }

image

Optional Tokens should be typed as `undefined` or the expected type

Problem/Rationale

Optional Tokens are not as explicit as they should be, and should allow for only:

  • undefined - no value registered
  • T - value that was registered

Today, they are maybe values (which allows nulls).

Solution/Change/Deliverable

Change type signature of Token<T> to:

type Token<T> = {
  (): T,
  optional: () => void | T,
};

Fix render and upstream timing

render timing
current =>

// pseudo
renderStart = now();
await render();
timer.render.resolve(now() - renderStart);

await next();

Resolving render timing before completing downstream when the status code has not yet determined is not ideal (always 404).

fix =>

// pseudo
renderStart = now();
await render();
renderTime = now() - renderStart;

await next();
timer.render.resolve(renderTime);

We only emit the render timing after response(status code) has been determined

upstream timing
current =>

// pseudo
// renderer
upstreamStart = now();
await next();
timer.upstream.resolve(now() - upstreamStart);

This is incorrect. upstream timing should measure the middlewares' execution from renderer all the way back to the timing plugin.

fix =>

// pseudo
// renderer

await next();
timer.upstreamStart = now();
// pseudo
// timing

return next().then(() => {
  const upstreamTime = now() - timing.from(ctx).upstreamStart;
  upstream.resolve(upstreamTime);
});

Throw if extraneous configuration is registered

We should throw if a non-plugin value is registered with fusion but not consumed. The main reason for this is that we want to enforce the least amount of code possible being shipped to the browser. Additionally, there may be configuration with semi-sensitive information that users would not want leaking to the browser. For example, a port list or service name list.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.