Merge branch 'master' into refactor-phase1

This commit is contained in:
Juan Picado @jotadeveloper 2018-06-04 22:31:00 +02:00 committed by GitHub
commit 4f4cd879a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 488 additions and 1181 deletions

View File

@ -22,7 +22,8 @@
"react-hot-loader/babel", "react-hot-loader/babel",
"transform-runtime", "transform-runtime",
"transform-object-rest-spread", "transform-object-rest-spread",
"transform-decorators-legacy" "transform-decorators-legacy",
"syntax-dynamic-import"
] ]
}, },
"test": { "test": {

5
.gitignore vendored
View File

@ -28,8 +28,5 @@ bundle.js.map
__tests__ __tests__
# Compiled script # Compiled script
static/index.html static/*
static/style.*
static/*.js
static/logo.*

View File

@ -62,7 +62,7 @@ Copy the [existing configuration](https://github.com/verdaccio/verdaccio/blob/ma
and adapt it for your use case: and adapt it for your use case:
```bash ```bash
wget https://github.com/verdaccio/verdaccio/blob/master/conf/full.yaml -O config.yaml wget https://raw.githubusercontent.com/verdaccio/verdaccio/master/conf/full.yaml -O config.yaml
``` ```
**Note:** Make sure you are using the right path for the storage that is used for **Note:** Make sure you are using the right path for the storage that is used for

View File

@ -59,6 +59,7 @@
"babel-jest": "22.4.3", "babel-jest": "22.4.3",
"babel-loader": "7.1.4", "babel-loader": "7.1.4",
"babel-plugin-flow-runtime": "0.17.0", "babel-plugin-flow-runtime": "0.17.0",
"babel-plugin-syntax-dynamic-import": "6.18.0",
"babel-plugin-transform-async-to-generator": "6.24.1", "babel-plugin-transform-async-to-generator": "6.24.1",
"babel-plugin-transform-class-properties": "6.24.1", "babel-plugin-transform-class-properties": "6.24.1",
"babel-plugin-transform-decorators-legacy": "1.3.4", "babel-plugin-transform-decorators-legacy": "1.3.4",
@ -113,7 +114,7 @@
"puppeteer": "1.1.1", "puppeteer": "1.1.1",
"react": "16.2.0", "react": "16.2.0",
"react-dom": "16.2.0", "react-dom": "16.2.0",
"react-hot-loader": "4.0.0", "react-hot-loader": "4.2.0",
"react-router-dom": "4.2.2", "react-router-dom": "4.2.2",
"react-syntax-highlighter": "5.8.0", "react-syntax-highlighter": "5.8.0",
"rimraf": "2.6.2", "rimraf": "2.6.2",
@ -129,8 +130,9 @@
"url-loader": "0.6.2", "url-loader": "0.6.2",
"verdaccio-auth-memory": "0.0.4", "verdaccio-auth-memory": "0.0.4",
"verdaccio-memory": "1.0.1", "verdaccio-memory": "1.0.1",
"webpack": "4.8.3", "webpack": "4.10.2",
"webpack-cli": "2.0.15", "webpack-bundle-analyzer": "2.13.1",
"webpack-cli": "3.0.1",
"webpack-dev-server": "3.1.4", "webpack-dev-server": "3.1.4",
"webpack-merge": "4.1.2", "webpack-merge": "4.1.2",
"whatwg-fetch": "2.0.3" "whatwg-fetch": "2.0.3"

View File

@ -43,8 +43,7 @@ module.exports = function(config, auth, storage) {
const defaultTitle = 'Verdaccio'; const defaultTitle = 'Verdaccio';
let webPage = template let webPage = template
.replace(/ToReplaceByVerdaccio/g, base) .replace(/ToReplaceByVerdaccio/g, base)
.replace(/ToReplaceByTitle/g, _.get(config, 'web.title') ? config.web.title : defaultTitle) .replace(/ToReplaceByTitle/g, _.get(config, 'web.title') ? config.web.title : defaultTitle);
.replace(/(main.*\.js|style.*\.css)/g, `${base}/-/static/$1`);
res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Type', 'text/html');

View File

@ -382,7 +382,11 @@ class Storage implements IStorageHandler {
const latest = info[DIST_TAGS].latest; const latest = info[DIST_TAGS].latest;
if (latest && info.versions[latest]) { if (latest && info.versions[latest]) {
packages.push(info.versions[latest]); const version = info.versions[latest];
const time = info.time[latest];
version.time = time;
packages.push(version);
} else { } else {
self.logger.warn( {package: locals[itemPkg]}, 'package @{package} does not have a "latest" tag?' ); self.logger.warn( {package: locals[itemPkg]}, 'package @{package} does not have a "latest" tag?' );
} }

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import {Tag} from 'element-react'; import {Tag} from 'element-react';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import isNil from 'lodash/isNil'; import isNil from 'lodash/isNil';
import {formatDateDistance} from '../../utils/DateUtils';
import classes from './package.scss'; import classes from './package.scss';
@ -12,24 +13,74 @@ export default class Package extends React.Component {
} }
render() { render() {
const { const {package: pkg} = this.props;
package: pkg
} = this.props;
return ( return (
<Link to={`detail/${pkg.name}`} className={classes.package}> <section className={classes.package}>
<h1>{pkg.name}<Tag type="gray">v{pkg.version}</Tag></h1> <Link to={`detail/${pkg.name}`}>
{this.renderAuthor(pkg)} <div className={classes.header}>
<p>{pkg.description}</p> {this.renderTitle(pkg)}
{this.renderAuthor(pkg)}
</div>
<div className={classes.footer}>
{this.renderDescription(pkg)}
</div>
<div className={classes.details}>
{this.renderPublished(pkg)}
{this.renderLicense(pkg)}
</div>
</Link> </Link>
</section>
); );
} }
renderPublished(pkg) {
if (pkg.time) {
return (<div className={classes.homepage}>
{`Published ${formatDateDistance(pkg.time)} ago`}
</div>);
}
return null;
}
renderLicense(pkg) {
if (pkg.license) {
return (<div className={classes.license}>
{pkg.license}
</div>);
}
return null;
}
renderDescription(pkg) {
return (
<p className={classes.description}>
{pkg.description}
</p>
);
}
renderTitle(pkg) {
return (
<div className={classes.title}>
<h1>
{pkg.name} {this.renderTag(pkg)}
</h1>
</div>
);
}
renderTag(pkg) {
return <Tag type="gray">v{pkg.version}</Tag>;
}
renderAuthor(pkg) { renderAuthor(pkg) {
if (isNil(pkg.author) || isNil(pkg.author.name)) { if (isNil(pkg.author) || isNil(pkg.author.name)) {
return; return;
} }
return <span role="author" className={classes.author}>By: {pkg.author.name}</span>; return <div role="author" className={classes.author}>{`By: ${pkg.author.name}`}</div>;
} }
} }

View File

@ -1,64 +1,90 @@
@import '../../styles/variable'; @import '../../styles/variable';
.package { .package {
display: block; .header {
position: relative; display: flex;
color: inherit; align-items: center;
margin: 0; margin: 10px 0 0;
padding: 10px 0; }
cursor: pointer;
text-decoration: none;
h1 { .footer {
font-size: 18px; display: flex;
p.description {
width: 100%;
margin-bottom: 0;
font-size: 14px;
color: darkgray;
}
}
:global { .details {
.el-tag { display: flex;
margin-left: 5px; font-size: 80%;
color: $description_color;
padding-top: 5px;
.license {
width: 20%;
text-align: right;
}
.homepage {
width: 80%;
}
}
> a {
display: block;
position: relative;
color: inherit;
margin: 0;
padding: 10px 0;
cursor: pointer;
text-decoration: none;
.title {
width: 100%;
h1 {
font-size: 18px;
margin: 0;
:global {
.el-tag {
margin-left: 5px;
}
}
}
}
.author {
color: $description_color;
font-size: inherit;
width: 30%;
text-align: right;
}
&:hover {
&::before {
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(255, 255, 255, 0.7);
content: '';
}
&::after {
display: block;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
left: 0;
text-align: center;
font-size: 18px;
} }
} }
} }
// Author
.author {
position: absolute;
top: 10px;
right: 0;
color: lightgrey;
font-size: inherit;
word-wrap: break-word;
width: 100px;
text-align: right;
}
p {
margin-bottom: 0;
font-size: 14px;
color: darkgray;
}
&:hover {
&::before {
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(255, 255, 255, 0.7);
content: '';
}
&::after {
display: block;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
left: 0;
content: 'Click to view detail';
text-align: center;
font-size: 18px;
}
}
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import _ from 'lodash'; import get from 'lodash/get';
import Module from '../../Module'; import Module from '../../Module';
import classes from './style.scss'; import classes from './style.scss';
@ -13,7 +13,7 @@ export default class Dependencies extends React.Component {
}; };
get dependencies() { get dependencies() {
return _.get(this, 'props.packageMeta.latest.dependencies', {}); return get(this, 'props.packageMeta.latest.dependencies', {});
} }
render() { render() {

View File

@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import format from 'date-fns/format';
import Module from '../../Module'; import Module from '../../Module';
import classes from './style.scss'; import {formatDate} from '../../../../utils/DateUtils';
const TIMEFORMAT = 'YYYY/MM/DD, HH:mm:ss'; import classes from './style.scss';
export default class LastSync extends React.Component { export default class LastSync extends React.Component {
static propTypes = { static propTypes = {
@ -21,14 +20,14 @@ export default class LastSync extends React.Component {
} }
}); });
const time = format(new Date(lastUpdate), TIMEFORMAT); const time = formatDate(lastUpdate);
return lastUpdate ? time : ''; return lastUpdate ? time : '';
} }
get recentReleases() { get recentReleases() {
let recentReleases = Object.keys(this.props.packageMeta.time).map((version) => { let recentReleases = Object.keys(this.props.packageMeta.time).map((version) => {
const time = format(new Date(this.props.packageMeta.time[version]), TIMEFORMAT); const time = formatDate(this.props.packageMeta.time[version]);
return {version, time}; return {version, time};
}); });

View File

@ -1,6 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import _ from 'lodash'; import get from 'lodash/get';
import filter from 'lodash/filter';
import size from 'lodash/size';
import uniqBy from 'lodash/uniqBy';
import Module from '../../Module'; import Module from '../../Module';
import classes from './style.scss'; import classes from './style.scss';
@ -19,27 +22,29 @@ export default class Maintainers extends React.Component {
} }
get author() { get author() {
return _.get(this, 'props.packageMeta.latest.author'); return get(this, 'props.packageMeta.latest.author');
} }
get contributors() { get contributors() {
let contributors = _.get(this, 'props.packageMeta.latest.contributors', {}); let contributors = get(this, 'props.packageMeta.latest.contributors', {});
return _.filter(contributors, (contributor) => { return filter(contributors, (contributor) => {
return ( return (
contributor.name !== _.get(this, 'author.name') && contributor.name !== get(this, 'author.name') &&
contributor.email !== _.get(this, 'author.email') contributor.email !== get(this, 'author.email')
); );
}); });
} }
get showAllContributors() { get showAllContributors() {
return this.state.showAllContributors || _.size(this.contributors) <= 5; return this.state.showAllContributors || size(this.contributors) <= 5;
} }
get uniqueContributors() { get uniqueContributors() {
if (!this.contributors) return []; if (!this.contributors) {
return [];
}
return _.uniqBy(this.contributors, (contributor) => contributor.name).slice(0, 5); return uniqBy(this.contributors, (contributor) => contributor.name).slice(0, 5);
} }
handleShowAllContributors() { handleShowAllContributors() {
@ -53,7 +58,11 @@ export default class Maintainers extends React.Component {
return (this.showAllContributors ? this.contributors : this.uniqueContributors) return (this.showAllContributors ? this.contributors : this.uniqueContributors)
.map((contributor, index) => { .map((contributor, index) => {
return <MaintainerInfo key={index} title="Contributors" name={contributor.name} avatar={contributor.avatar}/>; return <MaintainerInfo
key={index}
title="Contributors"
name={contributor.name}
avatar={contributor.avatar}/>;
}); });
} }

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import {HashRouter as Router, Route, Switch} from 'react-router-dom'; import {HashRouter as Router, Route, Switch} from 'react-router-dom';
import {asyncComponent} from './utils/asyncComponent';
import Header from './components/Header'; import Header from './components/Header';
import Home from './modules/home';
import Detail from './modules/detail';
import Footer from './components/Footer'; import Footer from './components/Footer';
const DetailPackage = asyncComponent(() => import('./modules/detail'));
const HomePage = asyncComponent(() => import('./modules/home'));
const RouterApp = () => { const RouterApp = () => {
return ( return (
<Router> <Router>
@ -13,9 +15,9 @@ const RouterApp = () => {
<Header/> <Header/>
<div className="container"> <div className="container">
<Switch> <Switch>
<Route exact path="/(search/:keyword)?" component={ Home } /> <Route exact path="/(search/:keyword)?" component={ HomePage } />
<Route exact path="/detail/@:scope/:package" component={Detail} /> <Route exact path="/detail/@:scope/:package" component={DetailPackage} />
<Route exact path="/detail/:package" component={Detail} /> <Route exact path="/detail/:package" component={DetailPackage} />
</Switch> </Switch>
</div> </div>
<Footer/> <Footer/>

View File

@ -1,10 +1,19 @@
/* Variables */ /* Variables */
$break-small: 800px;
$break-large: 1240px;
$description_color: lightgrey;
@mixin container-size { @mixin container-size {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
width: 100%; width: 100%;
min-width: 400px; min-width: 400px;
max-width: 1140px; max-width: $break-small;
@media screen and (min-width: $break-large) {
max-width: $break-large;
}
} }
$space-lg: 30px; $space-lg: 30px;

View File

@ -0,0 +1,12 @@
export const TIMEFORMAT = 'YYYY/MM/DD, HH:mm:ss';
import format from 'date-fns/format';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
export function formatDate(lastUpdate) {
return format(new Date(lastUpdate), TIMEFORMAT);
}
export function formatDateDistance(lastUpdate) {
return distanceInWordsToNow(new Date(lastUpdate));
}

View File

@ -0,0 +1,24 @@
import React from 'react';
export function asyncComponent(getComponent) {
return class AsyncComponent extends React.Component {
static Component = null;
state = {Component: AsyncComponent.Component};
componentWillMount() {
if (!this.state.Component) {
getComponent().then(({default: Component}) => {
AsyncComponent.Component = Component;
this.setState({Component});
});
}
}
render() {
const {Component} = this.state;
if (Component) {
return <Component {...this.props} />;
}
return null;
}
};
}

View File

@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Package /> component should load the component 1`] = `"<a class=\\"package\\" href=\\"detail/verdaccio\\"><h1>verdaccio<span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1><span role=\\"author\\" class=\\"author\\">By: Sam</span><p>Private NPM repository</p></a>"`; exports[`<Package /> component should load the component 1`] = `"<section class=\\"package\\"><a href=\\"detail/verdaccio\\"><div class=\\"header\\"><div class=\\"title\\"><h1>verdaccio <span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Sam</div></div><div class=\\"footer\\"><p class=\\"description\\">Private NPM repository</p></div><div class=\\"details\\"><div class=\\"homepage\\">Published about 1 month ago</div><div class=\\"license\\">MIT</div></div></a></section>"`;

View File

@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PackageList /> component should load the component with packages 1`] = `"<div class=\\"package-list-items\\"><div class=\\"pkgContainer\\"><h1 class=\\"listTitle\\">Available Packages</h1><li><a class=\\"package\\" href=\\"detail/verdaccio\\"><h1>verdaccio<span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1><span role=\\"author\\" class=\\"author\\">By: Sam</span><p>Private NPM repository</p></a></li><li><a class=\\"package\\" href=\\"detail/abc\\"><h1>abc<span class=\\"el-tag el-tag--gray\\">v1.0.1</span></h1><span role=\\"author\\" class=\\"author\\">By: Rose</span><p>abc description</p></a></li><li><a class=\\"package\\" href=\\"detail/xyz\\"><h1>xyz<span class=\\"el-tag el-tag--gray\\">v1.1.0</span></h1><span role=\\"author\\" class=\\"author\\">By: Martin</span><p>xyz description</p></a></li></div></div>"`; exports[`<PackageList /> component should load the component with packages 1`] = `"<div class=\\"package-list-items\\"><div class=\\"pkgContainer\\"><h1 class=\\"listTitle\\">Available Packages</h1><li><section class=\\"package\\"><a href=\\"detail/verdaccio\\"><div class=\\"header\\"><div class=\\"title\\"><h1>verdaccio <span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Sam</div></div><div class=\\"footer\\"><p class=\\"description\\">Private NPM repository</p></div><div class=\\"details\\"></div></a></section></li><li><section class=\\"package\\"><a href=\\"detail/abc\\"><div class=\\"header\\"><div class=\\"title\\"><h1>abc <span class=\\"el-tag el-tag--gray\\">v1.0.1</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Rose</div></div><div class=\\"footer\\"><p class=\\"description\\">abc description</p></div><div class=\\"details\\"></div></a></section></li><li><section class=\\"package\\"><a href=\\"detail/xyz\\"><div class=\\"header\\"><div class=\\"title\\"><h1>xyz <span class=\\"el-tag el-tag--gray\\">v1.1.0</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Martin</div></div><div class=\\"footer\\"><p class=\\"description\\">xyz description</p></div><div class=\\"details\\"></div></a></section></li></div></div>"`;

View File

@ -12,6 +12,8 @@ describe('<Package /> component', () => {
const props = { const props = {
name: 'verdaccio', name: 'verdaccio',
version: '1.0.0', version: '1.0.0',
time: '2018-05-03T23:36:55.046Z',
license: 'MIT',
description: 'Private NPM repository', description: 'Private NPM repository',
author: { name: 'Sam' } author: { name: 'Sam' }
}; };
@ -28,15 +30,15 @@ describe('<Package /> component', () => {
// integration expectations // integration expectations
expect(wrapper.find('a').prop('href')).toEqual('detail/verdaccio'); expect(wrapper.find('a').prop('href')).toEqual('detail/verdaccio');
expect(wrapper.find('h1').text()).toEqual('verdacciov1.0.0'); expect(wrapper.find('h1').text()).toEqual('verdaccio v1.0.0');
expect(wrapper.find('.el-tag--gray').text()).toEqual('v1.0.0'); expect(wrapper.find('.el-tag--gray').text()).toEqual('v1.0.0');
expect( expect(
wrapper wrapper.find('div').filterWhere(n => n.prop('role') === 'author')
.find('span')
.filterWhere(n => n.prop('role') === 'author')
.text() .text()
).toEqual('By: Sam'); ).toEqual('By: Sam');
expect(wrapper.find('p').text()).toEqual('Private NPM repository'); expect(wrapper.find('p').text()).toEqual('Private NPM repository');
expect(wrapper.find('.homepage').text()).toMatch(/Published about/);
expect(wrapper.find('.license').text()).toMatch(/MIT/);
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
}); });

View File

@ -7,6 +7,7 @@ module.exports = {
output: { output: {
path: `${env.APP_ROOT}/static/`, path: `${env.APP_ROOT}/static/`,
filename: '[name].[hash].js', filename: '[name].[hash].js',
publicPath: '/-/static',
}, },
resolve: { resolve: {
@ -22,6 +23,22 @@ module.exports = {
}), }),
], ],
optimization: {
runtimeChunk: {
name: 'manifest',
},
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -20,
chunks: 'all',
},
},
},
},
module: { module: {
rules: [ rules: [
/* Pre loader */ /* Pre loader */

1307
yarn.lock

File diff suppressed because it is too large Load Diff