This commit is contained in:
Mickael KERJEAN 2018-10-15 22:45:12 +11:00
commit 22d045266d
24 changed files with 313 additions and 171 deletions

View file

@ -1,22 +1,4 @@
pipeline:
test:
group: test
image: machines/nuage_build
pull: true
environment:
- GOPATH=/tmp/go
- PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/
- LD_LIBRARY_PATH=/usr/local/lib
- CGO_LDFLAGS_ALLOW='-fopenmp'
commands:
# Prepare
- mkdir -p $GOPATH/src/github.com/mickael-kerjean/nuage
- mv * $GOPATH/src/github.com/mickael-kerjean/nuage
- cd $GOPATH/src/github.com/mickael-kerjean/nuage
- cd server
- go get
- go test -v ./...
publish_docker:
group: release
image: docker
@ -28,5 +10,5 @@ pipeline:
commands:
- echo $DOCKER_PASSWORD | docker login -u=$DOCKER_USERNAME --password-stdin
- docker pull alpine:latest
- docker build --no-cache -t machines/nuage:master docker/prod
- docker build --no-cache -t machines/nuage docker/prod
- docker push machines/nuage

View file

@ -1,56 +1,118 @@
@import url('https://fonts.googleapis.com/css?family=Source+Code+Pro:400,600');
/* latin-ext */
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 400;
src: local('Source Code Pro'), local('SourceCodePro-Regular'), url(/assets/fonts/SourceCodePro-Regular-400-latin-ext.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 400;
src: local('Source Code Pro'), local('SourceCodePro-Regular'), url(/assets/fonts/SourceCodePro-Regular-400-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 600;
src: local('Source Code Pro Semibold'), local('SourceCodePro-Semibold'), url(/assets/fonts/SourceCodePro-Semibold-600-latin-ext.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 600;
src: local('Source Code Pro Semibold'), local('SourceCodePro-Semibold'), url(/assets/fonts/SourceCodePro-Semibold-600-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
--bg-color: #f1f2f2;
--color: #626469;
--emphasis: #375160;
--primary: #9AD1ED;
--emphasis-primary: #c5e2f1;
--secondary: #466372;
--emphasis-secondary: #466372;
--light: #909090;
--super-light: #f4f4f4;
--super-light: #f4f4f4;
--error: #f26d6d;
--success: #63d9b1;
--dark: #313538;
}
html {
font-family:"San Francisco","Roboto","Arial",sans-serif;
-webkit-text-size-adjust:100%;
font-family: "San Francisco", "Roboto", "Arial", sans-serif;
-webkit-text-size-adjust: 100%;
background: var(--bg-color);
color: var(--color);
}
body {overflow: hidden;}
body, html{
body {
overflow: hidden;
}
body,
html {
height: 100%;
margin: 0;
}
#main{height: 100%;}
a{color: inherit; text-decoration: none;}
select{-moz-appearance: none;}
#main {
height: 100%;
}
a {
color: inherit;
text-decoration: none;
}
select {
-moz-appearance: none;
}
select:-moz-focusring {
color: transparent;
outline: none;
border: none;
}
select::-ms-expand { display: none; }
button::-moz-focus-inner {
border: 0;
select::-ms-expand {
display: none;
}
input, textarea{
button::-moz-focus-inner {
border: 0;
}
input,
textarea {
transition: border 0.2s;
outline: none;
}
input[type="checkbox"]{position: relative; top: 1px; margin: 0; padding: 0; vertical-align: top;}
input[type="checkbox"] {
position: relative;
top: 1px;
margin: 0;
padding: 0;
vertical-align: top;
}
.no-select {
-webkit-touch-callout: none;
@ -61,74 +123,73 @@ input[type="checkbox"]{position: relative; top: 1px; margin: 0; padding: 0; vert
user-select: none;
}
button:focus,
a:focus, a:active,
a:focus,
a:active,
button::-moz-focus-inner,
input[type="reset"]::-moz-focus-inner,
input[type="button"]::-moz-focus-inner,
input[type="submit"]::-moz-focus-inner,
select::-moz-focus-inner,
input[type="file"] > input[type="button"]::-moz-focus-inner {
outline: none !important;
input[type="file"]>input[type="button"]::-moz-focus-inner {
outline: none !important;
}
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 #000;
color: transparent;
text-shadow: 0 0 0 #000;
}
.connect-form input:hover, .connect-form textarea:hover,
.connect-form input:focus, .connect-form textarea:focus{
.connect-form input:hover,
.connect-form textarea:hover,
.connect-form input:focus,
.connect-form textarea:focus {
border-color: rgb(154, 209, 237)!important;
}
.drag-drop{
.drag-drop {
z-index: 2;
}
.drag-drop.dragging > div{
background: rgba(0,0,0,0.1);
.drag-drop.dragging>div {
background: rgba(0, 0, 0, 0.1);
}
/* CONNECTION FORM */
.login-form button.active{
box-shadow: 0px 1px 5px rgba(0,0,0,0.20);
.login-form button.active {
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.20);
}
::-webkit-scrollbar {
height: 4px;
width: 4px
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, .1)
}
::-webkit-scrollbar{
height:4px;
width:4px
::-webkit-scrollbar-thumb {
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
-ms-border-radius: 2px;
-o-border-radius: 2px;
border-radius: 2px;
background: rgba(0, 0, 0, .2);
}
::-webkit-scrollbar-track{
background:rgba(0,0,0,.1)
.scroll-y {
scrollbar-3dlight-color: #7d7e94;
scrollbar-arrow-color: #c1c1d1;
scrollbar-darkshadow-color: #2d2c4d;
scrollbar-face-color: rgba(0, 0, 0, .1);
scrollbar-highlight-color: #7d7e94;
scrollbar-shadow-color: #2d2c4d;
scrollbar-track-color: rgba(0, 0, 0, .1);
}
::-webkit-scrollbar-thumb{
-webkit-border-radius:2px;
-moz-border-radius:2px;
-ms-border-radius:2px;
-o-border-radius:2px;
border-radius:2px;
background:rgba(0,0,0,.2);
}
.scroll-y{
scrollbar-3dlight-color:#7d7e94;
scrollbar-arrow-color:#c1c1d1;
scrollbar-darkshadow-color:#2d2c4d;
scrollbar-face-color:rgba(0,0,0,.1);
scrollbar-highlight-color:#7d7e94;
scrollbar-shadow-color:#2d2c4d;
scrollbar-track-color:rgba(0,0,0,.1);
}
.pointer{cursor: pointer;}
.pointer {
cursor: pointer;
}

View file

@ -0,0 +1,80 @@
import React from 'react';
import { Session } from '../model/';
import { Container, Loader } from '../components/';
import { memory } from '../helpers/';
import '../pages/error.scss';
export function LoggedInOnly(WrappedComponent){
memory.set('user::authenticated', false);
return class extends React.Component {
constructor(props){
super(props);
this.state = {
is_logged_in: memory.get('user::authenticated')
};
}
componentDidMount(){
if(this.state.is_logged_in === false){
Session.currentUser().then((res) => {
if(res.is_authenticated === false){
this.props.error({message: "Authentication Required"});
return;
}
memory.set('user::authenticated', true);
this.setState({is_logged_in: true});
}).catch((err) => {
if(err.code === "NO_INTERNET"){
this.setState({is_logged_in: true});
return;
}
this.props.error(err);
});
}
}
render(){
if(this.state.is_logged_in === true){
return <WrappedComponent {...this.props} />;
}
return null;
}
}
}
export function ErrorPage(WrappedComponent){
return class extends React.Component {
constructor(props){
super(props);
this.state = {
error: null
};
}
update(obj){
this.setState({error: obj});
}
render(){
if(this.state.error !== null){
const message = this.state.error.message || "There is nothing in here";
return (
<Container>
<div className="error-page">
<h1>Oops!</h1>
<h2>{message}</h2>
<p>{JSON.stringify(this.state.error)}</p>
</div>
</Container>
);
}
return (
<WrappedComponent error={this.update.bind(this)} {...this.props} />
);
}
}
}

View file

@ -19,6 +19,7 @@ export { Audio } from './audio';
export { Video } from './video';
export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown';
export { MapShot } from './mapshot';
export { LoggedInOnly, ErrorPage } from './decorator';
//export { Connect } from './connect';
// Those are commented because they will delivered as a separate chunk
// export { Editor } from './editor';

View file

@ -108,7 +108,6 @@ function handle_error_response(xhr, err){
}else if(xhr.status === 500){
err({message: message || "Oups something went wrong with our servers", code: "INTERNAL_SERVER_ERROR"});
}else if(xhr.status === 401){
if(location.pathname !== '/login'){ location.pathname = "/login"; }
err({message: message || "Authentication error", code: "Unauthorized"});
}else if(xhr.status === 403){
err({message: message || "You can\'t do that", code: "Forbidden"});

View file

@ -3,11 +3,10 @@ function Memory(){
return {
get: function(key){
if(!data[key]) return null;
if(data[key] === undefined) return null;
return data[key];
},
set: function(key, value){
if(!data[key]) data[key] = {};
data[key] = value;
},
all: function(){

View file

@ -4,19 +4,15 @@ import Router from './router';
import './assets/css/reset.scss';
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/assets/worker/cache.js').catch(function(error) {
console.log('ServiceWorker registration failed:', error);
});
}
window.onload = () => {
ReactDOM.render(<Router/>, document.getElementById('main'));
};
window.log = function(){console.log.apply(this, arguments)};
window.addEventListener("DOMContentLoaded", () => {
const className = 'ontouchstart' in window ? 'touch-yes' : 'touch-no';
document.body.classList.add(className);
ReactDOM.render(<Router/>, document.getElementById('main'));
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/assets/worker/cache.js').catch(function(error) {
console.log('ServiceWorker registration failed:', error);
});
}
});

View file

@ -1,7 +1,7 @@
import { http_get, http_post, http_delete } from '../helpers/';
class SessionManager{
isLoggedIn(){
currentUser(){
let url = '/api/session'
return http_get(url)
.then(data => data.result);

View file

@ -45,8 +45,10 @@ export class ConnectPage extends React.Component {
authenticate(params){
this.setState({loading: true});
Session.authenticate(params)
.then((path) => {
.then(Session.currentUser)
.then((user) => {
let url = '/files/';
let path = user.home
if(path){
path = path.replace(/^\/?(.*?)\/?$/, "$1");
if(path !== ""){

View file

@ -2,6 +2,7 @@
width: 80%;
max-width: 600px;
margin: 50px auto 0 auto;
flex-direction: column;
h1{margin: 5px 0; font-size: 3.1em;}
h2{margin: 10px 0; font-weight: normal;}

View file

@ -6,14 +6,23 @@ import './filespage.scss';
import './error.scss';
import { Files } from '../model/';
import { sort, onCreate, onRename, onDelete, onUpload, onSearch } from './filespage.helper';
import { NgIf, Loader, EventReceiver } from '../components/';
import { NgIf, Loader, EventReceiver, LoggedInOnly, ErrorPage } from '../components/';
import { notify, debounce, goToFiles, goToViewer, event, settings_get, settings_put } from '../helpers/';
import { BreadCrumb, FileSystem, FrequentlyAccess, Submenu } from './filespage/';
import InfiniteScroll from 'react-infinite-scroller';
const PAGE_NUMBER_INIT = 3;
const LOAD_PER_SCROLL = 24;
const PAGE_NUMBER_INIT = 2;
const LOAD_PER_SCROLL = 48;
// usefull when user press the back button while keeping the current context
let LAST_PAGE_PARAMS = {
path: null,
scroll: 0,
page_number: PAGE_NUMBER_INIT
};
@ErrorPage
@LoggedInOnly
@EventReceiver
@DragDropContext(('ontouchstart' in window)? HTML5Backend : HTML5Backend)
export class FilesPage extends React.Component {
@ -30,8 +39,7 @@ export class FilesPage extends React.Component {
metadata: null,
frequents: [],
page_number: PAGE_NUMBER_INIT,
loading: true,
error: null
loading: true
};
this.goToFiles = goToFiles.bind(null, this.props.history);
@ -50,7 +58,6 @@ export class FilesPage extends React.Component {
this.props.subscribe('file.delete', onDelete.bind(this));
this.props.subscribe('file.refresh', this.onRefresh.bind(this));
window.addEventListener('keydown', this.toggleHiddenFilesVisibilityonCtrlK);
this.hideError();
}
componentWillUnmount() {
@ -61,6 +68,10 @@ export class FilesPage extends React.Component {
this.props.unsubscribe('file.refresh');
window.removeEventListener('keydown', this.toggleHiddenFilesVisibilityonCtrlK);
this._cleanupListeners();
LAST_PAGE_PARAMS.path = this.state.path;
LAST_PAGE_PARAMS.scroll = this.refs.$scroll.scrollTop;
LAST_PAGE_PARAMS.page_number = this.state.page_number;
}
componentWillReceiveProps(nextProps){
@ -76,10 +87,6 @@ export class FilesPage extends React.Component {
}
}
hideError(){
this.setState({error: null});
}
toggleHiddenFilesVisibilityonCtrlK(e){
if(e.keyCode === 72 && e.ctrlKey === true){
e.preventDefault();
@ -112,16 +119,24 @@ export class FilesPage extends React.Component {
metadata: res.metadata,
files: sort(files, this.state.sort),
loading: false,
page_number: PAGE_NUMBER_INIT
page_number: function(){
if(this.state.path === LAST_PAGE_PARAMS.path){
return LAST_PAGE_PARAMS.page_number;
}
return PAGE_NUMBER_INIT;
}.bind(this)()
}, () => {
if(this.state.path === LAST_PAGE_PARAMS.path){
this.refs.$scroll.scrollTop = LAST_PAGE_PARAMS.scroll;
}
});
}else{
notify.send(res, 'error');
}
}, (error) => {
this.setState({error: error});
this.props.error(error);
});
this.observers.push(observer);
this.setState({error: null});
if(path === "/"){
Files.frequents().then((s) => this.setState({frequents: s}));
}
@ -210,10 +225,10 @@ export class FilesPage extends React.Component {
<div className="component_page_filespage">
<BreadCrumb className="breadcrumb" path={this.state.path} />
<div className="page_container">
<div className="scroll-y">
<div ref="$scroll" className="scroll-y">
<InfiniteScroll pageStart={0} loader={$moreLoading} hasMore={this.state.files.length > 70}
initialLoad={false} useWindow={false} loadMore={this.loadMore.bind(this)} threshold={100}>
<NgIf className="container" cond={this.state.loading === false && this.state.error === null}>
<NgIf className="container" cond={!this.state.loading}>
<NgIf cond={this.state.path === '/'}>
<FrequentlyAccess files={this.state.frequents}/>
</NgIf>
@ -225,14 +240,9 @@ export class FilesPage extends React.Component {
</NgIf>
</NgIf>
</InfiniteScroll>
<NgIf cond={this.state.loading && this.state.error === null}>
<NgIf cond={!!this.state.loading}>
<Loader/>
</NgIf>
<NgIf cond={this.state.error !== null} className="error-page">
<h1>Oops!</h1>
<h2>It seems this directory doesn't exist</h2>
<p>{JSON.stringify(this.state.error)}</p>
</NgIf>
</div>
</div>
</div>

View file

@ -408,7 +408,7 @@ class LazyLoadImage extends React.Component {
error: false
};
this.$scroll = document.querySelector(props.scroller);
this.onScroll = debounce(this.onScroll.bind(this), 100);
this.onScroll = debounce(this.onScroll.bind(this), 250);
}
componentDidMount(){

View file

@ -13,10 +13,14 @@ export class HomePage extends React.Component {
}
componentDidMount(){
Session.isLoggedIn()
Session.currentUser()
.then((res) => {
if(res === true){
this.setState({redirection: "/files"});
if(res && res.is_authenticated === true){
let url = "/files"
if(res.home){
url += res.home
}
this.setState({redirection: url});
}else{
this.setState({redirection: "/login"});
}

View file

@ -4,7 +4,7 @@ import Path from 'path';
import './viewerpage.scss';
import './error.scss';
import { Files } from '../model/';
import { BreadCrumb, Bundle, NgIf, Loader, Container, EventReceiver, EventEmitter } from '../components/';
import { BreadCrumb, Bundle, NgIf, Loader, Container, EventReceiver, EventEmitter, LoggedInOnly , ErrorPage } from '../components/';
import { debounce, opener, notify } from '../helpers/';
import { AudioPlayer, FileDownloader, ImageViewer, PDFViewer } from './viewerpage/';
@ -19,6 +19,8 @@ const IDE = (props) => (
</Bundle>
);
@ErrorPage
@LoggedInOnly
@EventReceiver
export class ViewerPage extends React.Component {
constructor(props){
@ -31,8 +33,7 @@ export class ViewerPage extends React.Component {
content: null,
needSaving: false,
isSaving: false,
loading: true,
error: null
loading: true
};
this.props.subscribe('file.select', this.onPathUpdate.bind(this));
}
@ -41,10 +42,10 @@ export class ViewerPage extends React.Component {
this.setState({
path: props.match.url.replace('/view', '') + (location.hash || ""),
filename: Path.basename(props.match.url.replace('/view', '')) || 'untitled.dat'
}, () => { this.componentWillMount(); });
}, () => { this.componentDidMount(); });
}
componentWillMount(){
componentDidMount(){
const metadata = () => {
return new Promise((done, err) => {
let app_opener = opener(this.state.path);
@ -54,7 +55,7 @@ export class ViewerPage extends React.Component {
opener: app_opener
}, () => done(app_opener));
}).catch(error => {
notify.send(err, 'error');
this.props.error(error);
err(error);
});
});
@ -67,15 +68,14 @@ export class ViewerPage extends React.Component {
if(err && err.code === 'BINARY_FILE'){
this.setState({opener: 'download', loading: false});
}else{
this.setState({error: err});
this.props.error(err);
}
});
}else{
this.setState({loading: false});
return;
}
this.setState({loading: false});
};
return metadata()
.then(data_fetch);
return metadata().then(data_fetch);
}
componentWillUnmount() {
@ -106,7 +106,7 @@ export class ViewerPage extends React.Component {
if(err && err.code === 'CANCELLED'){ return; }
this.setState({isSaving: false});
notify.send(err, 'error');
return Promise.reject();
return Promise.reject(err);
});
}
@ -125,7 +125,7 @@ export class ViewerPage extends React.Component {
<div className="component_page_viewerpage">
<BreadCrumb needSaving={this.state.needSaving} className="breadcrumb" path={this.state.path} />
<div className="page_container">
<NgIf cond={this.state.loading === false && this.state.error === null}>
<NgIf cond={this.state.loading === false}>
<NgIf cond={this.state.opener === 'editor'}>
<IDE needSavingUpdate={this.needSaving.bind(this)}
needSaving={this.state.needSaving}
@ -151,14 +151,9 @@ export class ViewerPage extends React.Component {
<FileDownloader data={this.state.url} filename={this.state.filename} />
</NgIf>
</NgIf>
<NgIf cond={this.state.loading === true && this.state.error === null}>
<NgIf cond={this.state.loading === true}>
<Loader/>
</NgIf>
<NgIf cond={this.state.error !== null} className="error-page">
<h1>Oops!</h1>
<h2>There is nothing in here</h2>
<p>{JSON.stringify(this.state.error)}</p>
</NgIf>
</div>
</div>
);

View file

@ -19,6 +19,9 @@ func NewBool(t bool) *bool {
}
func NewString(t string) *string {
if t == "" {
return nil
}
return &t
}

View file

@ -56,6 +56,13 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) {
}
func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
http.SetCookie(res, &http.Cookie{
Name: "download",
Value: "",
MaxAge: -1,
Path: "/",
})
path, err := pathBuilder(ctx, req.URL.Query().Get("path"))
if err != nil {
SendErrorResult(res, err)
@ -71,13 +78,6 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) {
return
}
http.SetCookie(res, &http.Cookie{
Name: "download",
Value: "",
MaxAge: -1,
Path: "/",
})
file, err = services.ProcessFileBeforeSend(file, &ctx, req, &res)
if err != nil {
SendErrorResult(res, err)

View file

@ -8,21 +8,28 @@ import (
"time"
)
func SessionIsValid(ctx App, res http.ResponseWriter, req *http.Request) {
type Session struct {
Home *string `json:"home,omitempty"`
IsAuth bool `json:"is_authenticated"`
}
func SessionGet(ctx App, res http.ResponseWriter, req *http.Request) {
r := Session {
IsAuth: false,
}
if ctx.Backend == nil {
SendSuccessResult(res, false)
SendSuccessResult(res, r)
return
}
if _, err := ctx.Backend.Ls("/"); err != nil {
SendSuccessResult(res, false)
home, err := model.GetHome(ctx.Backend)
if err != nil {
SendSuccessResult(res, r)
return
}
home, _ := model.GetHome(ctx.Backend)
if home == "" {
SendSuccessResult(res, true)
return
}
SendSuccessResult(res, true)
r.IsAuth = true
r.Home = NewString(home)
SendSuccessResult(res, r)
}
func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) {

View file

@ -5,7 +5,6 @@ import (
. "github.com/mickael-kerjean/nuage/server/common"
"github.com/mickael-kerjean/nuage/server/model"
"net/http"
"log"
)
func ShareList(ctx App, res http.ResponseWriter, req *http.Request) {
@ -29,7 +28,6 @@ func ShareGet(ctx App, res http.ResponseWriter, req *http.Request) {
func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) {
s := extractParams(req, &ctx)
log.Println("EXPIRE::", s.Expire, ctx.Body["expire"])
s.Path = NewStringFromInterface(ctx.Body["path"])
if err := model.ShareUpsert(&s); err != nil {

View file

@ -14,7 +14,7 @@ func Init(a *App) *http.Server {
// API
session := r.PathPrefix("/api/session").Subrouter()
session.HandleFunc("", APIHandler(SessionIsValid, *a)).Methods("GET")
session.HandleFunc("", APIHandler(SessionGet, *a)).Methods("GET")
session.HandleFunc("", APIHandler(SessionAuthenticate, *a)).Methods("POST")
session.HandleFunc("", APIHandler(SessionLogout, *a)).Methods("DELETE")
session.Handle("/auth/{service}", APIHandler(SessionOAuthBackend, *a)).Methods("GET")

View file

@ -19,8 +19,7 @@ let config = {
chunkFilename: "assets/js/chunk_[name]_[id]_[chunkhash].js"
},
module: {
rules: [
{
rules: [{
test: path.join(__dirname, 'client'),
use: ['babel-loader'],
exclude: /node_modules/
@ -29,6 +28,10 @@ let config = {
test: /\.html$/,
loader: 'html-loader'
},
{
test: /\.woff2$/,
loader: 'woff-loader'
},
{
test: /\.scss$/,
loaders: ['style-loader', 'css-loader', 'sass-loader']
@ -55,20 +58,21 @@ let config = {
new webpack.optimize.OccurrenceOrderPlugin(),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'client', 'index.html'),
inject:true
inject: true
}),
new CopyWebpackPlugin([
{ from: 'manifest.json', to: "assets/" },
{ from: 'worker/*.js', to: "assets/" },
{ from: 'assets/logo/*' },
{ from: 'assets/icons/*' }
{ from: 'assets/icons/*' },
{ from: 'assets/fonts/*' }
], { context: path.join(__dirname, 'client') }),
//new BundleAnalyzerPlugin()
]
};
if(process.env.NODE_ENV === 'production'){
if (process.env.NODE_ENV === 'production') {
config.plugins.push(new UglifyJSPlugin({
sourceMap: false
}));
@ -79,8 +83,8 @@ if(process.env.NODE_ENV === 'production'){
threshold: 0,
minRatio: 0.8
}));
}else{
} else {
config.devtool = '#inline-source-map';
}
module.exports = config;
module.exports = config;