Skip to content

Instantly share code, notes, and snippets.

@tenthree
Last active January 15, 2021 12:52
Show Gist options
  • Save tenthree/3ae5f0bb1fad7099be0c0d22d236e773 to your computer and use it in GitHub Desktop.
Save tenthree/3ae5f0bb1fad7099be0c0d22d236e773 to your computer and use it in GitHub Desktop.
@vue/cli project configure example (with default scss settings)
// --------------------------------------------------
// require modules
// --------------------------------------------------
const pkg = require('./package.json')
const path = require('path').posix
const exec = require('child_process').execSync
const BannerPlugin = require('webpack/lib/BannerPlugin.js')
// --------------------------------------------------
// Prerender routes
// --------------------------------------------------
const prerenderRoutes = [
// '/',
// '/about'
]
// --------------------------------------------------
// Basic configuration
// --------------------------------------------------
const publicPath = (process.env.NODE_ENV !== 'production') ? '/' : '/'
const outputDir = !prerenderRoutes.length ? 'dist' : path.join('dist', publicPath)
const filenameHashing = false
const runtimeCompiler = false
const disableHtmlMinify = true
// --------------------------------------------------
// Build destinations by types
// --------------------------------------------------
const dest = {
script: 'js',
style: 'css',
image: 'images',
media: 'media',
font: 'fonts'
}
// --------------------------------------------------
// SASS auto injection data
// --------------------------------------------------
const sassInjectionData = `
// @import "@/scss/settings/variables.scss";
// @import "@/scss/settings/mixins.scss";
`
// --------------------------------------------------
// Webpack DevServer
// --------------------------------------------------
const devServer = {
historyApiFallback: true,
disableHostCheck: false,
allowedHosts: ['.ngrok.io'],
proxy: {
// [doc] https://github.com/chimurai/http-proxy-middleware#http-proxy-options
// e.g. proxy github api
// from: https://api.github.com/repos/f2etw/jobs/issues
// to: /api-github/repos/f2etw/jobs/issues
'/api-github/': {
target: 'https://api.github.com',
changeOrigin: true,
pathRewrite: { '^/api-github/': '' }
}
}
}
// --------------------------------------------------
// PWA configuration
// --------------------------------------------------
// const pwa = {
// workboxPluginMode: 'InjectManifest',
// workboxOptions: {
// swSrc: 'src/service-worker.js',
// importWorkboxFrom: 'disabled'
// // To use the latest workbox version(v4.3.0)
// // You can disable default workbox version(v3.6.3), and import it manually
// // e.g. importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.0/workbox-sw.js')
// },
// iconPaths: {
// favicon32: `${dest.image}/icons/favicon-32x32.png`,
// favicon16: `${dest.image}/icons/favicon-16x16.png`,
// appleTouchIcon: `${dest.image}/icons/apple-touch-icon-152x152.png`,
// maskIcon: `${dest.image}/icons/safari-pinned-tab.svg`,
// msTileImage: `${dest.image}/icons/msapplication-icon-144x144.png`
// }
// }
// --------------------------------------------------
// BannerPlugin configuration
// --------------------------------------------------
let user, email, branch, commit
try {
user = exec('git config user.name').toString().trim()
email = exec('git config user.email').toString().trim()
branch = exec('git rev-parse --abbrev-ref HEAD').toString().trim()
commit = exec('git rev-parse --short HEAD').toString().trim()
} catch (err) {
user = ''
email = ''
branch = ''
commit = ''
}
const now = new Date().toLocaleString('default', { hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
const banner = `/*!
@build with webpack
------------------------------
project : ${pkg.name}
author : ${user} <${email}>
branch : ${branch}
commit : ${commit}
file: : [file]
hash : [hash]
chunk : [chunkhash]
update : ${now}
------------------------------
*/`
// --------------------------------------------------
// Configure webpack
// --------------------------------------------------
const configureWebpack = config => {
// production mode only
if (process.env.NODE_ENV === 'production') {
return {
/*
// prevent xlsx|canvg|pdfmake files generated from amcharts4
// treat them as external libraries in webpack
// [doc] https://www.amcharts.com/docs/v4/getting-started/integrations/using-webpack/#Large_file_sizes
// [doc] https://webpack.js.org/configuration/externals/#function
externals (context, request, callback) {
if (/xlsx|canvg|pdfmake/.test(request)) {
return callback(null, `commonjs ${request}`)
}
callback()
}
*/
/*
// performance warning bundle size
performance: {
maxAssetSize: 500000,
maxEntrypointSize: 614400
}
*/
}
}
return {}
}
// --------------------------------------------------
// Chain webpack
// --------------------------------------------------
const chainWebpack = config => {
// filename pattern
const assetHash = !filenameHashing ? '' : '.[hash:8]'
const assetNamePattern = `[name]${assetHash}.[ext]`
// common
config.module.rule('images')
.use('url-loader')
.loader('url-loader')
.tap(options => {
options = {
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
name: path.join(dest.image, assetNamePattern)
}
}
}
return options
})
config.module.rule('svg')
.use('file-loader')
.loader('file-loader')
.tap(options => {
options.name = path.join(dest.image, assetNamePattern)
return options
})
config.module.rule('media')
.use('url-loader')
.loader('url-loader')
.tap(options => {
options = {
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
name: path.join(dest.media, assetNamePattern)
}
}
}
return options
})
config.module.rule('fonts')
.use('url-loader')
.loader('url-loader')
.tap(options => {
options = {
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
name: path.join(dest.font, assetNamePattern)
}
}
}
return options
})
// support csv module
// [cmd] npm i -D csv-loader papaparse
config.module.rule('csv')
.test(/\.csv$/)
.use('csv')
.loader('csv-loader')
.options({
dynamicTyping: true,
header: true,
skipEmptyLines: true
})
// production mode only
if (process.env.NODE_ENV === 'production') {
const hash = !filenameHashing ? '' : '.[contenthash:8]'
const scriptFullPath = path.join(dest.script, `[name]${hash}.js`)
const styleFullPath = path.join(dest.style, `[name]${hash}.css`)
config
.output
.filename(scriptFullPath)
.chunkFilename(scriptFullPath)
config
.plugin('extract-css')
.tap(args => {
args[0] = {
filename: styleFullPath,
chunkFilename: styleFullPath
}
return [...args]
})
config
.plugin('banner')
.use(BannerPlugin, [{ banner, raw: true }])
if (disableHtmlMinify) {
// Disable HtmlWebpackPlugin minify
const entries = Object.keys(config.entryPoints.entries() || [])
if (entries.length > 1) {
entries.forEach(entry => {
config
.plugin(`html-${entry}`)
.tap(args => {
args[0].minify = false
return [...args]
})
})
} else {
config
.plugin('html')
.tap(args => {
args[0].minify = false
return [...args]
})
}
}
if (prerenderRoutes.length) {
// add Prerender SPA Plugin
// [cmd] npm i -D prerender-spa-plugin
// [doc] https://github.com/chrisvfritz/prerender-spa-plugin
const PrerenderSPAPlugin = require('prerender-spa-plugin')
config
.plugin('prerender')
.use(PrerenderSPAPlugin, [
{
staticDir: path.join(__dirname, 'dist'),
indexPath: path.join(__dirname, 'dist', publicPath, 'index.html'),
routes: prerenderRoutes.map(route => `${publicPath}${route}`.replace(/\/+/g, '/')),
server: {
port: 8080
},
renderer: new PrerenderSPAPlugin.PuppeteerRenderer({
// WSL ubuntu
// [issue] https://github.com/puppeteer/puppeteer/issues/1837
// [Dependencies] https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix
// args: [
// '--no-sandbox',
// // '--disable-setuid-sandbox',
// // '--disable-gpu',
// '--single-process'
// ],
inject: {},
headless: false,
// renderAfterDocumentEvent: 'prerender' // document.dispatchEvent(new Event('prerender'))
renderAfterElementExists: '[prerender]' // add "prerender" attribute on the root element of the vue route component
}),
postProcess (renderedRoute) {
// [issue] https://github.com/chrisvfritz/prerender-spa-plugin/issues/198
// [issue] https://github.com/chrisvfritz/prerender-spa-plugin/issues/335
const html = renderedRoute.html.split('</head>')
html[0] = html[0].replace(/<script.*src="[^<]([0-9a-zA-Z/-])+\.js".*><\/script>/g, function (target) {
console.log(`\n\n[clean unscript] ${target}`)
return ''
})
renderedRoute.html = html.join('</head>')
return renderedRoute
}
}
])
}
}
}
// --------------------------------------------------
// Export vue configuration
// --------------------------------------------------
module.exports = {
publicPath,
outputDir,
filenameHashing,
runtimeCompiler,
css: {
loaderOptions: {
sass: {
prependData: sassInjectionData // use "data" option if sass-loader < v8.0.0
}
}
},
devServer,
// pwa,
configureWebpack,
chainWebpack
}
// --------------------------------------------------
// RWD media query generator
// --------------------------------------------------
@mixin rwd ($device) {
@media only screen and (#{$device}) {
@content;
}
}
// --------------------------------------------------
// inject pseudo element ":before" for vertical-align
// --------------------------------------------------
@mixin use-vertical-align ($align: middle) {
@if ($align == "none") {
&:before {
content: normal;
width: auto;
height: auto;
}
} @else {
&:before {
content: "";
display: inline-block;
width: 0;
height: 100%;
vertical-align: $align;
}
}
}
// --------------------------------------------------
// inject pseudo element ":before" as space placeholder
// --------------------------------------------------
@mixin use-fixed-ratio ($width: none, $height: none) {
@if ($width == "none") {
&:before {
content: normal;
display: inline;
padding: 0;
}
} @else if ($width != "none") and ($height == "none") {
&:before {
content: "";
display: block;
width: 100%;
height: 0;
padding-top: 100%;
}
} @else {
&:before {
content: "";
display: block;
width: 100%;
height: 0;
padding-top: ($height / $width) * 100%;
}
}
}
// --------------------------------------------------
// apply multi-formats blur for browsers compatibility
// --------------------------------------------------
@mixin use-filter-blur($radius: 4) {
// filter: url("data:image/svg+xml;utf8,<svg height=\"0\" xmlns=\"http://www.w3.org/2000/svg\"><filter id=\"svg-filter-blur\" x=\"-5%\" y=\"-5%\" width=\"110%\" height=\"110%\"><feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"#{$radius}\"/></filter></svg>#svg-filter-blur");
filter: url("data:image/svg+xml;utf-8;base64,PHN2ZyBoZWlnaHQ9IjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGZpbHRlciBpZD0ic3ZnLWZpbHRlci1ibHVyIiB4PSItNSUiIHk9Ii01JSIgd2lkdGg9IjExMCUiIGhlaWdodD0iMTEwJSI+PGZlR2F1c3NpYW5CbHVyIGluPSJTb3VyY2VHcmFwaGljIiBzdGREZXZpYXRpb249IjQiLz48L2ZpbHRlcj48L3N2Zz4=#svg-filter-blur");
filter: blur(#{$radius}px);
filter: progid:DXImageTransform.Microsoft.Blur(PixelRadius="#{$radius}");
}
// --------------------------------------------------
// auto inject z-index from $AUTO_LAYERS in variables.scss
// --------------------------------------------------
@mixin use-layer($self) {
@if not index($AUTO_LAYERS, "#{$self}") {
@error "#{$self} is not defined in $AUTO_LAYERS."
}
z-index: index($AUTO_LAYERS, "#{$self}")
}
// --------------------------------------------------
// RWD devices information
// --------------------------------------------------
$RWD_MOBILE: "min-width: 0";
$RWD_MOBILE_ONLY: "max-width: 519px";
$RWD_TABLET_SMALL: "min-width: 520px";
$RWD_TABLET: "min-width: 760px";
$RWD_DESKTOP: "min-width: 960px";
$RWD_DESKTOP_WIDE: "min-width: 1280px";
// --------------------------------------------------
// Generate z-index automatically in order with "@use-layer" mixin
// --------------------------------------------------
$AUTO_LAYERS: (
'.the-content',
'.the-header',
'.the-menu'
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment