Skip to content

Commit

Permalink
Refactor fusion-plugin-react-router to make depending on browser hist…
Browse files Browse the repository at this point in the history
…ory easier
  • Loading branch information
btford committed Apr 20, 2020
1 parent fcca3a2 commit e1d53c5
Showing 1 changed file with 132 additions and 98 deletions.
230 changes: 132 additions & 98 deletions fusion-plugin-react-router/src/plugin.js
Expand Up @@ -18,6 +18,7 @@ import {
unescape,
memoize,
RouteTagsToken,
RoutePrefixToken,
} from 'fusion-core';
import type {Token, Context, FusionPlugin} from 'fusion-core';

Expand Down Expand Up @@ -60,18 +61,37 @@ type PluginDepsType = {
RouteTags: typeof RouteTagsToken,
};

// Preserve browser history instance across HMR
// Preserving browser history across hmr fixes warning "Warning: You cannot change <Router history>"
// we don't want to preserve the `browserHistory` instance across jsdom tests however, as it will cause
// routes to match based on the previous location information.
// Note that for a given client, the route prefix is static.
// That makes it safe to do this caching.
let browserHistory;
function getBrowserHistory(basename) {
if (!browserHistory ||
(__DEV__ && typeof window.jsdom !== 'undefined')) {
browserHistory = createBrowserHistory({basename});
}
return browserHistory;
}

const plugin: FusionPlugin<PluginDepsType, HistoryWrapperType> = createPlugin({
export const BrowserHistoryToken = createToken('BrowserHistory');

export const browserHistoryPlugin = createPlugin({
deps: {prefix: RoutePrefixToken},
provides: ({prefix}) => getBrowserHistory(prefix)
});

const browserPlugin: FusionPlugin<PluginDepsType, HistoryWrapperType> = createPlugin({
deps: {
emitter: UniversalEventsToken.optional,
Provider: RouterProviderToken.optional,
getStaticContext: GetStaticContextToken.optional,
RouteTags: RouteTagsToken,
browserHistory: BrowserHistoryToken
},
middleware: (
{RouteTags, emitter, Provider = DefaultProvider, getStaticContext},
{RouteTags, emitter, browserHistory, Provider = DefaultProvider, getStaticContext},
self
) => {
return async (ctx, next) => {
Expand All @@ -81,101 +101,23 @@ const plugin: FusionPlugin<PluginDepsType, HistoryWrapperType> = createPlugin({
return next();
}
const myAPI = self.from(ctx);
if (__NODE__) {
let pageData = {
title: ctx.path,
page: ctx.path,
};
const context = getStaticContext
? getStaticContext(ctx)
: {
action: null,
location: null,
set status(code: number) {
ctx.status = code;
},
set url(url: string) {
const toUrl = addRoutePrefix(url, prefix);
if (typeof toUrl === 'string') {
ctx.redirect(toUrl);
}
},
};
// Expose the history object
const history = createServerHistory(prefix, context, prefix + ctx.url);
myAPI.history = history;
ctx.element = (
<Router
history={history}
Provider={Provider}
onRoute={d => {
pageData = d;
tags.name = pageData.title;
tags.page = pageData.page;
}}
basename={prefix}
context={context}
>
{ctx.element}
</Router>
);
return next().then(() => {
ctx.template.body.push(
html`
<script id="__ROUTER_DATA__" type="application/json">
${JSON.stringify(pageData)}
</script>
`
);

if (emitter) {
const scopedEmitter = emitter.from(ctx);
const emitTiming = type => timing => {
scopedEmitter.emit(type, {
title: pageData.title,
page: pageData.page,
status: ctx.status,
timing,
});
};
scopedEmitter.map(payload => {
if (payload && typeof payload === 'object') {
payload.__url__ = pageData.title;
}
return payload;
});
ctx.timing.end.then(timing => {
emitTiming('pageview:server')(timing);
ctx.timing.render.then(emitTiming('render:server'));
});
// TODO(#3): We should consider adding render/downstream/upstream timings for the browser
let pageData = {};
const element = document.getElementById('__ROUTER_DATA__');
if (element) {
pageData = JSON.parse(unescape(element.textContent));
tags.name = pageData.title;
tags.page = pageData.page;
}
emitter &&
emitter.map(payload => {
if (payload && typeof payload === 'object') {
payload.__url__ = pageData.title;
payload.__urlParams__ = pageData.params;
}
return payload;
});
} else if (__BROWSER__) {
// TODO(#3): We should consider adding render/downstream/upstream timings for the browser
let pageData = {};
const element = document.getElementById('__ROUTER_DATA__');
if (element) {
pageData = JSON.parse(unescape(element.textContent));
tags.name = pageData.title;
tags.page = pageData.page;
}
emitter &&
emitter.map(payload => {
if (payload && typeof payload === 'object') {
payload.__url__ = pageData.title;
payload.__urlParams__ = pageData.params;
}
return payload;
});
// preserving browser history across hmr fixes warning "Warning: You cannot change <Router history>"
// we don't want to preserve the `browserHistory` instance across jsdom tests however, as it will cause
// routes to match based on the previous location information.
if (
!browserHistory ||
(__DEV__ && typeof window.jsdom !== 'undefined')
) {
browserHistory = createBrowserHistory({basename: ctx.prefix});
}

// Expose the history object
myAPI.history = browserHistory;
ctx.element = (
Expand All @@ -194,19 +136,111 @@ const plugin: FusionPlugin<PluginDepsType, HistoryWrapperType> = createPlugin({
</Router>
);
return next();
}
};
},
provides() {
return {
from: memoize(() => {
// Note that `history` is set by the middleware layer above
const api: {history: RouterHistoryType} = ({
history: null,
}: any);
return api;
}),
})
}
}
});

const serverPlugin: FusionPlugin<PluginDepsType, HistoryWrapperType> = createPlugin({
deps: {
emitter: UniversalEventsToken.optional,
Provider: RouterProviderToken.optional,
getStaticContext: GetStaticContextToken.optional,
RouteTags: RouteTagsToken,
},
middleware: (
{RouteTags, emitter, Provider = DefaultProvider, getStaticContext},
self
) => {
return async (ctx, next) => {
const tags = RouteTags.from(ctx);
const prefix = ctx.prefix || '';
if (!ctx.element) {
return next();
}
const myAPI = self.from(ctx);
let pageData = {
title: ctx.path,
page: ctx.path,
};
const context = getStaticContext
? getStaticContext(ctx)
: {
action: null,
location: null,
set status(code: number) {
ctx.status = code;
},
set url(url: string) {
const toUrl = addRoutePrefix(url, prefix);
if (typeof toUrl === 'string') {
ctx.redirect(toUrl);
}
},
};
// Expose the history object
const history = createServerHistory(prefix, context, prefix + ctx.url);
myAPI.history = history;
ctx.element = (
<Router
history={history}
Provider={Provider}
onRoute={d => {
pageData = d;
tags.name = pageData.title;
tags.page = pageData.page;
}}
basename={prefix}
context={context}
>
{ctx.element}
</Router>
);
return next().then(() => {
ctx.template.body.push(
html`
<script id="__ROUTER_DATA__" type="application/json">
${JSON.stringify(pageData)}
</script>
`
);

if (emitter) {
const scopedEmitter = emitter.from(ctx);
const emitTiming = type => timing => {
scopedEmitter.emit(type, {
title: pageData.title,
page: pageData.page,
status: ctx.status,
timing,
});
};
scopedEmitter.map(payload => {
if (payload && typeof payload === 'object') {
payload.__url__ = pageData.title;
}
return payload;
});
ctx.timing.end.then(timing => {
emitTiming('pageview:server')(timing);
ctx.timing.render.then(emitTiming('render:server'));
});
}
});
};
},
});

const plugin = __NODE__ ? serverPlugin : browserPlugin;

export default plugin;

0 comments on commit e1d53c5

Please sign in to comment.