Skip to content

Commit 783b566

Browse files
committed
feat(Package): routing of /package/@:scope/:name@:version
1 parent d69412a commit 783b566

File tree

5 files changed

+107
-12
lines changed

5 files changed

+107
-12
lines changed

.eslintrc

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"react/jsx-filename-extension": "off",
88
"react/forbid-prop-types": [2, { "forbid": ["any"] }],
99
"react/no-did-mount-set-state": "off",
10-
"import/prefer-default-export": "off"
10+
"import/prefer-default-export": "off",
11+
"prefer-template": "off"
1112
}
1213
}

package-lock.json

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"react-dom": "^16.2.0",
1212
"react-router-dom": "^4.2.2",
1313
"react-scripts": "1.1.1",
14+
"semver-match": "^0.1.1",
1415
"warning": "^3.0.0"
1516
},
1617
"scripts": {

src/Routes.js

+32-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
11
import React from "react";
2-
import { HashRouter, Route, Switch } from "react-router-dom";
2+
import { HashRouter, Route, Switch, Redirect } from "react-router-dom";
33

44
import MainLayout from "./containers/MainLayout/MainLayout";
55

66
import Home from "./containers/Home/Home";
77
import Package from "./containers/Package/Package";
88

9+
/**
10+
* Compiles a render method to pass to a Route that will redirect to "latest" version
11+
* Examples:
12+
* - /package/react -> package/react@latest
13+
* - /package/@angular/core -> /package/@angular/core@latest
14+
* The Package container will handle it from their (there is always a "latest" dist tag to match)
15+
* @param {Boolean} scoped
16+
*/
17+
const compileRedirectToLatest = scoped => (
18+
{ match: { params } } // eslint-disable-line
19+
) => (
20+
<Redirect
21+
replace
22+
to={
23+
"/package" +
24+
(scoped ? "/@" + params.scope : "") +
25+
("/" + params.name + "@latest")
26+
}
27+
/>
28+
);
29+
930
const Routes = () => (
1031
<HashRouter>
1132
<MainLayout>
@@ -16,9 +37,17 @@ const Routes = () => (
1637
path="/package/@:scope/:name@:version"
1738
component={Package}
1839
/>
19-
<Route exact path="/package/@:scope/:name" component={Package} />
40+
<Route
41+
exact
42+
path="/package/@:scope/:name"
43+
render={compileRedirectToLatest(true)}
44+
/>
2045
<Route exact path="/package/:name@:version" component={Package} />
21-
<Route exact path="/package/:name" component={Package} />
46+
<Route
47+
exact
48+
path="/package/:name"
49+
render={compileRedirectToLatest(false)}
50+
/>
2251
</Switch>
2352
</MainLayout>
2453
</HashRouter>

src/containers/Package/Package.js

+64-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import React, { Component, Fragment } from "react";
22
import PropTypes from "prop-types";
3-
import { Link } from "react-router-dom";
3+
import { Link, withRouter } from "react-router-dom";
4+
import { match as matchSemver } from "semver-match";
45

56
import { formatPackageString } from "../../utils/string";
67
import { getInstance as api } from "../../services/ApiManager";
78

8-
export default class Package extends Component {
9+
class Package extends Component {
910
static propTypes = {
10-
match: PropTypes.object.isRequired
11+
match: PropTypes.object.isRequired,
12+
history: PropTypes.object.isRequired
1113
};
1214
constructor(props) {
1315
super(props);
@@ -17,9 +19,15 @@ export default class Package extends Component {
1719
state: "loading"
1820
};
1921
async componentDidMount() {
20-
const { scope, name } = this.props.match.params;
21-
this.loadInfos(scope, name);
22+
const { scope, name, version } = this.props.match.params;
23+
this.loadInfos(scope, name, version);
2224
}
25+
/**
26+
* Once the container is mounted, we have to track for changes in the router
27+
* to relaunch xhr to npm-registry, since componentDidMount won't be re-triggered
28+
* That way, we trigger this.loadInfos with fresh props from the router
29+
* @param {Object} newProps
30+
*/
2331
componentWillReceiveProps(newProps) {
2432
const { scope, name, version } = this.props.match.params;
2533
const {
@@ -28,17 +36,60 @@ export default class Package extends Component {
2836
version: newVersion
2937
} = newProps.match.params;
3038
if (scope !== newScope || name !== newName || version !== newVersion) {
31-
this.loadInfos(newScope, newName);
39+
this.loadInfos(newScope, newName, newVersion);
40+
}
41+
}
42+
/**
43+
* Redirect a final version is matched
44+
* Returns the matched versions if matched (and redirects the router).
45+
* If no need to redirect, returns false.
46+
* @param {String} scope
47+
* @param {String} name
48+
* @param {String} range
49+
* @param {Array} versions
50+
* @param {Object} distTags
51+
* @return {Boolean|String}
52+
*/
53+
redirectUntilMatchVersion(scope, name, range, versions = [], distTags = {}) {
54+
if (typeof range !== "undefined") {
55+
const matchedVersion = matchSemver(range, versions, distTags) || "latest";
56+
console.log({ range, versions, distTags, matchedVersion });
57+
if (
58+
matchedVersion &&
59+
matchedVersion !== this.props.match.params.version
60+
) {
61+
this.props.history.replace(
62+
`/package/${formatPackageString({ scope, name })}@${matchedVersion}`
63+
);
64+
}
65+
return matchedVersion;
3266
}
67+
return false;
3368
}
34-
async loadInfos(scope, name) {
69+
/**
70+
* Loads infos from the npm registry.
71+
* @param {String} scope
72+
* @param {String} name
73+
* @param {String} range
74+
*/
75+
async loadInfos(scope, name, range) {
3576
this.setState({ state: "loading" });
3677
try {
3778
const { data: packageInfos } = await api().packageInfos(
3879
formatPackageString({ scope, name })
3980
);
4081
console.log(packageInfos.name, { packageInfos });
41-
this.setState({ packageInfos, state: "loaded" });
82+
const matched = this.redirectUntilMatchVersion(
83+
scope,
84+
name,
85+
range,
86+
Object.keys(packageInfos.versions),
87+
packageInfos["dist-tags"]
88+
);
89+
if (matched) {
90+
console.log("matched", matched);
91+
this.setState({ packageInfos, state: "loaded" });
92+
}
4293
} catch (e) {
4394
console.error(e);
4495
this.setState({ packageInfos: null, state: "error" });
@@ -80,3 +131,8 @@ export default class Package extends Component {
80131
);
81132
}
82133
}
134+
135+
/**
136+
* Gives access to history/location/match in props from the router - https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md
137+
*/
138+
export default withRouter(Package);

0 commit comments

Comments
 (0)