Vite - Rollup node_modules not defined as modules - node-modules

I have a Webpack project which I am migrating to Vite. I have the dev server working, and now I am trying to get the build to work. I have run into an issue where a number of my dependencies cannot be imported as modules because they must be global. This includes Backbone, jQuery, moment.js, and DevExtreme. (I will be slowly getting rid of these dependencies, but for now, there is a large code base which is based on them)
I first tried to have them loaded as modules and manually add them to the global context, but that failed because when importing you can't manage the order in which the files are loaded. Backbone and DevExtreme require jQuery to be loaded in first. If it isn't, the page fails to load.
So I have a list of node_modules referenced directly from my html entry points.
Here is a simplified example of what I currently have.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1"
/>
<title>rollup test</title>
<script src="/node_modules/jquery/dist/jquery.min.js"></script>
</head>
<body>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
oldMain.js
$(function() {
$("body").append("Here is some text");
});
main.ts
import './oldMain.js';
vite.config.ts
import path from "path";
import { defineConfig } from "vite";
export default defineConfig({
build: {
rollupOptions: {
input: {
index: path.resolve(__dirname, "index.html"),
},
}
}
});
package.json:
{
"name": "issue-example",
"version": "1.0.0",
"main": "index.html",
"scripts": {
"build": "vue-tsc --noEmit && vite build"
},
"dependencies": {
"jquery": "^3.6.0",
"vue": "^3.2.33",
},
"devDependencies": {
"typescript": "^4.6.3",
"vite": "^2.9.5",
"vue-tsc": "^0.34.10",
}
}
At the moment, the only way forward that I can see is to write a rollup plugin that extracts the list of non-module scripts from the html file, gets the target files from their paths, copies them to the dist/assets directory, and changes the src paths in the html to the new location.
Is there a better way? A plugin that already exists that knows how to do this? Is there a different way to include these dependencies so they will be globally available?

The solution I ended up going with is:
I use handlebars to create the index.html file the necessary scripts.
Have a .ts file which decides what scripts need to be in the index.html file. It checks whether the current build is dev or prod and puts the relevant path in for each script.
My index.html looks like:
<html>
<head>
[... meta, base, title, link, etc.]
{{{injectScripts externalScripts}}}
</head>
<body>
[... etc]
</body>
</html>
I have a simple handlebars plugin called injectScripts which simply does that - it puts the scripts in where I call the it in the html. Not my favorite solution to a problem, but it works well enough.

Related

how to link external css and js in laravel 8/9 jetstream inertia js

I am working on a laravel 8 project for learning purposes. I am new in jetstream, inertia.js here I can not understand how to add or link external CSS/js files in innertia.js.
Someone, please help me.
you can just add this on the main page layout :
<script src="{{ asset('js/data.js') }}"></script>
then add your js file in this path : public/js.yourfile.js
// Layout.vue
import { Head } from '#inertiajs/inertia-vue3'
<Head>
<title>My app</title>
<meta head-key="description" name="description" content="This is the default description" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<script src="your script location"></script>
</Head>
// About.vue
import { Head } from '#inertiajs/inertia-vue3'
<Head>
<title>About - My app</title>
<meta head-key="description" name="description" content="This is a page specific description" />
</Head>
Actually, there are many ways to link CSS/JS
Here I share a very simple way to link
first, you have to store your external file in the public directory for your project.
then in your index file
("asset" automatically locate public folder that's why you don't need to mention that.)
Note: same process for CSS file.

How to set up Alpine.js 3 with mix

I am having trouble getting started with Alpine.js v3 in a new project. This is my package.json and Alpine setup:
"devDependencies": {
"laravel-mix": "^6.0.19"
},
"dependencies": {
"alpinejs": "^3.0.6"
}
import Alpine from 'alpinejs'
window.Alpine = Alpine
window.Alpine.start()
Alpine.data('demo', () => ({
open: false,
toggle() {
this.open = !this.open
}
}))
driving this markup:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demo</title>
<script src="assets/app.js"></script>
</head>
<body>
<div x-data="demo">
<button #click="toggle">Expand</button>
<div x-show="open">Content...</div>
</div>
</body>
</html>
and I build my javascript using laravel-mix:
// webpack.mix.js
let mix = require('laravel-mix');
mix
.setPublicPath('public')
.js('src/app.js', 'public/assets');
The javascript build fine and the page loads without errors, but when I click the toggle button, the content expands as expected, but I get the following error in the browser console and the content won't hide on subsequent clicks.
Uncaught (in promise) TypeError: i is not a function app.js:2282:28
If I defer my script or move it to the bottom, the page is broken on load and I get the following:
Uncaught TypeError: undefined is not a non-null object app.js:1995:12
Can anyone help me fix my setup?
Most likely you just forget to add defer attribute to script tag
<script defer src="assets/app.js"></script>
But also as documentation says
If you imported Alpine into a bundle, you have to make sure you are registering any extension code IN BETWEEN when you import the Alpine global object, and when you initialize Alpine by calling Alpine.start().
So change your script to
import Alpine from 'alpinejs';
Alpine.data('demo', () => ({
open: false,
toggle() {
this.open = !this.open
}
}));
window.Alpine = Alpine;
// should be last
Alpine.start();
Note: if there are some errors like assignment to undeclared variable data you may change your import to import Alpine from 'alpinejs/dist/module.cjs';

Visual Studio Code - Debug With Chrome - Breakpoints Run At Start, Not When Met

EDIT: I have found the solution. Since others may make the same mistake, I will put the answer at the bottom.
I am using Visual Studio Code. I am new to it.
I have Debugger For Chrome and Debugger For Edge installed.
I have Live Server installed.
I have launch.json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Chrome",
"request": "launch",
"type": "pwa-chrome",
"url": "http://127.0.0.1:5500/a.html",
"webRoot": "${workspaceFolder}"
}
]
}
I have my basic html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="myfunc()">Click</button>
</body>
<script>
function myfunc(){
alert("clicked");
}
</script>
</html>
I have my breakpoint on the line alert("clicked");.
I click the Debugging icon on the left with the spider, and then click the green button to launch.
Now the weird bit:
The program launches and breaks immediately on the alert line, even though the button has not been pressed.
I continue.
But then when I click the button and expect the break point to be met. It is not. The alert occurs without breaking at all.
Anyone know what I am doing wrong?
Solution:
It does not like the <script> outside the body. If you change the html file to have the <script> inside the body, then it works fine.
Like so:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="myfunc()">Click</button>
<script>
function myfunc(){
alert("clicked");
}
</script>
</body>
</html>
I found out what was going wrong.
It does not like the outside the body. If you change the html file to have the inside the body, then it works fine.
Like so:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="myfunc()">Click</button>
<script>
function myfunc(){
alert("clicked");
}
</script>
</body>
</html>

How do paths work in Vue.component() statement?

I am trying to learn Laravel and Vue properly, after an initial attempt back in the spring. I'm still pretty new to both though.
I am using Laravel 8.x in Windows 10 along with Vue 2.6.12. I am working my way through a video on combining Vue with Laravel. The video is for an older version of Laravel, probably 5.5, and probably a slightly older Vue as well so that may well be the nature of my problem. Here's the link to the video. I'm at about the 8:00 minute mark.
The problem I'm having is that when I try to execute my code, Laravel doesn't see my Articles component.
My app.js file is at /resources/assets/js/app.js. Here is the code:
require('./bootstrap');
window.Vue = required('vue');
Vue.component ('Articles', require('./components/Articles.vue'));
const app = new Vue({
el: '#app'
});
The file that contains the script tag is at /resources/views/welcome.blade.php. Here is the code:
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Larticles App</title>
<link href="https://fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css">
</head>
<body>
<div id="app">
<div class="container">
<Articles></Articles>
</div>
</div>
<script src="{{ asset('js/app.js') }}">console.log("Made it to here!");</script>
</body>
</html>
The Article component is at /resources/assets/js/components/Articles.vue. Here is the code:
<template>
<div>
<h2>Articles</h2>
</div>
</template>
<script>
export default {
name: "Articles",
beforeCreate() {
console.log("Articles - beforeCreate()");
},
created() {
console.log("Articles - created()");
},
beforeMount() {
console.log("Articles - beforeMount()");
},
mounted() {
console.log("Articles - mounted");
}
}
</script>
<style lang="scss" scoped>
#app {
background-color: gold;
color: blue;
}
</style>
What do I need to change to make this work? I think the issue is the require portion of the Vue.component statement in app.js but I've tried every variation I can think of without success. I can't think of anything else to try! I can't find anything in the Vue manual on special syntax for this statement in Vue or Laravel.
I should mention some things. I've deviated slightly from what he does in the video as part of my troubleshooting. I've capitalized Articles in the first parameter of the Vue.component statement and I've also capitalized it in the container div of the welcome.blade.php file. (Initially, I wrote it all lower case in each of those places but it didn't work that way either.) I've also added several console.log statements in the components lifecycle hooks and in the script tag. Absolutely none of them appear in the console though.
Also, for some reason, my IDE, VS Code, insists on displaying in red in the welcome.blade.php. Red always makes me think of errors but there is no error message of any kind. If I write those tags as (and change the app.js accordingly), they stay red so I don't think this is a casing issue.
I should also mention that the Inspector shows the as . Shouldn't it be showing exactly what I have in my welcome.blade.php file?
Use lower case letters when importing the component
<articles></articles>
Try to set each component you're reuire as default. Then use lower case as mentioned above. See below.
Vue.component ('Articles', require('./components/Articles.vue').default);
<articles></articles>

How to use Laravel Mix and WorkBox?

I'm trying to build a PWA for my app; and have spent almost 48 hours trying to figure out how to make use of Workbox with Laravel Mix. What's ironical is that Google says Workbox is meant to make things easy!
Buh!
Okay, so far I've figured out that -
I will need to use the InjectManifest Plugin because I want to integrate Push notifications service in my Service Worker
I have no clue how to specifiy the paths for swSrc and swDest.
What code should go into my webpack.mix.js and whether I should have a temporary service-worker inside my resources/js folder to create a new service worker inside public/ folder.
Can someone help?
PS: I've read almost every blog and help article; but none talks about reliably using Workbox with Laravel mix. Would really appreciate some help here.
I have done a lot of research into this recently and whilst this may not be a full answer to your question, it should give you, or anyone else visiting this page, enough guidance to get started...
I will add to this answer as I learn and research more.
For the purposes of this answer, I will assume your service worker is called service-worker.js, however, you can obviously call it whatever you like.
Step 1 - Laravel Mix
Assuming you are using Dynamic Importing in your project (if you aren't, you should be), you will need to downgrade Laravel Mix to version 3. There is an acknowledged bug in Laravel Mix 4 that prevents CSS from bundling correctly and this will not be fixed until Webpack 5 is released.
In addition, the steps outlined in this answer are specifically configured for Laravel Mix 3.
Step 2 - Import or ImportScripts
The second issue to solve is whether to utilise the workbox-webpack-plugin for injecting the workbox global using importScripts or whether you should disable this (using importWorkboxFrom: 'disabled') and just individually import the specific modules you need...
The documentation states:
When using a JavaScript bundler, you don't need (and actually shouldn't use) the workbox global or the workbox-sw module, as you can import the individual package files directly.
This implies that we should be using import instead of injecting the importScripts.
However, there are various issues here:
We do not want service-worker.js to be included in the build manifest as this will be injected into the precache manifest
We do not want service-worker.js to be versioned in production i.e. the name should always be service-worker.js, not service-worker.123abc.js.
InjectManifest will fail to inject the manifest because the service-worker.js file will not exist at the time that it runs.
Therefore, in order to utilise import instead of importScripts, we must have two separate webpack (mix) configurations (see conclusion for guidance on how to do this). I am not 100% certain this is correct, but I will update my answer once I have received an answer to either of the following (please support them to increase chance of receiving an answer):
How to use Workbox with a Bundler (Webpack etc.)
https://github.com/GoogleChrome/workbox/issues/2207
Step 3 - File Structure
Assuming you are using InjectManifest and not GenerateSW, you will need to write your own service worker which will have the JS manifest injected into it by the webpack plugin on each build. This, quite simply, means you need to create a file in your source directory that will be used as the service worker.
Mine is located at src/js/service-worker.js (this will be different if you are building in a full Laravel project, I am simply using Laravel Mix in a standalone app)
Step 4 - Registering the Service Worker
There are various ways to do this; some like to inject inline JS into the HTML template, but others, myself included, simply register the service worker at the top of their app.js. Either way, the code should look something along the lines of:
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/service-worker.js');
});
}
Step 5 - Writing your Service Worker; workbox Global, or Module Importing
As mentioned in the previous quote from the documentation, it is encouraged to import the specifically required modules into your service worker, instead of utilising the workbox global or workbox-sw module.
For more information on how to use the individual modules, and how to actually write your service worker, see the following documentation:
https://developers.google.com/web/tools/workbox/guides/using-bundlers
Conclusion
Based on all of my research (which is still ongoing), I have taken the following approach outlined below.
Before reading, please bear in mind that this is configured for a standalone static PWA (i.e. not a full Laravel project).
/src/service-worker.js (the service worker)
When using a bundler such as webpack, it is advised to utlilise import to ensure you include only the necessary workbox modules. This is my service worker skeleton:
import config from '~/config'; // This is where I store project based configurations
import { setCacheNameDetails } from 'workbox-core';
import { precacheAndRoute } from 'workbox-precaching';
import { registerNavigationRoute } from 'workbox-routing';
// Set the cache details
setCacheNameDetails({
prefix: config.app.name.replace(/\s+/g, '-').toLowerCase(),
suffix: config.app.version,
precache: 'precache',
runtime: 'runtime',
googleAnalytics: 'ga'
});
// Load the assets to be precached
precacheAndRoute(self.__precacheManifest);
// Ensure all requests are routed to index.html (SPA)
registerNavigationRoute('/index.html');
/package.json
Splitting the Mix configuration
"scripts": {
"development": "npm run dev-service-worker && npm run dev-core",
"dev": "npm run development",
"dev-service-worker": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=service-worker.mix",
"dev-core": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=core.mix",
"watch": "npm run dev-core -- --watch",
"watch-poll": "npm run watch -- --watch-poll",
"production": "npm run prod-service-worker && npm run prod-core",
"prod": "npm run production",
"prod-service-worker": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=service-worker.mix",
"prod-core": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=core.mix"
}
Command Explanation
All standard commands will work in the same way as usual (i.e. npm run dev etc.). See known issue about npm run watch
npm run <environment>-service-worker will build just the service worker in the specified environment
npm run <environment>-core will build just the core application in the specified environment
Known Issues
If you are using an html template that utilises the webpack manifest then you may have issues with npm run watch. I have been unable to get this to work correctly as of yet
Downgrading to Laravel Mix 3
"devDependencies": {
"laravel-mix": "^3.0.0"
}
This can also be achieved by running npm install laravel-mix#3.0.0
/static/index.ejs
This HTML template is used to generate the single page application index.html. This template is dependant on the webpack manifest being injected.
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" class="no-js">
<head>
<!-- General meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="<%= config.meta.description %>">
<meta name="rating" content="General">
<meta name="author" content="Sine Macula">
<meta name="robots" content="index, follow">
<meta name="format-detection" content="telephone=no">
<!-- Preconnect and prefetch urls -->
<link rel="preconnect" href="<%= config.api.url %>" crossorigin>
<link rel="dns-prefetch" href="<%= config.api.url %>">
<!-- Theme Colour -->
<meta name="theme-color" content="<%= config.meta.theme %>">
<!-- General link tags -->
<link rel="canonical" href="<%= config.app.url %>">
<!-- Manifest JSON -->
<link rel="manifest" href="<%= StaticAsset('/manifest.json') %>" crossorigin>
<!-- ----------------------------------------------------------------------
---- Icon Tags
---- ----------------------------------------------------------------------
----
---- The following will set up the favicons and the apple touch icons to be
---- used when adding the app to the homescreen of an iPhone, and to
---- display in the head of the browser.
----
---->
<!--[if IE]>
<link rel="shortcut icon" href="<%= StaticAsset('/favicon.ico') %>">
<![endif]-->
<link rel="apple-touch-icon" sizes="72x72" href="<%= StaticAsset('/apple-touch-icon-72x72.png') %>">
<link rel="apple-touch-icon" sizes="120x120" href="<%= StaticAsset('/apple-touch-icon-120x120.png') %>">
<link rel="apple-touch-icon" sizes="180x180" href="<%= StaticAsset('/apple-touch-icon-180x180.png') %>">
<link rel="icon" type="image/png" sizes="16x16" href="<%= StaticAsset('/favicon-16x16.png') %>">
<link rel="icon" type="image/png" sizes="32x32" href="<%= StaticAsset('/favicon-32x32.png') %>">
<link rel="icon" type="image/png" sizes="192x192" href="<%= StaticAsset('/android-chrome-192x192.png') %>">
<link rel="icon" type="image/png" sizes="194x194" href="<%= StaticAsset('/favicon-194x194.png') %>">
<link rel="mask-icon" href="<%= StaticAsset('/safari-pinned-tab.svg') %>" color="<%= config.meta.theme %>">
<meta name="msapplication-TileImage" content="<%= StaticAsset('/mstile-144x144.png') %>">
<meta name="msapplication-TileColor" content="<%= config.meta.theme %>">
<!-- ----------------------------------------------------------------------
---- Launch Images
---- ----------------------------------------------------------------------
----
---- Define the launch 'splash' screen images to be used on iOS.
----
---->
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-640x1136.png') %>" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-750x1294.png') %>" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1242x2148.png') %>" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1125x2436.png') %>" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1536x2048.png') %>" media="(min-device-width: 768px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1668x2224.png') %>" media="(min-device-width: 834px) and (max-device-width: 834px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-2048x2732.png') %>" media="(min-device-width: 1024px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">
<!-- ----------------------------------------------------------------------
---- Application Tags
---- ----------------------------------------------------------------------
----
---- Define the application specific tags.
----
---->
<meta name="application-name" content="<%= config.app.name %>">
<meta name="apple-mobile-web-app-title" content="<%= config.app.name %>">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="<%= config.app.status_bar %>">
<meta name="mobile-web-app-capable" content="yes">
<meta name="full-screen" content="yes">
<meta name="browsermode" content="application">
<!-- ----------------------------------------------------------------------
---- Social Media and Open Graph Tags
---- ----------------------------------------------------------------------
----
---- The following will create objects for social media sites to read when
---- scraping the site.
----
---->
<!-- Open Graph -->
<meta property="og:site_name" content="<%= config.app.name %>">
<meta property="og:url" content="<%= config.app.url %>">
<meta property="og:type" content="website">
<meta property="og:title" content="<%= config.meta.title %>">
<meta property="og:description" content="<%= config.meta.description %>">
<meta property="og:image" content="<%= StaticAsset('/assets/images/brand/social-1200x630.jpg') %>">
<!-- Twitter -->
<meta name="twitter:card" content="app">
<meta name="twitter:site" content="<%= config.app.name %>">
<meta name="twitter:title" content="<%= config.meta.title %>">
<meta name="twitter:description" content="<%= config.meta.description %>">
<meta name="twitter:image" content="<%= StaticAsset('/assets/images/brand/social-440x220.jpg') %>">
<!-- ----------------------------------------------------------------------
---- JSON Linked Data
---- ----------------------------------------------------------------------
----
---- This will link the website to its associated social media page. This
---- adds to the credibility of the website as it allows search engines to
---- determine the following of the company via social media
----
---->
<script type="application/ld+json">
{
"#context": "http://schema.org",
"#type": "Organization",
"name": "<%= config.company.name %>",
"url": "<%= config.app.url %>",
"sameAs": [<%= '"' + Object.values(config.company.social).map(x => x.url).join('","') + '"' %>]
}
</script>
<!-- Define the page title -->
<title><%= config.meta.title %></title>
<!-- Generate the prefetch/preload links -->
<% webpack.chunks.slice().reverse().forEach(chunk => { %>
<% chunk.files.forEach(file => { %>
<% if (file.match(/\.(js|css)$/)) { %>
<link rel="<%= chunk.initial ? 'preload' : 'prefetch' %>" href="<%= StaticAsset(file) %>" as="<%= file.match(/\.css$/) ? 'style' : 'script' %>">
<% } %>
<% }) %>
<% }) %>
<!-- Include the core styles -->
<% webpack.chunks.forEach(chunk => { %>
<% chunk.files.forEach(file => { %>
<% if (file.match(/\.(css)$/) && chunk.initial) { %>
<link rel="stylesheet" href="<%= StaticAsset(file) %>">
<% } %>
<% }) %>
<% }) %>
</head>
<body ontouchstart="">
<!-- No javascript error -->
<noscript>JavaScript turned off...</noscript>
<!-- The Vue JS app element -->
<div id="app"></div>
<!-- Include the core scripts -->
<% webpack.chunks.slice().reverse().forEach(chunk => { %>
<% chunk.files.forEach(file => { %>
<% if (file.match(/\.(js)$/) && chunk.initial) { %>
<script type="text/javascript" src="<%= StaticAsset(file) %>"></script>
<% } %>
<% }) %>
<% }) %>
</body>
</html>
/service-worker.mix.js (building the service worker)
This mix configuration will build your Service Worker (service-worker.js), and place it into the root of /dist.
Note: I like to clean my dist folder each time I build my project, and as this functionality must be run at this stage of the build process, I have included it in the below configuration.
const mix = require('laravel-mix');
const path = require('path');
// Set the public path
mix.setPublicPath('dist/');
// Define all the javascript files to be compiled
mix.js('src/js/service-worker.js', 'dist');
// Load any plugins required to compile the files
const Dotenv = require('dotenv-webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// Define the required plugins for webpack
const plugins = [
// Grant access to the environment variables
new Dotenv,
// Ensure the dist folder is cleaned for each build
new CleanWebpackPlugin
];
// Extend the default Laravel Mix webpack configuration
mix.webpackConfig({
plugins,
resolve: {
alias: {
'~': path.resolve('')
}
}
});
// Disable mix-manifest.json (remove this for Laravel projects)
Mix.manifest.refresh = () => void 0;
/core.mix.js (building the application)
This mix configuration will build your main application and place it in /dist/js.
There are various key parts of this mix configuration, each of which has been clearly outlined in the comments within. These are the top-level areas:
Code splitting to app.js, manifest.js, and vendor.js (and dynamic importing)
Laravel Mix versioning does not work as needed for the HTML template so laravel-mix-versionhash is utilised instead
html-webpack-plugin is utilised to generate index.html based on the index.ejs template (see above)
webpack-pwa-manifest is utilised to generate a manifest based
copy-webpack-plugin is utilised to copy the static files to the /dist directory, and to copy any necessary icons to the site root
imagemin-webpack-plugin is used to compress any static images in production
workbox-webpack-plugin is used to inject the webpack manifest into the precaching array used in the service worker. InjectManifest is used, not GenerateSW
Any necessary manifest transformations are applied once the build process is complete
There may be additions to the above but pretty much everything is described by the comments in the following code:
const config = require('./config'); // This is where I store project based configurations
const mix = require('laravel-mix');
const path = require('path');
const fs = require('fs');
// Include any laravel mix plugins
// NOTE: not needed in Laravel projects
require('laravel-mix-versionhash');
// Set the public path
mix.setPublicPath('dist/');
// Define all the SASS files to be compiled
mix.sass('src/sass/app.scss', 'dist/css');
// Define all the javascript files to be compiled
mix.js('src/js/app.js', 'dist/js');
// Split the js into bundles
mix.extract([
// Define the libraries to extract to `vendor`
// e.g. 'vue'
]);
// Ensure the files are versioned when running in production
// NOTE: This is not needed in Laravel projects, you simply need
// run `mix.version`
if (mix.inProduction()) {
mix.versionHash({
length: 8
});
}
// Set any necessary mix options
mix.options({
// This doesn't do anything yet, but once the new version
// of Laravel Mix is released, this 'should' extract the
// styles from the Vue components and place them in a
// css file, as opposed to placing them inline
//extractVueStyles: true,
// Ensure the urls are not processed
processCssUrls: false,
// Apply any postcss plugins
postCss: [
require('css-declaration-sorter'),
require('autoprefixer')
]
});
// Disable mix-manifest.json
// NOTE: not needed in Laravel projects
Mix.manifest.refresh = () => void 0;
// Load any plugins required to compile the files
const Dotenv = require('dotenv-webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WebpackPwaManifest = require('webpack-pwa-manifest');
const { InjectManifest } = require('workbox-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ImageminPlugin = require('imagemin-webpack-plugin').default;
// Define the required plugins for webpack
const plugins = [
// Grant access to the environment variables
new Dotenv,
// Process and build the html template
// NOTE: not needed if using Laravel and blade
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'static', 'index.ejs'),
inject: false,
minify: !mix.inProduction() ? false : {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
useShortDoctype: true
},
templateParameters: compilation => ({
webpack: compilation.getStats().toJson(),
config,
StaticAsset: (file) => {
// This will ensure there are no double slashes (bug in Laravel Mix)
return (config.app.static_url + '/' + file).replace(/([^:]\/)\/+/g, "$1");
}
})
}),
// Generate the manifest file
new WebpackPwaManifest({
publicPath: '',
filename: 'manifest.json',
name: config.app.name,
description: config.meta.description,
theme_color: config.meta.theme,
background_color: config.meta.theme,
orientation: config.app.orientation,
display: "fullscreen",
start_url: '/',
inject: false,
fingerprints: false,
related_applications: [
{
platform: 'play',
url: config.app.stores.google.url,
id: config.app.stores.google.id
},
{
platform: 'itunes',
url: config.app.stores.apple.url,
id: config.app.stores.apple.id
}
],
// TODO: Update this once the application is live
screenshots: [
{
src: config.app.static_url + '/assets/images/misc/screenshot-1-720x1280.png',
sizes: '1280x720',
type: 'image/png'
}
],
icons: [
{
src: path.resolve(__dirname, 'static/assets/images/icons/android-chrome-512x512.png'),
sizes: [72, 96, 128, 144, 152, 192, 384, 512],
destination: path.join('assets', 'images', 'icons')
}
]
}),
// Copy any necessary directories/files
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, 'static'),
to: path.resolve(__dirname, 'dist'),
toType: 'dir',
ignore: ['*.ejs']
},
{
from: path.resolve(__dirname, 'static/assets/images/icons'),
to: path.resolve(__dirname, 'dist'),
toType: 'dir'
}
]),
// Ensure any images are optimised when copied
new ImageminPlugin({
disable: process.env.NODE_ENV !== 'production',
test: /\.(jpe?g|png|gif|svg)$/i
}),
new InjectManifest({
swSrc: path.resolve('dist/service-worker.js'),
importWorkboxFrom: 'disabled',
importsDirectory: 'js'
})
];
// Extend the default Laravel Mix webpack configuration
mix.webpackConfig({
plugins,
output: {
chunkFilename: 'js/[name].js',
}
}).then(() => {
// As the precached filename is hashed, we need to read the
// directory in order to find the filename. Assuming there
// are no other files called `precache-manifest`, we can assume
// it is the first value in the filtered array. There is no
// need to test if [0] has a value because if it doesn't
// this needs to throw an error
let filename = fs
.readdirSync(path.normalize(`${__dirname}/dist/js`))
.filter(filename => filename.startsWith('precache-manifest'))[0];
// In order to load the precache manifest file, we need to define
// self in the global as it is not available in node.
global['self'] = {};
require('./dist/js/' + filename);
let manifest = self.__precacheManifest;
// Loop through the precache manifest and apply any transformations
manifest.map(entry => {
// Remove any double slashes
entry.url = entry.url.replace(/(\/)\/+/g, "$1");
// If the filename is hashed then remove the revision
if (entry.url.match(/\.[0-9a-f]{8}\./)) {
delete entry.revision;
}
// Apply any other transformations or additions here...
});
// Filter out any entries that should not be in the manifest
manifest = manifest.filter(entry => {
return entry.url.match(/.*\.(css|js|html|json)$/)
|| entry.url.match(/^\/([^\/]+\.(png|ico|svg))$/)
|| entry.url.match(/\/images\/icons\/icon_([^\/]+\.(png))$/)
|| entry.url.match(/\/images\/misc\/splash-([^\/]+\.(png))$/);
});
// Concatenate the contents of the precache manifest and then save
// the file
const content = 'self.__precacheManifest = (self.__precacheManifest || []).concat(' + JSON.stringify(manifest) + ');';
fs.writeFileSync('./dist/js/' + filename, content, 'utf8', () => {});
});
/src/js/app.js (the main application)
This is where you register your service worker, and obviously define your application etc...
/**
* Register the service worker as soon as the page has finished loading.
*/
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
// TODO: Broadcast updates of the service worker here...
navigator.serviceWorker.register('/service-worker.js');
});
}
// Define the rest of your application here...
// e.g. window.Vue = require('vue');

Resources