diff --git a/samples/react/MusicStore/ReactApp/TypedRedux.ts b/samples/react/MusicStore/ReactApp/TypedRedux.ts index 8da9487..4afa5d9 100644 --- a/samples/react/MusicStore/ReactApp/TypedRedux.ts +++ b/samples/react/MusicStore/ReactApp/TypedRedux.ts @@ -20,6 +20,11 @@ export function isActionType(action: Action, actionClass: Acti 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() { diff --git a/samples/react/MusicStore/ReactApp/boot-server.tsx b/samples/react/MusicStore/ReactApp/boot-server.tsx new file mode 100644 index 0000000..1183b12 --- /dev/null +++ b/samples/react/MusicStore/ReactApp/boot-server.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { Provider } from 'react-redux'; +import { renderToString } from 'react-dom/server'; +import { match, RouterContext } from 'react-router'; +React; + +import { routes } from './routes'; +import configureStore from './configureStore'; +import { ApplicationState } from './store'; + +export default function (params: any, callback: (err: any, result: { html: string, store: Redux.Store }) => void) { + match({ routes, location: params.location }, (error, redirectLocation, renderProps: any) => { + try { + if (error) { + throw error; + } + + const store = configureStore(params.history, params.state); + const html = renderToString( + + + + ); + + callback(null, { html, store }); + } catch (error) { + callback(error, null); + } + }); +} diff --git a/samples/react/MusicStore/ReactApp/boot.tsx b/samples/react/MusicStore/ReactApp/boot.tsx index 049a807..6a8b6fc 100644 --- a/samples/react/MusicStore/ReactApp/boot.tsx +++ b/samples/react/MusicStore/ReactApp/boot.tsx @@ -1,19 +1,19 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { browserHistory } from 'react-router'; +import { browserHistory, Router } from 'react-router'; import { Provider } from 'react-redux'; React; // Need this reference otherwise TypeScript doesn't think we're using it and ignores the import import './styles/styles.css'; import 'bootstrap/dist/css/bootstrap.css'; import configureStore from './configureStore'; -import { App } from './components/App'; +import { routes } from './routes'; const store = configureStore(browserHistory); ReactDOM.render( - + , document.getElementById('react-app') ); diff --git a/samples/react/MusicStore/ReactApp/components/App.tsx b/samples/react/MusicStore/ReactApp/components/App.tsx deleted file mode 100644 index 6a69415..0000000 --- a/samples/react/MusicStore/ReactApp/components/App.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from 'react'; -import { Router, Route, HistoryBase } from 'react-router'; -import NavMenu from './NavMenu'; -import Home from './public/Home'; -import Genres from './public/Genres'; -import GenreDetails from './public/GenreDetails'; -import AlbumDetails from './public/AlbumDetails'; - -export interface AppProps { - history: HistoryBase; -} - -export class App extends React.Component { - public render() { - return ( - - - - - - - - - ); - } -} - -class Layout extends React.Component<{ body: React.ReactElement }, void> { - public render() { - return
- -
- { this.props.body } -
-
; - } -} diff --git a/samples/react/MusicStore/ReactApp/configureStore.ts b/samples/react/MusicStore/ReactApp/configureStore.ts index 5c99212..1c6bede 100644 --- a/samples/react/MusicStore/ReactApp/configureStore.ts +++ b/samples/react/MusicStore/ReactApp/configureStore.ts @@ -2,13 +2,14 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import * as thunkModule from 'redux-thunk'; import { syncHistory, routeReducer } from 'react-router-redux'; import * as Store from './store'; +import { typedToPlain } from './TypedRedux'; export default function configureStore(history: HistoryModule.History, initialState?: Store.ApplicationState) { // Build middleware const thunk = (thunkModule as any).default; // Workaround for TypeScript not importing thunk module as expected const reduxRouterMiddleware = syncHistory(history); - const middlewares = [thunk, reduxRouterMiddleware]; - const devToolsExtension = (window as any).devToolsExtension; // If devTools is installed, connect to it + const middlewares = [thunk, reduxRouterMiddleware, typedToPlain]; + const devToolsExtension = null;//(window as any).devToolsExtension; // If devTools is installed, connect to it const finalCreateStore = compose( applyMiddleware(...middlewares), diff --git a/samples/react/MusicStore/ReactApp/domain-tasks.ts b/samples/react/MusicStore/ReactApp/domain-tasks.ts new file mode 100644 index 0000000..927895a --- /dev/null +++ b/samples/react/MusicStore/ReactApp/domain-tasks.ts @@ -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) { + 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 { + 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; +} + +interface DomainTasksState { + numRemainingTasks: number; + triggerResolved: () => void; + triggerRejected: () => void; +} diff --git a/samples/react/MusicStore/ReactApp/isomorphic-fetch.d.ts b/samples/react/MusicStore/ReactApp/isomorphic-fetch.d.ts index e980a14..a280907 100644 --- a/samples/react/MusicStore/ReactApp/isomorphic-fetch.d.ts +++ b/samples/react/MusicStore/ReactApp/isomorphic-fetch.d.ts @@ -1,3 +1,3 @@ declare module 'isomorphic-fetch' { - export default function fetch(url: string): Promise; + export default function fetch(url: string, opts: any): Promise; } diff --git a/samples/react/MusicStore/ReactApp/render-server.js b/samples/react/MusicStore/ReactApp/render-server.js new file mode 100644 index 0000000..542505c --- /dev/null +++ b/samples/react/MusicStore/ReactApp/render-server.js @@ -0,0 +1,74 @@ +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 + ``; + callback(null, html) + } + }); + }).catch(function(error) { + callback(error, null); + }); +} + +render('/', (err, html) => { + if (err) { + throw err; + } + + console.log(html); +}); diff --git a/samples/react/MusicStore/ReactApp/routes.tsx b/samples/react/MusicStore/ReactApp/routes.tsx new file mode 100644 index 0000000..bf4f5fc --- /dev/null +++ b/samples/react/MusicStore/ReactApp/routes.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Router, Route, HistoryBase } from 'react-router'; +import NavMenu from './components/NavMenu'; +import Home from './components/public/Home'; +import Genres from './components/public/Genres'; +import GenreDetails from './components/public/GenreDetails'; +import AlbumDetails from './components/public/AlbumDetails'; + +class Layout extends React.Component<{ body: React.ReactElement }, void> { + public render() { + return
+ +
+ { this.props.body } +
+
; + } +} + +export const routes = + + + + +; diff --git a/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts b/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts index 49358ca..a29b4b0 100644 --- a/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts +++ b/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts @@ -1,4 +1,4 @@ -import fetch from 'isomorphic-fetch'; +import { fetch } from '../tracked-fetch'; import { typeName, isActionType, Action, Reducer } from '../TypedRedux'; import { ActionCreator } from './'; import { Genre } from './GenreList'; diff --git a/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts b/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts index 775aeb2..1d00322 100644 --- a/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts +++ b/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts @@ -1,4 +1,4 @@ -import fetch from 'isomorphic-fetch'; +import { fetch } from '../tracked-fetch'; import { typeName, isActionType, Action, Reducer } from '../TypedRedux'; import { ActionCreator } from './'; diff --git a/samples/react/MusicStore/ReactApp/store/GenreDetails.ts b/samples/react/MusicStore/ReactApp/store/GenreDetails.ts index f3f411b..56b33b8 100644 --- a/samples/react/MusicStore/ReactApp/store/GenreDetails.ts +++ b/samples/react/MusicStore/ReactApp/store/GenreDetails.ts @@ -1,4 +1,4 @@ -import fetch from 'isomorphic-fetch'; +import { fetch } from '../tracked-fetch'; import { typeName, isActionType, Action, Reducer } from '../TypedRedux'; import { ActionCreator } from './'; import { Album } from './FeaturedAlbums'; diff --git a/samples/react/MusicStore/ReactApp/store/GenreList.ts b/samples/react/MusicStore/ReactApp/store/GenreList.ts index af52616..df9be71 100644 --- a/samples/react/MusicStore/ReactApp/store/GenreList.ts +++ b/samples/react/MusicStore/ReactApp/store/GenreList.ts @@ -1,4 +1,4 @@ -import fetch from 'isomorphic-fetch'; +import { fetch } from '../tracked-fetch'; import { typeName, isActionType, Action, Reducer } from '../TypedRedux'; import { ActionCreator } from './'; diff --git a/samples/react/MusicStore/ReactApp/tracked-fetch.ts b/samples/react/MusicStore/ReactApp/tracked-fetch.ts new file mode 100644 index 0000000..80795d6 --- /dev/null +++ b/samples/react/MusicStore/ReactApp/tracked-fetch.ts @@ -0,0 +1,13 @@ +import isomorphicFetch from 'isomorphic-fetch'; +import { addTask } from './domain-tasks'; + +export function fetch(url: string): Promise { + // 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; +} diff --git a/samples/react/MusicStore/package.json b/samples/react/MusicStore/package.json index ce2001a..16599f6 100644 --- a/samples/react/MusicStore/package.json +++ b/samples/react/MusicStore/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "bootstrap": "^3.3.6", + "domain-context": "^0.5.1", "isomorphic-fetch": "^2.2.1", "react": "^0.14.7", "react-bootstrap": "^0.28.2",