Move framework stuff into 'fx' folder

This commit is contained in:
SteveSandersonMS
2016-02-07 22:18:27 -08:00
parent 59b28ae5ee
commit 05905a42ab
16 changed files with 16 additions and 16 deletions

View File

@@ -0,0 +1,67 @@
// Credits for the type detection trick: http://www.bluewire-technologies.com/2015/redux-actions-for-typescript/
import * as React from 'react';
import { Dispatch } from 'redux';
import { connect as nativeConnect, ElementClass } from 'react-redux';
interface ActionClass<T extends Action> {
prototype: T;
}
export function typeName(name: string) {
return function<T extends Action>(actionClass: ActionClass<T>) {
// Although we could determine the type name using actionClass.prototype.constructor.name,
// it's dangerous to do that because minifiers may interfere with it, and then serialized state
// might not have the expected meaning after a recompile. So we explicitly ask for a name string.
actionClass.prototype.type = name;
}
}
export function isActionType<T extends Action>(action: Action, actionClass: ActionClass<T>): action is T {
return action.type == actionClass.prototype.type;
}
// Middleware for transforming Typed Actions into plain actions
export const typedToPlain = (store: any) => (next: any) => (action: any) => {
next(Object.assign({}, action));
};
export abstract class Action {
type: string;
constructor() {
// Make it an own-property (not a prototype property) so that it's included when JSON-serializing
this.type = this.type;
}
}
export interface Reducer<TState> extends Function {
(state: TState, action: Action): TState;
}
export interface ActionCreatorGeneric<TState> extends Function {
(dispatch: Dispatch, getState: () => TState): any;
}
interface ClassDecoratorWithProps<TProps> extends Function {
<T extends (typeof ElementClass)>(component: T): T;
props: TProps;
}
type ReactComponentClass<T, S> = new(props: T) => React.Component<T, S>;
class ComponentBuilder<TOwnProps, TActions, TExternalProps> {
constructor(private stateToProps: (appState: any) => TOwnProps, private actionCreators: TActions) {
}
public withExternalProps<TAddExternalProps>() {
return this as any as ComponentBuilder<TOwnProps, TActions, TAddExternalProps>;
}
public get allProps(): TOwnProps & TActions & TExternalProps { return null; }
public connect<TState>(componentClass: ReactComponentClass<TOwnProps & TActions & TExternalProps, TState>): ReactComponentClass<TExternalProps, TState> {
return nativeConnect(this.stateToProps, this.actionCreators as any)(componentClass);
}
}
export function provide<TOwnProps, TActions>(stateToProps: (appState: any) => TOwnProps, actionCreators: TActions) {
return new ComponentBuilder<TOwnProps, TActions, {}>(stateToProps, actionCreators);
}

View File

@@ -0,0 +1,49 @@
const domain = require('domain') as any;
const domainContext = require('domain-context') as any;
const domainTasksStateKey = '__DOMAIN_TASKS';
export function addTask(task: PromiseLike<any>) {
if (task && domain.active) {
const state = domainContext.get(domainTasksStateKey) as DomainTasksState;
if (state) {
state.numRemainingTasks++;
task.then(() => {
// The application may have other listeners chained to this promise *after*
// this listener. Since we don't want the combined task to complete until
// all the handlers for child tasks have finished, delay the following by
// one tick.
setTimeout(() => {
state.numRemainingTasks--;
if (state.numRemainingTasks === 0) {
state.triggerResolved();
}
}, 0);
}, state.triggerRejected);
}
}
}
export function run(codeToRun: () => void): Promise<void> {
return new Promise((resolve, reject) => {
domainContext.runInNewDomain(() => {
const state: DomainTasksState = {
numRemainingTasks: 0,
triggerResolved: resolve,
triggerRejected: reject
};
domainContext.set(domainTasksStateKey, state);
codeToRun();
// If no tasks were registered synchronously, then we're done already
if (state.numRemainingTasks === 0) {
resolve();
}
});
}) as any as Promise<void>;
}
interface DomainTasksState {
numRemainingTasks: number;
triggerResolved: () => void;
triggerRejected: () => void;
}

View File

@@ -0,0 +1,3 @@
declare module 'isomorphic-fetch' {
export default function fetch(url: string, opts: any): Promise<any>;
}

View File

@@ -0,0 +1,76 @@
var createMemoryHistory = require('history/lib/createMemoryHistory');
var url = require('url');
var babelCore = require('babel-core');
var babelConfig = {
presets: ["es2015", "react"]
};
var origJsLoader = require.extensions['.js'];
require.extensions['.js'] = loadViaBabel;
require.extensions['.jsx'] = loadViaBabel;
function loadViaBabel(module, filename) {
// Assume that all the app's own code is ES2015+ (optionally with JSX), but that none of the node_modules are.
// The distinction is important because ES2015+ forces strict mode, and it may break ES3/5 if you try to run it in strict
// mode when the developer didn't expect that (e.g., current versions of underscore.js can't be loaded in strict mode).
var useBabel = filename.indexOf('node_modules') < 0;
if (useBabel) {
var transformedFile = babelCore.transformFileSync(filename, babelConfig);
return module._compile(transformedFile.code, filename);
} else {
return origJsLoader.apply(this, arguments);
}
}
var domainTasks = require('./domain-tasks.js');
var bootServer = require('../boot-server.jsx').default;
function render(requestUrl, callback) {
var store;
var params = {
location: url.parse(requestUrl),
history: createMemoryHistory(requestUrl),
state: undefined
};
// Open a new domain that can track all the async tasks commenced during first render
domainTasks.run(function() {
// Since route matching is asynchronous, add the rendering itself to the list of tasks we're awaiting
domainTasks.addTask(new Promise(function (resolve, reject) {
// Now actually perform the first render that will match a route and commence associated tasks
bootServer(params, function(error, result) {
if (error) {
reject(error);
} else {
// The initial 'loading' state HTML is irrelevant - we only want to capture the state
// so we can use it to perform a real render once all data is loaded
store = result.store;
resolve();
}
});
}));
}).then(function() {
// By now, all the data should be loaded, so we can render for real based on the state now
params.state = store.getState();
bootServer(params, function(error, result) {
if (error) {
callback(error, null);
} else {
var html = result.html + `<script>window.__INITIAL_STATE = ${ JSON.stringify(store.getState()) }</script>`;
callback(null, html)
}
});
}).catch(function(error) {
process.nextTick(() => { // Because otherwise you can't throw from inside a catch
callback(error, null);
});
});
}
render('/', (err, html) => {
if (err) {
throw err;
}
console.log(html);
});

View File

@@ -0,0 +1,13 @@
import isomorphicFetch from 'isomorphic-fetch';
import { addTask } from './domain-tasks';
export function fetch(url: string): Promise<any> {
// TODO: Find some way to supply the base URL via domain context
var promise = isomorphicFetch('http://localhost:5000' + url, {
headers: {
Connection: 'keep-alive'
}
});
addTask(promise);
return promise;
}