Skip to content

Commit

Permalink
react-navi bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesknelson committed Sep 23, 2018
1 parent 87ccd83 commit 2ca5814
Show file tree
Hide file tree
Showing 56 changed files with 4,968 additions and 7,685 deletions.
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["babel-preset-react-app"]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"private": true,
"devDependencies": {
"babel-loader": "^7.1.2",
"cross-env": "^5.0.5",
"lerna": "^3.3.2",
"rimraf": "^2.6.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as React from 'react'
import {
createPage,
createJunction,
Expand Down Expand Up @@ -34,7 +33,7 @@ let Article2 = createPage({
})


class ArticlesComponent extends React.Component<{ route: JunctionRoute<typeof ArticlesJunction['meta']> }, any> {
class ArticlesComponent extends React.Component {
render() {
let { route } = this.props

Expand All @@ -61,7 +60,7 @@ let ArticlesJunction = createJunction({
})


class AppComponent extends React.Component<{ route: JunctionRoute }> {
class AppComponent extends React.Component {
render() {
let { route } = this.props

Expand Down
5 changes: 2 additions & 3 deletions packages/junctions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@
"devDependencies": {
"@types/jest": "^22.1.0",
"cross-env": "^5.0.5",
"history": "^4.7.2",
"jest": "^22.1.4",
"react": "^16.2.0",
"rimraf": "^2.6.2",
"rollup": "^0.50.0",
"rollup-plugin-commonjs": "^8.2.6",
"rollup-plugin-node-resolve": "^3.0.0",
"ts-jest": "^22.0.1",
"typescript": "3.0.3",
"zen-observable": "^0.7.1"
"typescript": "3.0.3"
},
"dependencies": {
"@types/history": "^4.6.2"
Expand Down
8 changes: 3 additions & 5 deletions packages/junctions/src/BrowserNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { createBrowserHistory, History, locationsAreEqual } from 'history';
import { Navigation, NavigationState } from './Navigation'
import { RouteType } from './Route'
import { Router } from './Router'
import { Router, RouterOptions, createRouter } from './Router'
import { RoutingState } from './RoutingState'
import { Observer, SimpleSubscription, createOrPassthroughObserver } from './Observable'
import { HistoryRoutingObservable, createHistoryRoutingObservable } from './HistoryRoutingObservable';


type BrowserNavigationOptions<Context> = {
router: Router<Context>,

interface BrowserNavigationOptions<Context> extends RouterOptions<Context> {
/**
* You can manually supply a history object. This is useful for
* integration with react-router.
Expand Down Expand Up @@ -55,7 +53,7 @@ export class BrowserNavigation<Context> implements Navigation {

constructor(options: BrowserNavigationOptions<Context>) {
this.history = options.history || createBrowserHistory()
this.router = options.router
this.router = createRouter(options)

if (options.setDocumentTitle !== false) {
this.setDocumentTitle = typeof options.setDocumentTitle === 'function' ? options.setDocumentTitle : ((x) => x || 'Untitled Page')
Expand Down
38 changes: 31 additions & 7 deletions packages/junctions/src/Errors.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { Location, createURL, concatLocations } from './Location'
import { MappingMatch } from './Mapping';

// See https://stackoverflow.com/questions/31089801/extending-error-in-javascript-with-es6-syntax-babel
class ExtendableError extends Error {
// See https://stackoverflow.com/questions/30402287/extended-errors-do-not-have-message-or-stack-trace
export class NaviError extends Error {
__proto__: NaviError;

constructor(message) {
const trueProto = new.target.prototype;

super(message);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
this.__proto__ = trueProto;

if (Error.hasOwnProperty('captureStackTrace'))
Error.captureStackTrace(this, this.constructor);
else
Object.defineProperty(this, 'stack', {
value: (new Error()).stack
});

Object.defineProperty(this, 'message', {
value: message
});
}
}

export class NotFoundError extends ExtendableError {
export class NotFoundError extends NaviError {
location: Location
url: string
match: MappingMatch
Expand All @@ -29,7 +42,18 @@ export class NotFoundError extends ExtendableError {
}
}

export class UnmanagedLocationError extends ExtendableError {
export class UnresolvableError extends NaviError {
details: any

constructor(details: any) {
super(`Some parts of your app couldn't be loaded.`)

this.details = details
this.name = 'UnresolvableError'
}
}

export class UnmanagedLocationError extends NaviError {
location: Location

constructor(location: Location) {
Expand Down
31 changes: 14 additions & 17 deletions packages/junctions/src/HistoryRoutingObservable.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { History } from 'history'
import { UnmanagedLocationError } from './Errors'
import { Location, createURL } from './Location'
import { RouteType } from './Route'
import { Router } from './Router'
Expand All @@ -23,6 +24,7 @@ export class HistoryRoutingObservable<Context> implements Observable<RoutingStat
readonly history: History
readonly router: Router<Context>

private error?: any
private waitUntilSteadyDeferred?: Deferred<RoutingState>
private observers: Observer<RoutingState>[]
private lastLocation: Location
Expand Down Expand Up @@ -52,7 +54,10 @@ export class HistoryRoutingObservable<Context> implements Observable<RoutingStat
* content is loaded before making the first render.
*/
async getSteadyState(): Promise<RoutingState> {
if (this.lastState.isSteady) {
if (this.error) {
return Promise.reject(this.error)
}
else if (this.lastState.isSteady) {
return Promise.resolve(this.lastState)
}
else if (!this.waitUntilSteadyDeferred) {
Expand Down Expand Up @@ -92,39 +97,31 @@ export class HistoryRoutingObservable<Context> implements Observable<RoutingStat

// The router only looks at path and search, so if they haven't
// changed, there's no point recreating the observable.
if (!(pathHasChanged || searchHasChanged || force)) {
if (!force && !(pathHasChanged || searchHasChanged) && !this.lastState.error) {
this.update({
location,
})
return
}

this.lastLocation = location

if (this.observableSubscription) {
this.observableSubscription.unsubscribe()
}

try {
this.routingObservable = this.router.observable(location, { withContent: true })
let observable = this.router.observable(location, { withContent: true })
if (observable) {
this.routingObservable = observable
this.observableSubscription = this.routingObservable.subscribe(this.handleRouteChange)
this.update({
location,
state: this.routingObservable.getState(),
})
}
catch (e) {
if (this.waitUntilSteadyDeferred) {
this.waitUntilSteadyDeferred.reject(e)
delete this.waitUntilSteadyDeferred
}
for (let i = 0; i < this.observers.length; i++) {
let observer = this.observers[i]
if (observer.error) {
observer.error(e)
}
}
else if (!this.lastLocation) {
throw new UnmanagedLocationError(location)
}

this.lastLocation = location
}

private handleRouteChange = (state: RoutingState) => {
Expand Down
9 changes: 4 additions & 5 deletions packages/junctions/src/MemoryNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { createMemoryHistory, History } from 'history';
import { Navigation, NavigationState } from './Navigation'
import { Router } from './Router'
import { Router, RouterOptions, createRouter } from './Router'
import { RoutingState } from './RoutingState'
import { Observer, SimpleSubscription, createOrPassthroughObserver } from './Observable'
import { HistoryRoutingObservable, createHistoryRoutingObservable } from './HistoryRoutingObservable';


type MemoryNavigationOptions<Context> = {
url: string,
router: Router<Context>
interface MemoryNavigationOptions<Context> extends RouterOptions<Context> {
url: string
}


Expand All @@ -27,7 +26,7 @@ export class MemoryNavigation<Context> implements Navigation {
this.history = createMemoryHistory({
initialEntries: [options.url],
})
this.router = options.router
this.router = createRouter(options)
this.historyRoutingObservable = createHistoryRoutingObservable({
history: this.history,
router: this.router,
Expand Down
36 changes: 32 additions & 4 deletions packages/junctions/src/Resolver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { RouterEnv } from './RouterEnv';
import { RouterEvent } from './Router';
import { UnresolvableError } from './Errors';

export type Resolvable<
T,
Context=any
> = (env: RouterEnv<Context>) => PromiseLike<T> | T
> = (env: RouterEnv<Context>) => PromiseLike<{ default: T } | T> | T

export enum ResolverStatus {
Ready = 'Ready',
Expand Down Expand Up @@ -108,7 +109,7 @@ export class Resolver<Context=any> {
this.results.set(resolvable, {
id: currentResult.id,
status: ResolverStatus.Ready,
value,
value: extractDefault(value),
})
return true
}
Expand All @@ -119,8 +120,22 @@ export class Resolver<Context=any> {
this.results.set(resolvable, {
id: currentResult.id,
status: ResolverStatus.Error,
error,
error: new UnresolvableError(error),
})

// Remove errors from the cache in a short delay that
// accounts for them being used
// TODO: use requestIdleCallback instead
setTimeout(() => {
let result = this.results.get(resolvable)
if (result && currentResult && result.id === currentResult.id && result.error) {
// No need to notify any subscribers that the
// cache has been purged, as we don't want to
// cause a re-render.
this.results.delete(resolvable)
}
})

return true
}
}
Expand Down Expand Up @@ -150,6 +165,19 @@ export class Resolver<Context=any> {

// Not all promise libraries use the ES6 `Promise` constructor,
// so there isn't a better way to check if it's a promise :-(
function isPromiseLike<T>(x: PromiseLike<T> | T): x is PromiseLike<T> {
function isPromiseLike<T>(x: PromiseLike<{ default: T } | T> | T): x is PromiseLike<{ default: T } | T> {
return !!x && !!(x['then'])
}

function extractDefault<T>(value: { default: T } | T): T {
if (hasDefault(value)) {
return value.default
}
else {
return value
}
}

function hasDefault<T>(value: { default: T } | T): value is { default: T } {
return 'default' in (value as any)
}
17 changes: 10 additions & 7 deletions packages/junctions/src/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface RouterEvent {


export interface RouterOptions<Context> {
junction: Junction,
rootJunction: Junction,
rootPath?: string,
initialContext?: Context,
onEvent?: (event: RouterEvent) => void,
Expand Down Expand Up @@ -58,8 +58,14 @@ export class Router<Context=any> {
private rootMapping: AbsoluteMapping

constructor(options: RouterOptions<Context>) {
this.rootMapping = createRootMapping(options.junction, options.rootPath)
this.rootJunction = options.junction
if (process.env.NODE_ENV !== "production") {
if (!options.rootJunction || options.rootJunction.type !== RouteType.Junction) {
throw new Error(`Expected to receive a Junction object for the "junction" prop, but instead received "${options.rootJunction}".`)
}
}

this.rootMapping = createRootMapping(options.rootJunction, options.rootPath)
this.rootJunction = options.rootJunction
this.resolver = new Resolver(
new RouterEnv(options.initialContext || {} as any, this),
options.onEvent || (() => {}),
Expand All @@ -70,7 +76,7 @@ export class Router<Context=any> {
this.resolver.setEnv(new RouterEnv(context || {}, this))
}

observable(locationOrURL: Location | string, options: RouterLocationOptions = {}): RoutingObservable {
observable(locationOrURL: Location | string, options: RouterLocationOptions = {}): RoutingObservable | undefined {
// need to somehow keep track of which promises in the resolver correspond to which observables,
// so that I don't end up updating observables which haven't actually changed.
let location = typeof locationOrURL === 'string' ? parseLocationString(locationOrURL) : locationOrURL
Expand All @@ -84,9 +90,6 @@ export class Router<Context=any> {
options
)
}
else {
throw new UnmanagedLocationError(location)
}
}

mapObservable(locationOrURL: Location | string, options: RouterMapOptions = {}): RoutingMapObservable {
Expand Down
2 changes: 1 addition & 1 deletion packages/junctions/src/RoutingState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function createRoutingState(location: Location, topRoute: JunctionRoute):
firstRoute: routes[0] as JunctionRoute,
lastRoute,
isSteady: isRouteSteady(routes[0]),
error: lastRoute && lastRoute.error,
error: lastRoute && (lastRoute.error || lastRoute.contentError),
status,
}
}
1 change: 1 addition & 0 deletions packages/junctions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { HistoryRoutingObservable, createHistoryRoutingObservable } from './Hist
export { createPage, Page } from './Page'
export { createRedirect, Redirect } from './Redirect'
export * from './Route'
export * from './Errors'
export { Router, createRouter } from './Router'
export { RoutingState } from './RoutingState'
export { MaybeResolvableNode, Node } from './Node'
Expand Down
4 changes: 4 additions & 0 deletions packages/react-navi/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ import uglify from 'rollup-plugin-uglify'
const env = process.env.NODE_ENV
const config = {
external: [
'junctions',
'history',
'react',
'react-dom',
],
globals: {
'junctions': 'Junctions',
'history': 'History',
'react': 'React',
'react-dom': 'ReactDOM',
},
Expand Down
Loading

0 comments on commit 2ca5814

Please sign in to comment.