Merge branch 'webpack-tweaking' into 'develop'

Improve front-end performance

See merge request funkwhale/funkwhale!499
This commit is contained in:
Eliot Berriot 2018-12-20 13:50:05 +01:00
commit e1450d28b8
68 changed files with 3856 additions and 42770 deletions

1
.gitignore vendored
View File

@ -93,5 +93,6 @@ po/*.po
docs/swagger
_build
front/src/translations.json
front/src/translations/*.json
front/locales/en_US/LC_MESSAGES/app.po
*.prof

View File

@ -58,15 +58,6 @@ class RavenDSN(types.StringPreference):
field_kwargs = {"required": False}
@global_preferences_registry.register
class RavenEnabled(types.BooleanPreference):
show_in_api = True
section = raven
name = "front_enabled"
default = False
verbose_name = "Report front-end errors with Raven"
@global_preferences_registry.register
class InstanceNodeinfoEnabled(types.BooleanPreference):
show_in_api = False

View File

@ -0,0 +1,51 @@
Improved front-end performance by stripping unused dependencies, reducing bundle size
and enabling gzip compression
Enable gzip compression [manual action suggested]
-------------------------------------------------
Gzip compression will be enabled on new instances by default
and will reduce the amount of bandwidth consumed by your instance.
If you with to benefit from gzip compression on your instance,
edit your reverse proxy virtualhost file (located at ``/etc/nginx/sites-available/funkwhale.conf``) and add the following snippet
in the server block, then reload your nginx server::
server {
# ... exiting configuration
# compression settings
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/activity+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
# end of compression settings
}

View File

@ -29,6 +29,40 @@ server {
# HSTS
add_header Strict-Transport-Security "max-age=31536000";
# compression settings
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/activity+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
location / {
include /etc/nginx/funkwhale_proxy.conf;
proxy_pass http://fw/;

View File

@ -43,6 +43,41 @@ server {
root ${FUNKWHALE_FRONTEND_PATH};
# compression settings
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/activity+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
# end of compression settings
location / {
include /etc/nginx/funkwhale_proxy.conf;
# this is needed if you have file import via upload enabled

View File

@ -43,6 +43,39 @@ http {
charset utf-8;
client_max_body_size 30M;
include /etc/nginx/funkwhale_proxy.conf;
# compression settings
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/activity+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
location /front/ {
proxy_pass http://funkwhale-front/front/;

View File

@ -182,7 +182,7 @@ work. We will store the necessary files in the ``/srv/funkwhale/custom`` directo
mkdir custom
cat <<EOF > custom/settings.json
{
"additionalStylesheets": ["/custom/custom.css"]
"additionalStylesheets": ["/front/custom/custom.css"]
}
EOF
cat <<EOF > custom/custom.css
@ -194,7 +194,7 @@ work. We will store the necessary files in the ``/srv/funkwhale/custom`` directo
By executing the previous commands, you will end up with two files in your ``/srv/funkwhale/custom``
directory:
- ``settings.json`` will tell the front-end what stylesheets you want to load (``/custom/custom.css`` in this example)
- ``settings.json`` will tell the front-end what stylesheets you want to load (``/front/custom/custom.css`` in this example)
- ``custom.css`` will hold your custom CSS
The last step to make this work is to ensure both files are served by the reverse proxy.

View File

@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "scripts/i18n-compile.sh && vue-cli-service serve --port ${VUE_PORT:-8000} --host ${VUE_HOST:-0.0.0.0}",
"serve": "vue-cli-service serve --port ${VUE_PORT:-8000} --host ${VUE_HOST:-0.0.0.0}",
"build": "scripts/i18n-compile.sh && vue-cli-service build",
"lint": "vue-cli-service lint",
"i18n-extract": "scripts/i18n-extract.sh",
@ -20,8 +20,7 @@
"lodash": "^4.17.10",
"masonry-layout": "^4.2.2",
"moment": "^2.22.2",
"raven-js": "^3.26.4",
"semantic-ui-css": "^2.3.3",
"semantic-ui-css": "^2.4.1",
"showdown": "^1.8.6",
"vue": "^2.5.17",
"vue-gettext": "^2.1.0",
@ -49,7 +48,8 @@
"node-sass": "^4.9.3",
"sass-loader": "^7.1.0",
"sinon": "^6.1.5",
"vue-template-compiler": "^2.5.17"
"vue-template-compiler": "^2.5.17",
"webpack-bundle-size-analyzer": "^3.0.0"
},
"eslintConfig": {
"root": true,

View File

@ -1,3 +1,5 @@
{
"additionalStylesheets": ["/custom.css"]
"additionalStylesheets": [
"/front/custom.css"
]
}

View File

@ -1,3 +1,7 @@
#!/bin/bash -eux
locales=$(tail -n +2 src/locales.js | sed -e 's/export default //' | jq '.locales[].code' | xargs echo)
find locales -name '*.po' | xargs $(yarn bin)/gettext-compile --output src/translations.json
locales=$(tail -n +2 src/locales.js | sed -e 's/export default //' | jq '.locales[].code' | grep -v 'en_US' | xargs echo)
for locale in $locales; do
find "locales/$locale" -name '*.po' | $(yarn bin)/gettext-compile locales/$locale/LC_MESSAGES/app.po --output src/translations/$locale.json
done
# find locales -name '*.po' | xargs $(yarn bin)/gettext-compile --output src/translations.json

View File

@ -43,10 +43,6 @@
:version="version"
@show:shortcuts-modal="showShortcutsModal = !showShortcutsModal"
></app-footer>
<raven
v-if="$store.state.instance.settings.raven.front_enabled.value"
:dsn="$store.state.instance.settings.raven.front_dsn.value"
></raven>
<playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
<shortcuts-modal @update:show="showShortcutsModal = $event" :show="showShortcutsModal"></shortcuts-modal>
<GlobalEvents @keydown.h.exact="showShortcutsModal = !showShortcutsModal"/>
@ -56,18 +52,16 @@
<script>
import axios from 'axios'
import _ from 'lodash'
import _ from '@/lodash'
import {mapState} from 'vuex'
import { WebSocketBridge } from 'django-channels'
import GlobalEvents from '@/components/utils/global-events'
import translations from '@/translations'
import Sidebar from '@/components/Sidebar'
import AppFooter from '@/components/Footer'
import Raven from '@/components/Raven'
import ServiceMessages from '@/components/ServiceMessages'
import locales from './locales'
import PlaylistModal from '@/components/playlists/PlaylistModal'
import ShortcutsModal from '@/components/ShortcutsModal'
@ -76,7 +70,6 @@ export default {
components: {
Sidebar,
AppFooter,
Raven,
PlaylistModal,
ShortcutsModal,
GlobalEvents,
@ -139,7 +132,7 @@ export default {
},
autodetectLanguage () {
let userLanguage = navigator.language || navigator.userLanguage
let available = _.keys(translations)
let available = locales.locales.map(e => { return e.code })
let matching = available.filter((a) => {
return userLanguage.replace('-', '_') === a
})
@ -223,181 +216,5 @@ export default {
</script>
<style lang="scss">
// we do the import here instead in main.js
// as resolve order is not deterministric in webpack
// and we end up with CSS rules not applied,
// see https://github.com/webpack/webpack/issues/215
@import "semantic/semantic.css";
@import "style/vendor/media";
html,
body {
@include media("<desktop") {
font-size: 90%;
}
}
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.instance-chooser {
margin-top: 2em;
}
.main.pusher,
.footer {
@include media(">desktop") {
margin-left: 350px !important;
margin-top: 50px;
}
transform: none !important;
}
.main.pusher > .ui.secondary.menu {
margin-left: 0;
margin-right: 0;
border: none;
box-shadow: inset 0px -2px 0px 0px rgba(34, 36, 38, 0.15);
.ui.item {
border: none;
border-bottom-style: none;
margin-bottom: 0px;
&.active {
box-shadow: inset 0px -2px 0px 0px #000;
}
}
@include media(">tablet") {
padding: 0 2.5rem;
}
@include media(">desktop") {
position: fixed;
left: 350px;
right: 0px;
top: 0px;
z-index: 99;
}
background-color: white;
.item {
padding-top: 1.5em;
padding-bottom: 1.5em;
}
}
.service-messages {
position: fixed;
bottom: 1em;
left: 1em;
@include media(">desktop") {
left: 350px;
}
}
.main-pusher {
padding: 1.5rem 0;
}
.ui.stripe.segment,
#footer {
padding: 2em;
@include media(">tablet") {
padding: 4em;
}
}
.ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.ui.small.text.container {
max-width: 500px !important;
}
.button.icon.tiny {
padding: 0.5em !important;
}
.sidebar {
.logo {
path {
fill: white;
}
}
}
.discrete {
color: rgba(0, 0, 0, 0.87);
}
.link {
cursor: pointer;
}
.ui.really.basic.button {
&:not(:focus) {
box-shadow: none !important;
background-color: none !important;
}
}
.floated.buttons .button ~ .dropdown {
border-left: none;
}
.ui.icon.header .circular.icon {
display: flex;
justify-content: center;
}
.segment-content .button {
margin: 0.5em;
}
a {
cursor: pointer;
}
.segment.hidden {
display: none;
}
button.reset {
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
/* inherit font & color from ancestor */
color: inherit;
font: inherit;
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
line-height: normal;
/* Corrects font smoothing for webkit */
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
/* Corrects inability to style clickable `input` types in iOS */
-webkit-appearance: none;
text-align: inherit;
}
.ui.table > caption {
font-weight: bold;
padding: 0.5em;
text-align: left;
}
[role="button"] {
cursor: pointer;
}
.left.floated {
float: left;
}
.right.floated {
float: right;
}
@import "style/_main";
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -27,9 +27,9 @@
<p>{{ instance.short_description.value }}</p>
</div>
<div
v-if="instance.long_description.value"
v-if="markdown && instance.long_description.value"
class="ui middle aligned stackable text container"
v-html="$options.filters.markdown(instance.long_description.value)">
v-html="markdown.makeHtml(instance.long_description.value)">
</div>
</section>
</main>
@ -43,8 +43,17 @@ export default {
components: {
Stats
},
created() {
data () {
return {
markdown: null
}
},
created () {
this.$store.dispatch("instance/fetchSettings")
let self = this
import('showdown').then(module => {
self.markdown = new module.default.Converter()
})
},
computed: {
...mapState({

View File

@ -20,7 +20,7 @@
<div class="ui form">
<div class="ui field">
<label><translate>Change language</translate></label>
<select class="ui dropdown" v-model="$language.current">
<select class="ui dropdown" :value="$language.current" @change="updateLanguage($event.target.value)">
<option v-for="(language, key) in $language.available" :key="key" :value="key">{{ language }}</option>
</select>
</div>
@ -60,7 +60,9 @@
</template>
<script>
import Vue from "vue"
import { mapState } from "vuex"
import axios from 'axios'
export default {
props: ["version"],
@ -74,6 +76,13 @@ export default {
if (confirm) {
this.$store.commit("instance/instanceUrl", null)
}
},
updateLanguage(value) {
let self = this
import(`../translations/${value}.json`).then((response) =>{
Vue.$translations[value] = response.default[value]
self.$language.current = value
})
}
},
computed: {

View File

@ -24,7 +24,7 @@
</template>
<script>
import _ from "lodash"
import _ from "@/lodash"
export default {
props: {

View File

@ -1,40 +0,0 @@
<template>
<div class="raven"></div>
</template>
<script>
import Raven from 'raven-js'
import RavenVue from 'raven-js/plugins/vue'
import Vue from 'vue'
import logger from '@/logging'
export default {
props: ['dsn'],
created () {
Raven.uninstall()
this.setUp()
},
destroyed () {
Raven.uninstall()
},
methods: {
setUp () {
Raven.uninstall()
logger.default.info('Installing raven...')
Raven.config(this.dsn).addPlugin(RavenVue, Vue).install()
}
},
watch: {
dsn: function () {
this.setUp()
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped >
.raven {
display: none;
}
</style>

View File

@ -16,8 +16,8 @@
<div class="menu-area">
<div class="ui compact fluid two item inverted menu">
<a class="active item" role="button" @click.prevent.stop="selectedTab = 'library'" data-tab="library"><translate>Browse</translate></a>
<a class="item" role="button" @click.prevent.stop="selectedTab = 'queue'" data-tab="queue">
<a :class="[{active: selectedTab === 'library'}, 'item']" role="button" @click.prevent.stop="selectedTab = 'library'" data-tab="library"><translate>Browse</translate></a>
<a :class="[{active: selectedTab === 'queue'}, 'item']" role="button" @click.prevent.stop="selectedTab = 'queue'" data-tab="queue">
<translate>Queue</translate>&nbsp;
<template v-if="queue.tracks.length === 0">
<translate>(empty)</translate>
@ -29,7 +29,7 @@
</div>
</div>
<div class="tabs">
<section class="ui bottom attached active tab" data-tab="library" :aria-label="labels.mainMenu">
<section :class="['ui', 'bottom', 'attached', {active: selectedTab === 'library'}, 'tab']" :aria-label="labels.mainMenu">
<nav class="ui inverted vertical large fluid menu" role="navigation" :aria-label="labels.mainMenu">
<div class="item">
<header class="header"><translate>My account</translate></header>
@ -39,7 +39,7 @@
<translate :translate-params="{username: $store.state.auth.username}">
Logged in as %{ username }
</translate>
<img class="ui right floated circular tiny avatar image" v-if="$store.state.auth.profile.avatar.square_crop" :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" />
<img class="ui right floated circular tiny avatar image" v-if="$store.state.auth.profile.avatar.square_crop" v-lazy="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" />
</router-link>
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/settings'}"><i class="setting icon"></i><translate>Settings</translate></router-link>
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'notifications'}">
@ -113,7 +113,7 @@
</div>
</div>
</div>
<section class="ui bottom attached tab" data-tab="queue">
<section :class="['ui', 'bottom', 'attached', {active: selectedTab === 'queue'}, 'tab']">
<table class="ui compact inverted very basic fixed single line unstackable table">
<draggable v-model="tracks" element="tbody" @update="reorder">
<tr
@ -188,11 +188,6 @@ export default {
fetchInterval: null
}
},
mounted() {
$(this.$el)
.find(".menu .item")
.tab()
},
destroy() {
if (this.fetchInterval) {
clearInterval(this.fetchInterval)
@ -206,10 +201,8 @@ export default {
labels() {
let mainMenu = this.$gettext("Main menu")
let selectTrack = this.$gettext("Play this track")
let pendingRequests = this.$gettext("Pending import requests")
let pendingFollows = this.$gettext("Pending follow requests")
return {
pendingRequests,
pendingFollows,
mainMenu,
selectTrack

View File

@ -29,7 +29,7 @@
</template>
<script>
import _ from 'lodash'
import _ from '@/lodash'
import axios from 'axios'
import logger from '@/logging'
import AlbumCard from '@/components/audio/album/Card'

View File

@ -4,7 +4,7 @@
<script>
import {mapState} from 'vuex'
import _ from 'lodash'
import _ from '@/lodash'
import url from '@/utils/url'
import {Howl} from 'howler'

View File

@ -11,7 +11,7 @@
<div class="ui loader"></div>
</div>
<div class="card" v-for="album in albums" :key="album.id">
<div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover.original}]" :style="getImageStyle(album)">
<div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover.original}]" v-lazy:background-image="getImageUrl(album)">
<play-button class="play-overlay" :icon-only="true" :is-playable="album.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :album="album.id"></play-button>
</div>
<div class="content">
@ -36,7 +36,7 @@
</template>
<script>
import _ from 'lodash'
import _ from '@/lodash'
import axios from 'axios'
import PlayButton from '@/components/audio/PlayButton'
@ -87,17 +87,15 @@ export default {
this.offset = Math.max(this.offset - this.limit, 0)
}
},
getImageStyle (album) {
getImageUrl (album) {
let url = '../../../assets/audio/default-cover.png'
if (album.cover.original) {
url = this.$store.getters['instance/absoluteUrl'](album.cover.medium_square_crop)
} else {
return {}
}
return {
'background-image': `url("${url}")`
return null
}
return url
}
},
watch: {
@ -108,10 +106,10 @@ export default {
}
</script>
<style scoped lang="scss">
@import '../../../style/vendor/media';
@import "../../../style/vendor/media";
.default-cover {
background-image: url('../../../assets/audio/default-cover.png') !important;
background-image: url("../../../assets/audio/default-cover.png") !important;
}
.wrapper {

View File

@ -11,7 +11,7 @@
<tbody>
<tr v-for="album in albums">
<td>
<img class="ui mini image" v-if="album.cover.original" :src="$store.getters['instance/absoluteUrl'](album.cover.small_square_crop)">
<img class="ui mini image" v-if="album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](album.cover.small_square_crop)">
<img class="ui mini image" v-else src="../../../assets/audio/default-cover.png">
</td>
<td colspan="4">
@ -82,5 +82,4 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -47,7 +47,7 @@
</template>
<script>
import _ from 'lodash'
import _ from '@/lodash'
import axios from 'axios'
import PlayButton from '@/components/audio/PlayButton'
@ -109,7 +109,7 @@ export default {
</script>
<style scoped lang="scss">
@import '../../../style/vendor/media';
@import "../../../style/vendor/media";
.play-overlay {
position: absolute;

View File

@ -7,7 +7,7 @@
<div :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']">
<h2 class="ui center aligned icon header">
<i v-if="!profile.avatar.square_crop" class="circular inverted user green icon"></i>
<img class="ui big circular image" v-else :src="$store.getters['instance/absoluteUrl'](profile.avatar.square_crop)" />
<img class="ui big circular image" v-else v-lazy="$store.getters['instance/absoluteUrl'](profile.avatar.square_crop)" />
<div class="content">
{{ profile.username }}
<div class="sub header" v-translate="{date: signupDate}">Registered since %{ date }</div>

View File

@ -53,7 +53,7 @@
</div>
<div class="ui six wide column">
<h3 class="ui header"><translate>Current avatar</translate></h3>
<img class="ui circular image" v-if="currentAvatar && currentAvatar.square_crop" :src="$store.getters['instance/absoluteUrl'](currentAvatar.medium_square_crop)" />
<img class="ui circular image" v-if="currentAvatar && currentAvatar.square_crop" v-lazy="$store.getters['instance/absoluteUrl'](currentAvatar.medium_square_crop)" />
<div class="ui hidden divider"></div>
<button @click="removeAvatar" v-if="currentAvatar && currentAvatar.square_crop" :class="['ui', {'loading': isLoadingAvatar}, ,'yellow', 'button']">
<translate>Remove avatar</translate>

View File

@ -3,7 +3,7 @@
<img
class="ui tiny circular avatar"
v-if="user.avatar && user.avatar.small_square_crop"
:src="$store.getters['instance/absoluteUrl'](user.avatar.small_square_crop)" />
v-lazy="$store.getters['instance/absoluteUrl'](user.avatar.small_square_crop)" />
<span v-else :style="defaultAvatarStyle" class="ui circular label">{{ user.username[0]}}</span>
&nbsp;@{{ user.username }}
</span>

View File

@ -1,53 +0,0 @@
<template>
<div class="comment">
<div class="content">
<a class="author">{{ user.username }}</a>
<div class="metadata">
<div class="date"><human-date :date="date"></human-date></div>
</div>
<div class="text" v-html="comment"></div>
</div>
<div class="actions">
<span
@click="collapsed = false"
v-if="truncated && collapsed"
class="expand">
<translate>Expand</translate>
</span>
<span
@click="collapsed = true"
v-if="truncated && !collapsed"
class="collapse">
<translate>Collapse</translate>
</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
user: {type: Object, required: true},
date: {required: true},
content: {type: String, required: true}
},
data () {
return {
collapsed: true,
length: 50
}
},
computed: {
comment () {
let text = this.content
if (this.collapsed) {
text = this.$options.filters.truncate(text, this.length)
}
return this.$options.filters.markdown(text)
},
truncated () {
return this.content.length > this.length
}
}
}
</script>

View File

@ -26,7 +26,7 @@
</template>
<script>
import _ from 'lodash'
import _ from '@/lodash'
import axios from 'axios'
import LibraryCard from '@/views/content/remote/Card'

View File

@ -61,7 +61,7 @@
</template>
<script>
import _ from 'lodash'
import _ from '@/lodash'
import axios from 'axios'
import logger from '@/logging'

View File

@ -69,7 +69,7 @@
</template>
<script>
import _ from "lodash"
import _ from "@/lodash"
import axios from "axios"
import logger from "@/logging"
import backend from "@/audio/backend"

View File

@ -70,7 +70,7 @@
<script>
import axios from "axios"
import _ from "lodash"
import _ from "@/lodash"
import $ from "jquery"
import logger from "@/logging"

View File

@ -123,6 +123,7 @@
</template>
<script>
import _ from "@/lodash"
import $ from "jquery";
import axios from "axios";
import logger from "@/logging";

View File

@ -92,7 +92,7 @@
<script>
import axios from "axios"
import _ from "lodash"
import _ from "@/lodash"
import $ from "jquery"
import logger from "@/logging"

View File

@ -94,7 +94,7 @@
<script>
import axios from "axios"
import $ from "jquery"
import _ from "lodash"
import _ from "@/lodash"
import BuilderFilter from "./Filter"
import TrackTable from "@/components/audio/track/Table"
import RadioButton from "@/components/radios/Button"

View File

@ -64,7 +64,7 @@
<script>
import axios from 'axios'
import $ from 'jquery'
import _ from 'lodash'
import _ from '@/lodash'
import Modal from '@/components/semantic/Modal'
import TrackTable from '@/components/audio/track/Table'

View File

@ -106,7 +106,7 @@
<script>
import axios from 'axios'
import _ from 'lodash'
import _ from '@/lodash'
import time from '@/utils/time'
import Pagination from '@/components/Pagination'
import ActionTable from '@/components/common/ActionTable'

View File

@ -86,7 +86,7 @@
<script>
import axios from 'axios'
import moment from 'moment'
import _ from 'lodash'
import _ from '@/lodash'
import Pagination from '@/components/Pagination'
import ActionTable from '@/components/common/ActionTable'
import OrderingMixin from '@/components/mixins/Ordering'

View File

@ -96,7 +96,7 @@
<script>
import axios from 'axios'
import _ from 'lodash'
import _ from '@/lodash'
import time from '@/utils/time'
import Pagination from '@/components/Pagination'
import ActionTable from '@/components/common/ActionTable'

View File

@ -67,7 +67,7 @@
</template>
<script>
import _ from 'lodash'
import _ from '@/lodash'
import axios from 'axios'
import {mapState} from 'vuex'

View File

@ -15,7 +15,7 @@
</template>
<script>
import _ from 'lodash'
import _ from '@/lodash'
import axios from 'axios'
import PlaylistCard from '@/components/playlists/Card'

View File

@ -1,61 +0,0 @@
<template>
<div :class="['ui', {collapsed: collapsed}, 'card']">
<div class="content">
<div class="header">{{ request.artist_name }}</div>
<div class="description">
<div
v-if="request.albums" v-html="$options.filters.markdown(request.albums)"></div>
<div class="ui comments">
<comment
:user="request.user"
:content="request.comment || ''"
:date="request.creation_date"></comment>
</div>
</div>
</div>
<div class="extra content">
<span >
<i v-if="request.status === 'pending'" class="hourglass start icon"></i>
<i v-if="request.status === 'accepted'" class="hourglass half icon"></i>
<i v-if="request.status === 'imported'" class="check icon"></i>
{{ request.status | capitalize }}
</span>
<button
@click="createImport"
v-if="request.status === 'pending' && importAction && $store.state.auth.availablePermissions['library']"
class="ui mini basic green right floated button"><translate>Create import</translate></button>
</div>
</div>
</template>
<script>
import Comment from '@/components/discussion/Comment'
export default {
props: {
request: {type: Object, required: true},
importAction: {type: Boolean, default: true}
},
components: {
Comment
},
data () {
return {
collapsed: true
}
},
methods: {
createImport () {
this.$router.push({
name: 'library.import.launch',
query: {request: this.request.id}})
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,127 +0,0 @@
<template>
<div>
<form v-if="!over" class="ui form" @submit.prevent="submit">
<p><translate>Something's missing in the library? Let us know what you would like to listen!</translate></p>
<div class="required field">
<label><translate>Artist name</translate></label>
<input v-model="currentArtistName" :placeholder="labels.artistNamePlaceholder" required maxlength="200">
</div>
<div class="field">
<label><translate>Albums</translate></label>
<p><translate>Leave this field empty if you're requesting the whole discography.</translate></p>
<input v-model="currentAlbums" :placeholder="labels.albumTitlePlaceholder" maxlength="2000">
</div>
<div class="field">
<label><translate>Comment</translate></label>
<textarea v-model="currentComment" rows="3" :placeholder="labels.commentPlaceholder" maxlength="2000"></textarea>
</div>
<button class="ui submit button" type="submit"><translate>Submit</translate></button>
</form>
<div v-else class="ui success message">
<div class="header"><translate>Request submitted!</translate></div>
<p><translate>We've received your request, you'll get some groove soon ;)</translate></p>
<button @click="reset" class="ui button"><translate>Submit another request</translate></button>
</div>
<div v-if="requests.length > 0">
<div class="ui divider"></div>
<h3 class="ui header"><translate>Pending requests</translate></h3>
<div class="ui list">
<div v-for="request in requests" class="item">
<div class="content">
<div class="header">{{ request.artist_name }}</div>
<div v-if="request.albums" class="description">
{{ request.albums|truncate }}</div>
<div v-if="request.comment" class="description">
{{ request.comment|truncate }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import $ from 'jquery'
import axios from 'axios'
import logger from '@/logging'
export default {
props: {
defaultArtistName: {type: String, default: ''},
defaultAlbums: {type: String, default: ''},
defaultComment: {type: String, default: ''}
},
created () {
this.fetchRequests()
},
mounted () {
$('.ui.radio.checkbox').checkbox()
},
data () {
return {
currentArtistName: this.defaultArtistName,
currentAlbums: this.defaultAlbums,
currentComment: this.defaultComment,
isLoading: false,
over: false,
requests: []
}
},
computed: {
labels () {
let artistNamePlaceholder = this.$gettext('The Beatles, Mickael Jackson…')
let albumTitlePlaceholder = this.$gettext('The White Album, Thriller…')
let commentPlaceholder = this.$gettext('Use this comment box to add details to your request if needed')
return {
artistNamePlaceholder,
albumTitlePlaceholder,
commentPlaceholder
}
}
},
methods: {
fetchRequests () {
let self = this
let url = 'requests/import-requests/'
axios.get(url, {}).then((response) => {
self.requests = response.data.results
})
},
submit () {
let self = this
this.isLoading = true
let url = 'requests/import-requests/'
let payload = {
artist_name: this.currentArtistName,
albums: this.currentAlbums,
comment: this.currentComment
}
axios.post(url, payload).then((response) => {
logger.default.info('Submitted request!')
self.isLoading = false
self.over = true
self.requests.unshift(response.data)
}, (response) => {
logger.default.error('error while submitting request')
self.isLoading = false
})
},
reset () {
this.over = false
this.currentArtistName = ''
this.currentAlbums = ''
this.currentComment = ''
},
truncate (string, length) {
if (string.length > length) {
return string.substring(0, length) + '…'
}
return string
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,7 +1,6 @@
import Vue from 'vue'
import Embed from './Embed'
import axios from 'axios'
import VuePlyr from 'vue-plyr'
Vue.use(VuePlyr)

View File

@ -1,7 +1,6 @@
import Vue from 'vue'
import moment from 'moment'
import showdown from 'showdown'
export function truncate (str, max, ellipsis) {
max = max || 100
@ -14,13 +13,6 @@ export function truncate (str, max, ellipsis) {
Vue.filter('truncate', truncate)
export function markdown (str) {
const converter = new showdown.Converter()
return converter.makeHtml(str)
}
Vue.filter('markdown', markdown)
export function ago (date) {
const m = moment(date)
return m.fromNow()

13
front/src/lodash.js Normal file
View File

@ -0,0 +1,13 @@
// cherry-pick specific lodash methods here to reduce bundle size
export default {
clone: require('lodash/clone'),
debounce: require('lodash/debounce'),
get: require('lodash/get'),
merge: require('lodash/merge'),
range: require('lodash/range'),
shuffle: require('lodash/shuffle'),
sortBy: require('lodash/sortBy'),
throttle: require('lodash/throttle'),
uniq: require('lodash/uniq'),
}

View File

@ -14,7 +14,6 @@ import VueLazyload from 'vue-lazyload'
import store from './store'
import GetTextPlugin from 'vue-gettext'
import { sync } from 'vuex-router-sync'
import translations from './translations.json'
import locales from '@/locales'
import filters from '@/filters' // eslint-disable-line
@ -23,12 +22,9 @@ import globals from '@/components/globals' // eslint-disable-line
sync(store, router)
window.$ = window.jQuery = require('jquery')
// this is absolutely dirty but at the moment, semantic UI does not
// play really nice with webpack and I want to get rid of Google Fonts
// require('./semantic/semantic.css')
require('semantic-ui-css/semantic.js')
require('./semantic.js')
require('masonry-layout')
let availableLanguages = (function () {
let l = {}
locales.locales.forEach(c => {
@ -54,7 +50,7 @@ Vue.use(GetTextPlugin, {
}
}
},
translations: translations,
translations: {},
silent: true
})

24
front/src/semantic.js Normal file
View File

@ -0,0 +1,24 @@
// require('semantic-ui-css/components/accordion.min.js')
require('semantic-ui-css/components/api.min.js')
require('semantic-ui-css/components/checkbox.min.js')
// require('semantic-ui-css/components/colorize.min.js')
require('semantic-ui-css/components/dimmer.min.js')
require('semantic-ui-css/components/dropdown.min.js')
// require('semantic-ui-css/components/embed.min.js')
// require('semantic-ui-css/components/form.min.js')
require('semantic-ui-css/components/modal.min.js')
// require('semantic-ui-css/components/nag.min.js')
// require('semantic-ui-css/components/popup.min.js')
require('semantic-ui-css/components/progress.min.js')
// require('semantic-ui-css/components/rating.min.js')
require('semantic-ui-css/components/search.min.js')
// require('semantic-ui-css/components/shape.min.js')
// require('semantic-ui-css/components/sidebar.min.js')
require('semantic-ui-css/components/site.min.js')
require('semantic-ui-css/components/state.min.js')
require('semantic-ui-css/components/sticky.min.js')
// require('semantic-ui-css/components/tab.min.js')
require('semantic-ui-css/components/transition.min.js')
// require('semantic-ui-css/components/video.min.js')
require('semantic-ui-css/components/visibility.min.js')
// require('semantic-ui-css/components/visit.min.js')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 957 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,6 +1,6 @@
import axios from 'axios'
import logger from '@/logging'
import _ from 'lodash'
import _ from '@/lodash'
function getDefaultUrl () {
return (
@ -40,14 +40,6 @@ export default {
enabled: {
value: true
}
},
raven: {
front_enabled: {
value: false
},
front_dsn: {
value: null
}
}
}
},
@ -131,7 +123,7 @@ export default {
})
},
fetchFrontSettings ({commit}) {
return axios.get('/settings.json').then(response => {
return axios.get('/front/settings.json').then(response => {
commit('frontSettings', response.data)
}, response => {
logger.default.error('Error when fetching front-end configuration (or no customization available)')

View File

@ -1,5 +1,5 @@
import logger from '@/logging'
import _ from 'lodash'
import _ from '@/lodash'
export default {
namespaced: true,
@ -86,7 +86,7 @@ export default {
if (callback && i + 1 === total) {
p.then(callback)
}
if (shouldPlay && p) {
if (shouldPlay && p && i + 1 === total) {
p.then(() => {
dispatch('next')
})

250
front/src/style/_main.scss Normal file
View File

@ -0,0 +1,250 @@
/*
Import this file into your LESS project to use Semantic UI without build tools
*/
/* Global */
@import "~semantic-ui-css/components/reset.css";
// we use our custom site css here to avoid loading google font
@import "./site";
/* Elements */
@import "~semantic-ui-css/components/button.css";
@import "~semantic-ui-css/components/container.css";
@import "~semantic-ui-css/components/divider.css";
// @import "~semantic-ui-css/components/flag.css";
@import "~semantic-ui-css/components/header.css";
@import "~semantic-ui-css/components/icon.css";
@import "~semantic-ui-css/components/image.css";
@import "~semantic-ui-css/components/input.css";
@import "~semantic-ui-css/components/label.css";
@import "~semantic-ui-css/components/list.css";
@import "~semantic-ui-css/components/loader.css";
// @import "~semantic-ui-css/components/placeholder.css";
// @import "~semantic-ui-css/components/rail.css";
// @import "~semantic-ui-css/components/reveal.css";
@import "~semantic-ui-css/components/segment.css";
@import "~semantic-ui-css/components/step.css";
/* Collections */
// @import "~semantic-ui-css/components/breadcrumb.css";
@import "~semantic-ui-css/components/form.css";
@import "~semantic-ui-css/components/grid.css";
@import "~semantic-ui-css/components/menu.css";
@import "~semantic-ui-css/components/message.css";
@import "~semantic-ui-css/components/table.css";
/* Views */
// @import "~semantic-ui-css/components/ad.css";
@import "~semantic-ui-css/components/card.css";
// @import "~semantic-ui-css/components/comment.css";
// @import "~semantic-ui-css/components/feed.css";
@import "~semantic-ui-css/components/item.css";
@import "~semantic-ui-css/components/statistic.css";
/* Modules */
// @import "~semantic-ui-css/components/accordion.css";
@import "~semantic-ui-css/components/checkbox.css";
@import "~semantic-ui-css/components/dimmer.css";
@import "~semantic-ui-css/components/dropdown.css";
// @import "~semantic-ui-css/components/embed.css";
@import "~semantic-ui-css/components/modal.css";
// @import "~semantic-ui-css/components/nag.css";
@import "~semantic-ui-css/components/popup.css";
@import "~semantic-ui-css/components/progress.css";
// @import "~semantic-ui-css/components/rating.css";
@import "~semantic-ui-css/components/search.css";
// @import "~semantic-ui-css/components/shape.css";
@import "~semantic-ui-css/components/sidebar.css";
@import "~semantic-ui-css/components/sticky.css";
@import "~semantic-ui-css/components/tab.css";
@import "~semantic-ui-css/components/transition.css";
// we do the import here instead in main.js
// as resolve order is not deterministric in webpack
// and we end up with CSS rules not applied,
// see https://github.com/webpack/webpack/issues/215
@import "./vendor/media";
html,
body {
@include media("<desktop") {
font-size: 90%;
}
}
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.instance-chooser {
margin-top: 2em;
}
.main.pusher,
.footer {
@include media(">desktop") {
margin-left: 350px !important;
margin-top: 50px;
}
transform: none !important;
}
.main.pusher > .ui.secondary.menu {
margin-left: 0;
margin-right: 0;
border: none;
box-shadow: inset 0px -2px 0px 0px rgba(34, 36, 38, 0.15);
.ui.item {
border: none;
border-bottom-style: none;
margin-bottom: 0px;
&.active {
box-shadow: inset 0px -2px 0px 0px #000;
}
}
@include media(">tablet") {
padding: 0 2.5rem;
}
@include media(">desktop") {
position: fixed;
left: 350px;
right: 0px;
top: 0px;
z-index: 99;
}
background-color: white;
.item {
padding-top: 1.5em;
padding-bottom: 1.5em;
}
}
.service-messages {
position: fixed;
bottom: 1em;
left: 1em;
@include media(">desktop") {
left: 350px;
}
}
.main-pusher {
padding: 1.5rem 0;
}
.ui.stripe.segment,
#footer {
padding: 2em;
@include media(">tablet") {
padding: 4em;
}
}
.ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.ui.small.text.container {
max-width: 500px !important;
}
.button.icon.tiny {
padding: 0.5em !important;
}
.sidebar {
.logo {
&.bordered.icon {
padding: .5em .41em !important;
}
path {
fill: white;
}
}
}
.discrete {
color: rgba(0, 0, 0, 0.87);
}
.link {
cursor: pointer;
}
.ui.really.basic.button {
&:not(:focus) {
box-shadow: none !important;
background-color: none !important;
}
}
.floated.buttons .button ~ .dropdown {
border-left: none;
}
.ui.icon.header .circular.icon {
display: flex;
justify-content: center;
}
.segment-content .button {
margin: 0.5em;
}
a {
cursor: pointer;
}
.segment.hidden {
display: none;
}
button.reset {
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
/* inherit font & color from ancestor */
color: inherit;
font: inherit;
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
line-height: normal;
/* Corrects font smoothing for webkit */
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
/* Corrects inability to style clickable `input` types in iOS */
-webkit-appearance: none;
text-align: inherit;
}
.ui.table > caption {
font-weight: bold;
padding: 0.5em;
text-align: left;
}
[role="button"] {
cursor: pointer;
}
.left.floated {
float: left;
}
.right.floated {
float: right;
}

202
front/src/style/_site.scss Normal file
View File

@ -0,0 +1,202 @@
/*!
* # Semantic UI 2.4.1 - Site
* http://github.com/semantic-org/semantic-ui/
*
*
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/
/*******************************
Page
*******************************/
html,
body {
height: 100%;
}
html {
font-size: 14px;
}
body {
margin: 0px;
padding: 0px;
overflow-x: hidden;
min-width: 320px;
background: #FFFFFF;
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-size: 14px;
line-height: 1.4285em;
color: rgba(0, 0, 0, 0.87);
font-smoothing: antialiased;
}
/*******************************
Headers
*******************************/
h1,
h2,
h3,
h4,
h5 {
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;
line-height: 1.28571429em;
margin: calc(2rem - 0.14285714em ) 0em 1rem;
font-weight: bold;
padding: 0em;
}
h1 {
min-height: 1rem;
font-size: 2rem;
}
h2 {
font-size: 1.71428571rem;
}
h3 {
font-size: 1.28571429rem;
}
h4 {
font-size: 1.07142857rem;
}
h5 {
font-size: 1rem;
}
h1:first-child,
h2:first-child,
h3:first-child,
h4:first-child,
h5:first-child {
margin-top: 0em;
}
h1:last-child,
h2:last-child,
h3:last-child,
h4:last-child,
h5:last-child {
margin-bottom: 0em;
}
/*******************************
Text
*******************************/
p {
margin: 0em 0em 1em;
line-height: 1.4285em;
}
p:first-child {
margin-top: 0em;
}
p:last-child {
margin-bottom: 0em;
}
/*-------------------
Links
--------------------*/
a {
color: #4183C4;
text-decoration: none;
}
a:hover {
color: #1e70bf;
text-decoration: none;
}
/*******************************
Scrollbars
*******************************/
/*******************************
Highlighting
*******************************/
/* Site */
::-webkit-selection {
background-color: #CCE2FF;
color: rgba(0, 0, 0, 0.87);
}
::-moz-selection {
background-color: #CCE2FF;
color: rgba(0, 0, 0, 0.87);
}
::selection {
background-color: #CCE2FF;
color: rgba(0, 0, 0, 0.87);
}
/* Form */
textarea::-webkit-selection,
input::-webkit-selection {
background-color: rgba(100, 100, 100, 0.4);
color: rgba(0, 0, 0, 0.87);
}
textarea::-moz-selection,
input::-moz-selection {
background-color: rgba(100, 100, 100, 0.4);
color: rgba(0, 0, 0, 0.87);
}
textarea::selection,
input::selection {
background-color: rgba(100, 100, 100, 0.4);
color: rgba(0, 0, 0, 0.87);
}
/* Force Simple Scrollbars */
body ::-webkit-scrollbar {
-webkit-appearance: none;
width: 10px;
height: 10px;
}
body ::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 0px;
}
body ::-webkit-scrollbar-thumb {
cursor: pointer;
border-radius: 5px;
background: rgba(0, 0, 0, 0.25);
-webkit-transition: color 0.2s ease;
transition: color 0.2s ease;
}
body ::-webkit-scrollbar-thumb:window-inactive {
background: rgba(0, 0, 0, 0.15);
}
body ::-webkit-scrollbar-thumb:hover {
background: rgba(128, 135, 139, 0.8);
}
/* Inverted UI */
body .ui.inverted::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
body .ui.inverted::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.25);
}
body .ui.inverted::-webkit-scrollbar-thumb:window-inactive {
background: rgba(255, 255, 255, 0.15);
}
body .ui.inverted::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.35);
}
/*******************************
Global Overrides
*******************************/
/*******************************
Site Overrides
*******************************/

View File

@ -16,7 +16,7 @@ export default {
durationFormatted (v) {
let duration = parseInt(v)
if (duration % 1 !== 0) {
return time.parse(0)
return this.parse(0)
}
duration = Math.round(duration)
return this.parse(duration)

View File

@ -142,11 +142,6 @@ export default {
"instance__nodeinfo_stats_enabled",
"instance__nodeinfo_private"
]
},
{
label: errorLabel,
id: "reporting",
settings: ["raven__front_enabled", "raven__front_dsn"]
}
]
}

View File

@ -120,7 +120,7 @@
<script>
import axios from 'axios'
import _ from 'lodash'
import _ from '@/lodash'
import time from '@/utils/time'
import {normalizeQuery, parseTokens, compileTokens} from '@/search'

View File

@ -56,7 +56,7 @@
<script>
import axios from "axios"
import _ from "lodash"
import _ from "@/lodash"
import $ from "jquery"
import OrderingMixin from "@/components/mixins/Ordering"

0
front/stats.json Normal file
View File

View File

@ -1,6 +1,6 @@
import {expect} from 'chai'
import {truncate, markdown, ago, capitalize, year} from '@/filters'
import {truncate, ago, capitalize, year} from '@/filters'
describe('filters', () => {
describe('truncate', () => {
@ -20,13 +20,6 @@ describe('filters', () => {
expect(output).to.equal('Hello pouet')
})
})
describe('markdown', () => {
it('renders markdown', () => {
const input = 'Hello world'
let output = markdown(input)
expect(output).to.equal('<p>Hello world</p>')
})
})
describe('ago', () => {
it('works', () => {
const input = new Date()

View File

@ -18,11 +18,11 @@ describe('store/instance', () => {
describe('mutations', () => {
it('settings', () => {
const state = {settings: {raven: {front_dsn: {value: 'test'}}}}
let settings = {raven: {front_enabled: {value: true}}}
const state = {settings: {users: {upload_quota: {value: 1}}}}
let settings = {users: {registration_enabled: {value: true}}}
store.mutations.settings(state, settings)
expect(state.settings).to.deep.equal({
raven: {front_dsn: {value: 'test'}, front_enabled: {value: true}}
users: {upload_quota: {value: 1}, registration_enabled: {value: true}}
})
})
})
@ -32,13 +32,13 @@ describe('store/instance', () => {
status: 200,
response: [
{
section: 'raven',
name: 'front_dsn',
value: 'test'
section: 'users',
name: 'upload_quota',
value: 1
},
{
section: 'raven',
name: 'front_enabled',
section: 'users',
name: 'registration_enabled',
value: false
}
]
@ -50,15 +50,15 @@ describe('store/instance', () => {
{
type: 'settings',
payload: {
raven: {
front_dsn: {
section: 'raven',
name: 'front_dsn',
value: 'test'
users: {
upload_quota: {
section: 'users',
name: 'upload_quota',
value: 1
},
front_enabled: {
section: 'raven',
name: 'front_enabled',
registration_enabled: {
section: 'users',
name: 'registration_enabled',
value: false
}
}

View File

@ -1,7 +1,7 @@
var sinon = require('sinon')
import {expect} from 'chai'
import _ from 'lodash'
import _ from '@/lodash'
import store from '@/store/queue'
import { testAction } from '../../utils'

View File

@ -1,4 +1,14 @@
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const webpack = require('webpack');
let plugins = [
// do not include moment.js locales since it's quite heavy
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
]
if (process.env.BUNDLE_ANALYZE === '1') {
plugins.push(new BundleAnalyzerPlugin())
}
module.exports = {
baseUrl: '/front/',
pages: {
@ -17,6 +27,7 @@ module.exports = {
config.optimization.delete('splitChunks')
},
configureWebpack: {
plugins: plugins,
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'

File diff suppressed because it is too large Load Diff