Vue.js 3 based frontend
This commit is contained in:
		
							
								
								
									
										29
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | # Logs | ||||||
|  | logs | ||||||
|  | *.log | ||||||
|  | npm-debug.log* | ||||||
|  | yarn-debug.log* | ||||||
|  | yarn-error.log* | ||||||
|  | pnpm-debug.log* | ||||||
|  | lerna-debug.log* | ||||||
|  |  | ||||||
|  | node_modules | ||||||
|  | .DS_Store | ||||||
|  | dist | ||||||
|  | dist-ssr | ||||||
|  | coverage | ||||||
|  | *.local | ||||||
|  |  | ||||||
|  | /cypress/videos/ | ||||||
|  | /cypress/screenshots/ | ||||||
|  |  | ||||||
|  | # Editor directories and files | ||||||
|  | .vscode/* | ||||||
|  | !.vscode/extensions.json | ||||||
|  | .idea | ||||||
|  | *.suo | ||||||
|  | *.ntvs* | ||||||
|  | *.njsproj | ||||||
|  | *.sln | ||||||
|  | *.sw? | ||||||
|  | *.kpt-pipeline | ||||||
							
								
								
									
										67
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | # log-viewer microfrontend (WIP) | ||||||
|  | This is an experimental microfrontend to view logs coming from camtiler, testing microfrontends concept and developing | ||||||
|  | Javascript/Node application directly on Kubernetes cluster using Skaffold (hot-reloading source files works!) | ||||||
|  |  | ||||||
|  | Currently the application itself is a really minimalistic frontend based on Vue.js 3 framework. | ||||||
|  |  | ||||||
|  | ## Getting started | ||||||
|  | 0. Have access with kubectl to desired Kubernetes cluster  | ||||||
|  | 0. Have local Docker daemon ready along with access to your image registry (building in-cluster with Kaniko is currently not tested)  | ||||||
|  | 1. Get Skaffold 2.0.0-beta*: https://github.com/GoogleContainerTools/skaffold/releases | ||||||
|  | 2. Run `skaffold dev --namespace {desired K8S namespace}` | ||||||
|  |  | ||||||
|  | And you should be good to go. Skaffold would create a deployment, build the container and upload it to your registry | ||||||
|  | and deploy it. | ||||||
|  |  | ||||||
|  | ## Good to know | ||||||
|  | ### Skaffold | ||||||
|  | All Skaffold configuration is in skaffold.yml. There we have: | ||||||
|  | 1. App/deployment name (metadata) | ||||||
|  | 2. Build artifacts - artifact that Skaffold would build and use, either in case of development or also actual deploy | ||||||
|  | 3. Profiles - similar to `npm` commands/profiles, you can have many different actions (run `skaffold {action}`). | ||||||
|  |    We only have `dev` which will build and deploy the `log-viewer-frontend` container | ||||||
|  |    (in manner how it's defined in Kubernetes manifest), and then enable `manual sync`, which defines which  | ||||||
|  |    changed files will be copied to the container when it's running - change in everything else will trigger | ||||||
|  |    rebuilding the container. | ||||||
|  |  | ||||||
|  | ### Hot-reload vs rebuilding the container | ||||||
|  | Currently the files that will be copied are fine-tuned as they are known to work well with Vue.js hot-reload feature | ||||||
|  | (eg actual frontend code). Other files, such as framework configuration files (`vite.config.js`, `vue.config.js`) | ||||||
|  | will trigger rebuilding and more importantly, restarting the container, to provide stability. It might not be necessary | ||||||
|  | to restart the container, this is just testing at this point. You can try it by introducing less explicit `sync`  | ||||||
|  | config in `skaffold.yml`. | ||||||
|  |  | ||||||
|  | Change in `packages*` files will also trigger rebuilding and restarting the container, with the difference of | ||||||
|  | caching the result of the `npm install` command - `node_modules` directory. So in case of changing framework config, | ||||||
|  | the container will be quickly rebuilt and restarted, but when changing packages in `package.json`, `npm install` | ||||||
|  | will also be ran when re-building the container. | ||||||
|  |  | ||||||
|  | ### Dockerfile | ||||||
|  | This is a really simple Dockerfile based on the official Node Docker image. | ||||||
|  | Only extra stuff it does is: | ||||||
|  | 1. exposing the port for reference | ||||||
|  | 2. changing workdir(?) | ||||||
|  | 3. running `npm install` on-demand | ||||||
|  | 4. copying other code into the container | ||||||
|  | 5. defining the run command (important!) | ||||||
|  |  | ||||||
|  | We also have the `.dockerignore` to filter out files that never need to make into the container, such as IDE | ||||||
|  | metadata directories, Git, readme etc. | ||||||
|  |  | ||||||
|  | With the current config, your local Docker daemon will be used to build the image and upload it to your image registry. | ||||||
|  | Building in-cluster with Kaniko should be supported (https://skaffold.dev/docs/pipeline-stages/builders/docker/) | ||||||
|  | but currently not tested.  | ||||||
|  |  | ||||||
|  | ### Kubernetes | ||||||
|  | The Kubernetes manifest(s) are supposed to be in the `k8s/` directory. | ||||||
|  | Skaffold will pick them up automatically from there, apply them (also cleaning up after itself), | ||||||
|  | and match the container images from it's manifest and Kubernetes manifest, so it knows when to make the Kubernetes | ||||||
|  | redeploy the image. | ||||||
|  |  | ||||||
|  | Here we have a simple deployment, where `https://playground.k-space.ee/` takes us to the (development) frontend | ||||||
|  | container and `https://playground.k-space.ee/events` gets data from the `camtiler log-backend`. | ||||||
|  |  | ||||||
|  | The actual production usage may vary a lot in case of frontend - one would usually not run a  | ||||||
|  | Node.js server to serve frontend assets, unless it's a SSR frontend (Nuxt, Next frameworks). | ||||||
|  | But for Node.js backends, it's pretty close to the standard. | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  |   <head> | ||||||
|  |     <meta charset="UTF-8" /> | ||||||
|  |     <link rel="icon" href="/favicon.ico" /> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|  |     <title>Log Viewer</title> | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|  |     <div id="app"></div> | ||||||
|  |     <script type="module" src="/src/main.js"></script> | ||||||
|  |   </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										15
									
								
								frontend/src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/App.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <script setup> | ||||||
|  | import LogViewer from './components/LogViewer.vue' | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <template> | ||||||
|  |     <LogViewer /> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: 'app1', | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | <style scoped> | ||||||
|  | </style> | ||||||
							
								
								
									
										74
									
								
								frontend/src/assets/base.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								frontend/src/assets/base.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | /* color palette from <https://github.com/vuejs/theme> */ | ||||||
|  | :root { | ||||||
|  |   --vt-c-white: #ffffff; | ||||||
|  |   --vt-c-white-soft: #f8f8f8; | ||||||
|  |   --vt-c-white-mute: #f2f2f2; | ||||||
|  |  | ||||||
|  |   --vt-c-black: #181818; | ||||||
|  |   --vt-c-black-soft: #222222; | ||||||
|  |   --vt-c-black-mute: #282828; | ||||||
|  |  | ||||||
|  |   --vt-c-indigo: #2c3e50; | ||||||
|  |  | ||||||
|  |   --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); | ||||||
|  |   --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); | ||||||
|  |   --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); | ||||||
|  |   --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); | ||||||
|  |  | ||||||
|  |   --vt-c-text-light-1: var(--vt-c-indigo); | ||||||
|  |   --vt-c-text-light-2: rgba(60, 60, 60, 0.66); | ||||||
|  |   --vt-c-text-dark-1: var(--vt-c-white); | ||||||
|  |   --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* semantic color variables for this project */ | ||||||
|  | :root { | ||||||
|  |   --color-background: var(--vt-c-white); | ||||||
|  |   --color-background-soft: var(--vt-c-white-soft); | ||||||
|  |   --color-background-mute: var(--vt-c-white-mute); | ||||||
|  |  | ||||||
|  |   --color-border: var(--vt-c-divider-light-2); | ||||||
|  |   --color-border-hover: var(--vt-c-divider-light-1); | ||||||
|  |  | ||||||
|  |   --color-heading: var(--vt-c-text-light-1); | ||||||
|  |   --color-text: var(--vt-c-text-light-1); | ||||||
|  |  | ||||||
|  |   --section-gap: 160px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (prefers-color-scheme: dark) { | ||||||
|  |   :root { | ||||||
|  |     --color-background: var(--vt-c-black); | ||||||
|  |     --color-background-soft: var(--vt-c-black-soft); | ||||||
|  |     --color-background-mute: var(--vt-c-black-mute); | ||||||
|  |  | ||||||
|  |     --color-border: var(--vt-c-divider-dark-2); | ||||||
|  |     --color-border-hover: var(--vt-c-divider-dark-1); | ||||||
|  |  | ||||||
|  |     --color-heading: var(--vt-c-text-dark-1); | ||||||
|  |     --color-text: var(--vt-c-text-dark-2); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | *, | ||||||
|  | *::before, | ||||||
|  | *::after { | ||||||
|  |   box-sizing: border-box; | ||||||
|  |   margin: 0; | ||||||
|  |   position: relative; | ||||||
|  |   font-weight: normal; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body { | ||||||
|  |   min-height: 100vh; | ||||||
|  |   color: var(--color-text); | ||||||
|  |   background: var(--color-background); | ||||||
|  |   transition: color 0.5s, background-color 0.5s; | ||||||
|  |   line-height: 1.6; | ||||||
|  |   font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, | ||||||
|  |     Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; | ||||||
|  |   font-size: 15px; | ||||||
|  |   text-rendering: optimizeLegibility; | ||||||
|  |   -webkit-font-smoothing: antialiased; | ||||||
|  |   -moz-osx-font-smoothing: grayscale; | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								frontend/src/assets/logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/assets/logo.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"  xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg> | ||||||
| After Width: | Height: | Size: 308 B | 
							
								
								
									
										12
									
								
								frontend/src/assets/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/assets/main.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | @import "./base.css"; | ||||||
|  |  | ||||||
|  | div#app { | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100vh; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .screenshots-drawer { | ||||||
|  |   position: fixed; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  | } | ||||||
							
								
								
									
										118
									
								
								frontend/src/components/LogViewer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								frontend/src/components/LogViewer.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | |||||||
|  | <template> | ||||||
|  |   <div style="height: 100%; width: 100%"> | ||||||
|  |   <ag-grid-vue | ||||||
|  |       style="width: 100%; height: 100%;" | ||||||
|  |       class="ag-theme-alpine" | ||||||
|  |       @grid-ready="onGridReady" | ||||||
|  |       :defaultColDef="defaultColDef" | ||||||
|  |       :columnDefs="columnDefs" | ||||||
|  |       :row-data="null" | ||||||
|  |       :supress-horisontal-scroll="true" | ||||||
|  |       :enable-scrolling="true" | ||||||
|  |   ></ag-grid-vue> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { AgGridVue } from "ag-grid-vue3"; | ||||||
|  | import "ag-grid-community/styles//ag-grid.css"; | ||||||
|  | import "ag-grid-community/styles//ag-theme-alpine.css"; | ||||||
|  | import ScreenshotCell from "./ScreenshotCell.js"; | ||||||
|  | import {watchEffect} from "vue"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { | ||||||
|  |     AgGridVue, | ||||||
|  |     ScreenshotCell: ScreenshotCell, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       gridApi: null, | ||||||
|  |       gridColumnApi: null, | ||||||
|  |       rowData: [], | ||||||
|  |       defaultColDef: { | ||||||
|  |         width: 50, | ||||||
|  |         initialPinned: true, | ||||||
|  |         filter: true, | ||||||
|  |         floatingFilter: true, | ||||||
|  |         resizable: true, | ||||||
|  |       }, | ||||||
|  |       columnDefs:  [ | ||||||
|  |         { | ||||||
|  |           field: '_id', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           field: 'kubernetes.namespace', | ||||||
|  |           headerName: 'namespace', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           field: 'kubernetes.pod.name', | ||||||
|  |           headerName: 'pod', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           field: 'kubernetes.container.name', | ||||||
|  |           headerName: 'container', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           field: 'message', | ||||||
|  |           tooltipValueGetter: (params) => 'Address: ' + params.value, | ||||||
|  |           width: 500, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           field: 'stream', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           field: '@timestamp', | ||||||
|  |           width: 70 | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   created() { | ||||||
|  |     this.setupStream() | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     setupStream() { | ||||||
|  |       let es = new EventSource('/events'); | ||||||
|  |       es.onmessage = (e) => this.handleReceiveMessage(e) | ||||||
|  |     }, | ||||||
|  |     onGridReady(params) { | ||||||
|  |       this.gridApi = params.api; | ||||||
|  |       this.gridColumnApi = params.columnApi; | ||||||
|  |     }, | ||||||
|  |     handleReceiveMessage (event) { | ||||||
|  |       const eventData = this.parseEventData(event.data); | ||||||
|  |       this.rowData.unshift(eventData) | ||||||
|  |       this.gridApi.setRowData(this.rowData) | ||||||
|  |       this.gridApi.sizeColumnsToFit() | ||||||
|  |     }, | ||||||
|  |     parseEventData (eventData) { | ||||||
|  |       try { | ||||||
|  |         let json = JSON.parse(eventData) | ||||||
|  |         if (!json.message) { | ||||||
|  |           json.message = JSON.stringify(json.json) | ||||||
|  |         } | ||||||
|  |         return json | ||||||
|  |       } catch (e) { | ||||||
|  |         console.error(e, eventData) | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     setColumnDefs (json) { | ||||||
|  |       const keys = Object.keys(json) | ||||||
|  |       const defs = [] | ||||||
|  |       keys.map((key) => { | ||||||
|  |         const definition = { | ||||||
|  |           field: key, | ||||||
|  |           initialPinned: true, | ||||||
|  |           filter: true, | ||||||
|  |           floatingFilter: true, | ||||||
|  |           minWidth: 80 | ||||||
|  |         } | ||||||
|  |         defs.push(definition) | ||||||
|  |       }) | ||||||
|  |       this.columnDefs = defs | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | </script> | ||||||
							
								
								
									
										30
									
								
								frontend/src/components/ScreenshotCell.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/src/components/ScreenshotCell.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | export default { | ||||||
|  |     template: `<div> | ||||||
|  |       <a @click="openDrawer">View screenshots</a> | ||||||
|  |       <div v-if="drawerOpen" class="screenshots-drawer"> | ||||||
|  |         <img v-for="screenshot in screenshots" :src="screenshot.orig"/> | ||||||
|  |       </div> | ||||||
|  |     </div>`, | ||||||
|  |     data: function () { | ||||||
|  |         return { | ||||||
|  |             screenshots: [], | ||||||
|  |             drawerOpen: false, | ||||||
|  |         }; | ||||||
|  |     }, | ||||||
|  |     beforeMount() { | ||||||
|  |         this.updateImage(this.params); | ||||||
|  |         this.updateImage(this.params); | ||||||
|  |     }, | ||||||
|  |     methods: { | ||||||
|  |         updateImage(params) { | ||||||
|  |             this.screenshots = params.value | ||||||
|  |             this.value = params.value; | ||||||
|  |         }, | ||||||
|  |         refresh(params) { | ||||||
|  |             this.updateImage(params); | ||||||
|  |         }, | ||||||
|  |         openDrawer () { | ||||||
|  |             this.drawerOpen = true | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  | }; | ||||||
							
								
								
									
										11
									
								
								frontend/src/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/main.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | import { createApp } from 'vue' | ||||||
|  | import { createPinia } from 'pinia' | ||||||
|  | import App from './App.vue' | ||||||
|  |  | ||||||
|  | import './assets/main.css' | ||||||
|  |  | ||||||
|  | const app = createApp(App) | ||||||
|  |  | ||||||
|  | app.use(createPinia()) | ||||||
|  |  | ||||||
|  | app.mount('#app') | ||||||
							
								
								
									
										3
									
								
								frontend/src/set-public-path.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/set-public-path.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | import { setPublicPath } from "systemjs-webpack-interop"; | ||||||
|  |  | ||||||
|  | setPublicPath("app1"); | ||||||
							
								
								
									
										14
									
								
								frontend/src/stores/counter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/stores/counter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import { ref, computed } from 'vue' | ||||||
|  | import { defineStore } from 'pinia' | ||||||
|  |  | ||||||
|  | export const useCounterStore = defineStore('counter', () => { | ||||||
|  |   const count = ref(0) | ||||||
|  |   const doubleCount = computed(() => count.value * 2) | ||||||
|  |   function increment() { | ||||||
|  |     count.value++ | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   return { count, doubleCount, increment } | ||||||
|  | }) | ||||||
							
								
								
									
										21
									
								
								frontend/vite.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/vite.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import vue from '@vitejs/plugin-vue' | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   resolve: { | ||||||
|  |     alias: { | ||||||
|  |       vue: 'vue/dist/vue.esm-bundler.js' | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   rollupOptions: { | ||||||
|  |     input: 'src/main.js', | ||||||
|  |     format: 'system', | ||||||
|  |     preserveEntrySignatures: true | ||||||
|  |   }, | ||||||
|  |   plugins: [vue({ | ||||||
|  |     template: { | ||||||
|  |       transformAssetUrls: { | ||||||
|  |         base: '/src' | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   })], | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								frontend/vue.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/vue.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  |  | ||||||
|  | const path = require('path'); | ||||||
|  | const fs = require('fs'); | ||||||
|  | const EventHooksPlugin = require('event-hooks-webpack-plugin'); | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     publicPath: '/logs', | ||||||
|  |     chainWebpack: (config) => { | ||||||
|  |         config.devServer.headers({ | ||||||
|  |             'Access-Control-Allow-Origin': '*', | ||||||
|  |         }); | ||||||
|  |         config.devServer.set('port', 8080); | ||||||
|  |         config.devServer.set('hot', true); | ||||||
|  |  | ||||||
|  |         config.output.filename('[name].js'); | ||||||
|  |         config.output.publicPath('/logs'); | ||||||
|  |  | ||||||
|  |         config.externals([ | ||||||
|  |             'vue', | ||||||
|  |             'vue-router' | ||||||
|  |         ]); | ||||||
|  |     }, | ||||||
|  |     lintOnSave: true, | ||||||
|  |     filenameHashing: false, | ||||||
|  |     configureWebpack: { | ||||||
|  |         plugins: [ | ||||||
|  |             new EventHooksPlugin({ | ||||||
|  |                 done: () => { | ||||||
|  |                     if (process.env.NODE_ENV !== 'development') { | ||||||
|  |                         const buildDir = path.join(__dirname, '/dist'); | ||||||
|  |                         fs.unlinkSync(`${buildDir}/index.html`); | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |             }), | ||||||
|  |         ], | ||||||
|  |     }, | ||||||
|  | }; | ||||||
		Reference in New Issue
	
	Block a user