Use Vue CLI to build assets

This commit is contained in:
Matt Isenhower 2018-07-31 12:14:27 -07:00
parent acdf589d52
commit 3cfec6d8f2
25 changed files with 3631 additions and 1807 deletions

View File

@ -1,3 +0,0 @@
{
presets: ['env', 'stage-2']
}

View File

@ -7,7 +7,7 @@ NINTENDO_SESSION_ID_JP=
SPLATNET_USER_AGENT=
# (Optional) Google Analytics tracking ID
GOOGLE_ANALYTICS_ID=
VUE_APP_GOOGLE_ANALYTICS_ID=
# (Optional) Sentry error reporting (https://sentry.io)
SENTRY_DSN=

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
],
}

View File

@ -28,7 +28,7 @@ server {
add_header Content-Type text/calendar;
}
# CORS (for third-party integrations)
# CORS (for modern ES module support and third-party integrations)
location /assets/ {
add_header Access-Control-Allow-Origin *;
}

View File

@ -1,10 +1,10 @@
{
"name": "splatoon2.ink",
"private": true,
"scripts": {
"dev": "webpack",
"watch": "npm run dev -- --watch",
"build": "webpack -p --env production",
"serve": "webpack-dev-server --hot --content-base dist/",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --modern --no-clean",
"lint": "vue-cli-service lint",
"splatnet": "node src/app splatnet",
"twitter": "node src/app twitter",
"twitter:test": "node src/app twitterTest",
@ -15,28 +15,15 @@
"locale-man": "node node_modules/locale-man/ -l en,es,es-MX,fr,fr-CA,de,nl,it,ru,ja -o src/locale"
},
"dependencies": {
"autoprefixer": "^7.1.2",
"axios": "^0.17.1",
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-polyfill": "^6.23.0",
"babel-preset-env": "^1.6.0",
"babel-preset-stage-2": "^6.24.1",
"bulma": "^0.6.1",
"bulma-tooltip": "^2.0.1",
"clean-webpack-plugin": "^0.1.16",
"console-stamp": "^0.2.5",
"copy-webpack-plugin": "^4.0.1",
"cozy-ical": "^1.1.22",
"cron": "^1.2.1",
"css-loader": "^0.28.4",
"delay": "^2.0.0",
"dotenv": "^4.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.5",
"he": "^1.1.1",
"html-loader": "^0.5.0",
"html-webpack-plugin": "^2.30.1",
"json-stable-stringify": "^1.0.1",
"jsonpath": "^1.0.0",
"lodash": "^4.17.4",
@ -44,31 +31,54 @@
"mkdirp": "^0.5.1",
"module-alias": "^2.1.0",
"moment-timezone": "^0.5.13",
"node-sass": "^4.5.3",
"postcss-loader": "^2.0.6",
"puppeteer": "^0.13.0",
"purify-css": "^1.2.5",
"purifycss-webpack": "^0.7.0",
"raven": "^2.1.1",
"sass-loader": "^6.0.6",
"style-loader": "^0.19.0",
"twitter": "^1.7.1",
"v-click-outside": "^1.0.0",
"vue": "^2.4.2",
"vue-clipboard2": "^0.2.0",
"vue-loader": "^13.0.2",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.4.2",
"vuex": "^3.0.1",
"vuex-i18n": "^1.8.0",
"webpack": "^3.4.1",
"webpack-dev-server": "^2.6.1"
"vuex-i18n": "^1.8.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.0.0-rc.9",
"@vue/cli-plugin-eslint": "^3.0.0-rc.9",
"@vue/cli-service": "^3.0.0-rc.9",
"locale-man": "^0.0.5",
"shipit-cli": "^3.0.0"
"node-sass": "^4.9.2",
"purify-css": "^1.2.5",
"purifycss-webpack": "^0.7.0",
"sass-loader": "^7.0.3",
"shipit-cli": "^3.0.0",
"vue-template-compiler": "^2.5.16"
},
"_moduleAliases": {
"@": "src"
}
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"firefox esr",
"not dead"
]
}

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: [
require('autoprefixer'),
],
}

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -8,25 +8,25 @@
<meta name="description" content="Splatoon 2 map rotation, upcoming Salmon Run schedules, SplatNet gear, and Splatfest info.">
<meta property="og:title" content="Splatoon 2 Map Schedules">
<meta property="og:description" content="Current/upcoming maps and Salmon Run schedules.">
<meta property="og:image" content="https://splatoon2.ink/<%= require('../src/web/assets/icons/apple-touch-icon.png') %>">
<meta property="og:image" content="https://splatoon2.ink/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Splatoon2.ink">
<% for (var css in htmlWebpackPlugin.files.chunks.main.css) { %>
<link href="<%= htmlWebpackPlugin.files.chunks.main.css[css] %>" rel="stylesheet">
<% } %>
<link rel="apple-touch-icon" sizes="180x180" href="<%= require('../src/web/assets/icons/apple-touch-icon.png') %>">
<link rel="icon" type="image/png" sizes="32x32" href="<%= require('../src/web/assets/icons/favicon-32x32.png') %>">
<link rel="icon" type="image/png" sizes="16x16" href="<%= require('../src/web/assets/icons/favicon-16x16.png') %>">
<link rel="apple-touch-icon" sizes="180x180" href="<%= BASE_URL %>apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="<%= BASE_URL %>favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="<%= BASE_URL %>favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<meta name="msapplication-config" content="/browserconfig.xml">
<link rel="mask-icon" href="<%= require('../src/web/assets/icons/safari-pinned-tab.svg') %>" color="#ff470f">
<link rel="shortcut icon" href="<%= require('../src/web/assets/icons/favicon.ico') %>">
<link rel="mask-icon" href="<%= BASE_URL %>safari-pinned-tab.svg" color="#ff470f">
<link rel="shortcut icon" href="<%= BASE_URL %>favicon.ico">
<meta name="theme-color" content="#cccccc">
<% if (process.env.VUE_APP_GOOGLE_ANALYTICS_ID) { %>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= VUE_APP_GOOGLE_ANALYTICS_ID %>"></script>
<script>window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', '<%= VUE_APP_GOOGLE_ANALYTICS_ID %>');</script>
<% } %>
</head>
<body>
<div id="app"></div>
<script src="<%= htmlWebpackPlugin.files.chunks.main.entry %>"></script>
<script async src='https://www.google-analytics.com/analytics.js'></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -4,12 +4,8 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<% for (var css in htmlWebpackPlugin.files.chunks.screenshots.css) { %>
<link href="<%= htmlWebpackPlugin.files.chunks.screenshots.css[css] %>" rel="stylesheet">
<% } %>
</head>
<body>
<div id="app"></div>
<script src="<%= htmlWebpackPlugin.files.chunks.screenshots.entry %>"></script>
</body>
</html>

94
src/common/regions.esm.js Normal file
View File

@ -0,0 +1,94 @@
// This is the same as regions.js but formatted for ES6 modules.
// Temporary fix for importing with vue-cli until I find something that works with nodejs as well.
export const splatoonRegions = [
{ key: null, name: 'Global', demonym: 'Global' },
{ key: 'na', name: 'North America & Oceania', demonym: 'North American & Oceanian' },
{ key: 'eu', name: 'Europe', demonym: 'European' },
{ key: 'jp', name: 'Japan', demonym: 'Japanese' },
];
export const languages = [
{ region: 'NA', language: 'en', name: 'English' },
{ region: 'EU', language: 'en', name: 'English (UK)' },
{ region: 'EU', language: 'es', name: 'Español' },
{ region: 'NA', language: 'es-MX', name: 'Español (MX)' },
{ region: 'EU', language: 'fr', name: 'Français' },
{ region: 'NA', language: 'fr-CA', name: 'Français (CA)' },
{ region: 'EU', language: 'de', name: 'Deutsch' },
{ region: 'EU', language: 'nl', name: 'Nederlands' },
{ region: 'EU', language: 'it', name: 'Italiano' },
{ region: 'EU', language: 'ru', name: 'Pусский' },
{ region: 'JP', language: 'ja', name: '日本語' },
];
export function getRegionByKey(key) {
return splatoonRegions.find(r => r.key == key);
}
export function detectSplatoonLanguage() {
let browserLanguages = window.navigator.languages || [window.navigator.language];
browserLanguages = browserLanguages.map(l => l.toLowerCase());
let availableLanguages = languages.map(l => l.language.toLowerCase());
// Try to find a match based on the first part of the available languages (i.e., match "es" for "es-ES")
for (let language of browserLanguages) {
for (let availableLanguage of availableLanguages) {
if (language.startsWith(availableLanguage))
return availableLanguage;
}
}
}
export function detectSplatoonRegion() {
if (window.navigator && window.navigator.language)
return detectSplatoonRegionFromLanguage(window.navigator.language);
return null;
}
function detectSplatoonRegionFromLanguage(language) {
switch (language) {
case 'en-US': // USA
case 'en-CA': // Canada (English)
case 'fr-CA': // Canada (French)
case 'es-MX': // Mexico
case 'en-AU': // Australia
case 'en-NZ': // New Zealand
return 'na';
case 'de-AT': // Austria
case 'nl-BE': // Belgium (Dutch)
case 'fr-BE': // Belgium (French)
case 'cs-CZ': // Czech Republic
case 'da-DK': // Denmark
case 'de': // Germany
case 'de-DE': // Germany
case 'es-ES': // Spain
case 'fi-FI': // Finland
case 'fr': // France
case 'fr-FR': // France
case 'el-GR': // Greece
case 'hu-HU': // Hungary
case 'it-IT': // Italy
case 'nl': // Netherlands
case 'nl-NL': // Netherlands
case 'nb-NO': // Norway
case 'pl-PL': // Poland
case 'pt-PT': // Portugal
case 'ru-RU': // Russia
case 'en-ZA': // South Africa
case 'sv-SE': // Sweden
case 'de-CH': // Switzerland (German)
case 'fr-CH': // Switzerland (French)
case 'it-CH': // Switzerland (Italian)
case 'en-GB': // United Kingdom
case 'en-IE': // Ireland
return 'eu';
case 'ja': // Japan
case 'ja-JP': // Japan
return 'jp';
}
return null;
}

View File

@ -24,7 +24,8 @@ function getRegionByKey(key) {
}
function detectSplatoonLanguage() {
let browserLanguages = window.navigator.languages.map(l => l.toLowerCase());
let browserLanguages = window.navigator.languages || [window.navigator.language];
browserLanguages = browserLanguages.map(l => l.toLowerCase());
let availableLanguages = languages.map(l => l.language.toLowerCase());
// Try to find a match based on the first part of the available languages (i.e., match "es" for "es-ES")

View File

@ -1,12 +1,11 @@
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
if (GOOGLE_ANALYTICS_ID) {
ga('create', GOOGLE_ANALYTICS_ID, 'auto');
ga('send', 'pageview');
}
export default {
event() {
ga('send', 'event', ...arguments);
event(category, action, label = undefined) {
if (window.gtag) {
window.gtag('event', {
event_category: category,
event_action: action,
event_label: label,
});
}
},
}
};

View File

@ -181,7 +181,7 @@
<script>
import axios from 'axios';
import { mapActions } from 'vuex';
import regions from '@/common/regions';
import * as regions from '@/common/regions.esm';
import Dropdown from './Dropdown.vue';
import ScheduleBox from './splatoon/ScheduleBox.vue';
import SalmonRunBox from './splatoon/SalmonRunBox.vue';

View File

@ -21,7 +21,7 @@ import axios from 'axios';
import Wrapper from '@/web/components/screenshots/Wrapper.vue';
import SplatfestBox from '@/web/components/splatoon/SplatfestBox.vue';
import SplatfestResultsBox from '@/web/components/splatoon/SplatfestResultsBox.vue';
import { splatoonRegions } from '@/common/regions';
import { splatoonRegions } from '@/common/regions.esm';
export default {
components: { Wrapper, SplatfestBox, SplatfestResultsBox },

View File

@ -56,7 +56,7 @@
<script>
import Vue from 'vue';
import analytics from '@/web/analytics';
import regions from '@/common/regions';
import * as regions from '@/common/regions.esm';
import Modal from '@/web/components/Modal.vue';
import Dropdown from '@/web/components/Dropdown.vue';
import SplatfestBox from './SplatfestBox.vue';

View File

@ -6,6 +6,8 @@ import './i18n';
import './directives.js';
import './filters.js';
import './assets/css/main.scss';
// Start the Vue app with our root component
import App from './components/App.vue';
new Vue({

View File

@ -7,6 +7,8 @@ import './i18n';
import './directives.js';
import './filters.js';
import './assets/css/screenshots.scss';
import VueRouter from 'vue-router';
Vue.use(VueRouter);

View File

@ -1,6 +1,6 @@
export default {
loadLocale({ dispatch }, locale) {
import(/* webpackChunkName: "lang-[request]" */ `@/web/locale/${locale}`)
.then(translations => dispatch('addLocale', { locale, translations }));
.then(translations => dispatch('addLocale', { locale, translations: translations.default }));
}
}

67
vue.config.js Normal file
View File

@ -0,0 +1,67 @@
const path = require('path');
const glob = require('glob');
const PurifyCSSPlugin = require('purifycss-webpack');
module.exports = {
assetsDir: 'assets',
productionSourceMap: false,
configureWebpack: {
optimization: {
splitChunks: {
cacheGroups: {
// We want to keep the vendors chunk, but we don't want to include moment since it's relatively large
// and only used for the screenshots page (which is never served to the client).
// An alternative method would be to set minChunks to 2 here.
vendors: {
test: /[\\/]node_modules[\\/](?!(moment|moment-timezone)[\\/])/
},
// The common chunk doesn't really make sense here since the only reason we're splitting
// pages is for the server-side screenshots page. Disabling it to save an HTTP request.
common: false
}
}
},
plugins: [
new PurifyCSSPlugin({
paths: [
...glob.sync(path.join(__dirname, 'src/web/html/**/*.html')),
...glob.sync(path.join(__dirname, 'src/web/components/**/*.vue')),
],
purifyOptions: {
whitelist: [
'.title:not(.is-spaced)+.subtitle', // Fix subtitle spacing
// Dynamic merchandise types
'.merchandise-box.shoes',
'.merchandise-box.head',
'.merchandise-box.clothes',
],
cleanCssOptions: {
rebase: false, // Leave relative paths alone when minifying CSS
},
},
}),
]
},
chainWebpack: config => {
config.plugin('prefetch-index').tap(options => {
options[0].fileBlacklist = options[0].fileBlacklist || [];
options[0].fileBlacklist.push(/lang-.+?\.js$/);
return options;
});
},
devServer: {
contentBase: [
'./public',
'./dist'
]
},
pages: {
index: {
entry: 'src/web/main.js'
},
screenshots: {
entry: 'src/web/screenshots.js'
}
}
}

View File

@ -1,140 +0,0 @@
require('dotenv').config();
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const PurifyCSSPlugin = require('purifycss-webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = function(env) {
const production = (env === 'production');
return {
resolve: { alias: { '@': path.resolve(__dirname, './src') } },
entry: {
main: [
'./src/web/main.js',
'./src/web/assets/css/main.scss',
],
screenshots: [
'./src/web/screenshots.js',
'./src/web/assets/css/screenshots.scss',
],
},
output: {
path: path.resolve(__dirname, './dist'),
filename: 'assets/js/[name].[hash:6].js',
chunkFilename: 'assets/js/[name].[hash:6].js',
},
devtool: (production) ? false : '#cheap-module-eval-source-map',
devServer: {
overlay: true,
historyApiFallback: true,
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
extractCSS: true,
},
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
publicPath: '../../',
use: [
{
loader: 'css-loader',
options: { sourceMap: !production },
},
{
loader: 'postcss-loader',
options: { sourceMap: !production },
},
{
loader: 'sass-loader',
options: { sourceMap: !production },
},
],
}),
},
{
test: /\.(woff2?|ttf|eot)$/,
loader: 'file-loader?name=assets/fonts/[name].[hash:6].[ext]',
},
{
test: /\.(png|jpe?g|gif|svg)$/,
loader: 'file-loader?name=assets/img/[name].[hash:6].[ext]',
},
{
test: /favicon\.ico$/,
loader: 'file-loader?name=favicon.ico',
}
],
},
plugins: [
// Remove old files
// new CleanWebpackPlugin([
// 'dist/assets/css/*',
// 'dist/assets/js/*',
// ]),
new webpack.DefinePlugin({
'GOOGLE_ANALYTICS_ID': JSON.stringify(process.env.GOOGLE_ANALYTICS_ID),
}),
// Extract CSS to a separate file
new ExtractTextPlugin({
filename: 'assets/css/[name].[contenthash:6].css',
disable: !production,
}),
// Remove unused CSS styles
new PurifyCSSPlugin({
paths: [
...glob.sync(path.join(__dirname, 'public/**/*.html')),
...glob.sync(path.join(__dirname, 'src/web/components/**/*.vue')),
],
minimize: production,
purifyOptions: {
whitelist: [
'.title:not(.is-spaced)+.subtitle', // Fix subtitle spacing
// Dynamic merchandise types
'.merchandise-box.shoes',
'.merchandise-box.head',
'.merchandise-box.clothes',
],
cleanCssOptions: {
rebase: false, // Leave relative paths alone when minifying CSS
},
},
}),
// Build main public HTML
new HtmlWebpackPlugin({
inject: false,
filename: 'index.html',
template: 'public/index.html',
minify: { collapseWhitespace: true },
}),
// Build HTML used for screenshot generation
new HtmlWebpackPlugin({
inject: false,
filename: 'screenshots.html',
template: 'public/screenshots.html',
minify: { collapseWhitespace: true },
}),
// Copy additional favicons
new CopyWebpackPlugin([
{ from: 'public' },
]),
],
}
}

4990
yarn.lock

File diff suppressed because it is too large Load Diff