I'm trying to get HMR to work on the server-side with an express app and I'm seeing some odd behavior. My simple test project;
index.ts
let httpListener: Server = null;
let AppServer = require('./AppServer').default;
const port = Config.serverPort;
if (process.env.NODE_ENV === 'dev') {
if ((module as any).hot) {
(module as any).hot.addDisposeHandler((data: any) => {
httpListener.close();
AppServer = require('./AppServer').default;
});
console.log('index.ts', (module as any).hot.dependencies);
(module as any).hot.accept((err: any) => {
console.log('HMR Error', err);
});
}
}
httpListener = AppServer.app.listen(port, (error: Error) => {
if (error) {
console.error(error);
} else {
console.info(`Listening on port ${port}.`);
}
});
AppServer.ts
class AppServer {
public app: express.Application = express();
constructor() {
this.app.use('/api', (new ApiRouter()).router);
}
}
export default new AppServer();
and ApiRouter.ts
export class ApiRouter {
public router: express.Router = express.Router();
constructor() {
this.router.use('/auth', (new AuthRouter()).router);
this.router.get('/', (req, res) => {
res.json({success: true});
});
}
}
Webpack bundles correctly, and HMR reports modules being updated. If I change some code in index.ts, those changes take effect. However, when I flip {success: true} to {success: false}, I see HMR update;
[HMR] Updated modules:
[HMR] - ./src/server/AppServer.ts
[HMR] - ./src/server/index.ts
[HMR] - ./src/server/api/ApiRouter.ts
but when I hit the endpoint, I get back {success: true}. So despite HMR seemingly doing the right thing, the code isn't being changed at run-time. I suspect I'm missing something fundamental about how module.hot.accept works here, but I can not figure out where I'm going wrong.
Has anyone gotten this to work correctly?
Related
Using Vite's dev server, if I try to access a non-existent URL (e.g. localhost:3000/nonexistent/index.html), I would expect to receive a 404 error. Instead I receive a 200 status code, along with the contents of localhost:3000/index.html.
How can I configure Vite so that it returns a 404 in this situation?
(This question: Serve a 404 page with app created with Vue-CLI, is very similar but relates to the Webpack-based Vue-CLI rather than Vite.)
Vite 3
Vite 3.x introduced appType, which can be used to enable/disable the history fallback. Setting it to 'mpa' disables the history fallback while keeping the index.html transform and the 404 handler enabled. The naming is somewhat misleading, as it implies the mode is only for MPAs, but on the contrary, you can use this mode for SPAs:
import { defineConfig } from 'vite'
export default defineConfig({
appType: 'mpa', // disable history fallback
})
Note the history fallback normally rewrites / to /index.html, so you'd have to insert your own middleware to do that if you want to keep that behavior:
import { defineConfig } from 'vite'
const rewriteSlashToIndexHtml = () => {
return {
name: 'rewrite-slash-to-index-html',
apply: 'serve',
enforce: 'post',
configureServer(server) {
// rewrite / as index.html
server.middlewares.use('/', (req, _, next) => {
if (req.url === '/') {
req.url = '/index.html'
}
next()
})
},
}
}
export default defineConfig({
appType: 'mpa', // disable history fallback
plugins: [
rewriteSlashToIndexHtml(),
],
})
Vite 2
Vite 2.x does not support disabling the history API fallback out of the box.
As a workaround, you can add a Vite plugin that removes Vite's history API fallback middleware (based on #ChrisCalo's answer):
// vite.config.js
import { defineConfig } from 'vite'
const removeViteSpaFallbackMiddleware = (middlewares) => {
const { stack } = middlewares
const index = stack.findIndex(({ handle }) => handle.name === 'viteSpaFallbackMiddleware')
if (index > -1) {
stack.splice(index, 1)
} else {
throw Error('viteSpaFallbackMiddleware() not found in server middleware')
}
}
const removeHistoryFallback = () => {
return {
name: 'remove-history-fallback',
apply: 'serve',
enforce: 'post',
configureServer(server) {
// rewrite / as index.html
server.middlewares.use('/', (req, _, next) => {
if (req.url === '/') {
req.url = '/index.html'
}
next()
})
return () => removeViteSpaFallbackMiddleware(server.middlewares)
},
}
}
export default defineConfig({
plugins: [
removeHistoryFallback(),
],
})
One disadvantage of this plugin is it relies on Vite's own internal naming of the history fallback middleware, which makes this workaround brittle.
You could modify fallback middleware to change the default behaves, or anything else you want. Here is an example. https://github.com/legend-chen/vite-404-redirect-plugin
Here's an approach that doesn't try to check what's on disk (which yielded incorrect behavior for me).
Instead, this approach:
removes Vite's SPA fallback middleware
it uses Vite's built-in HTML transformation and returns /dir/index.html (if it exists) for /dir or /dir/ requests
404s for everything else
// express not necessary, but its API does simplify things
const express = require("express");
const { join } = require("path");
const { readFile } = require("fs/promises");
// ADJUST THIS FOR YOUR PROJECT
const PROJECT_ROOT = join(__dirname, "..");
function removeHistoryFallback() {
return {
name: "remove-history-fallback",
configureServer(server) {
// returned function runs AFTER Vite's middleware is built
return function () {
removeViteSpaFallbackMiddleware(server.middlewares);
server.middlewares.use(transformHtmlMiddleware(server));
server.middlewares.use(notFoundMiddleware());
};
},
};
}
function removeViteSpaFallbackMiddleware(middlewares) {
const { stack } = middlewares;
const index = stack.findIndex(function (layer) {
const { handle: fn } = layer;
return fn.name === "viteSpaFallbackMiddleware";
});
if (index > -1) {
stack.splice(index, 1);
} else {
throw Error("viteSpaFallbackMiddleware() not found in server middleware");
}
}
function transformHtmlMiddleware(server) {
const middleware = express();
middleware.use(async (req, res, next) => {
try {
const rawHtml = await getIndexHtml(req.path);
const transformedHtml = await server.transformIndexHtml(
req.url, rawHtml, req.originalUrl
);
res.set(server.config.server.headers);
res.send(transformedHtml);
} catch (error) {
return next(error);
}
});
// named function for easier debugging
return function customViteHtmlTransformMiddleware(req, res, next) {
middleware(req, res, next);
};
}
async function getIndexHtml(path) {
const indexPath = join(PROJECT_ROOT, path, "index.html");
return readFile(indexPath, "utf-8");
}
function notFoundMiddleware() {
const middleware = express();
middleware.use((req, res) => {
const { method, path } = req;
res.status(404);
res.type("html");
res.send(`<pre>Cannot ${method} ${path}</pre>`);
});
return function customNotFoundMiddleware(req, res, next) {
middleware(req, res, next);
};
}
module.exports = {
removeHistoryFallback,
};
What's funny is that Vite seems to take the stance that:
it's a dev and build tool only, it's not to be used in production
built files are meant to be served statically, therefore, it doesn't come with a production server
However, for static file servers:
some configurations of static file servers will return index files when a directory is requested
they generally don't fallback to serving index.html when a file is not found and instead return a 404 in those situations
Therefore, it doesn't make much sense that Vite's dev server has this fallback behavior when it's targeting production environments that don't have it. It would be nice if there were a "correct" way to just turn off the history fallback while keeping the rest of the serving behavior (HTML transformation, etc).
I am using Vue with axios like
[...]
import VueAxios from "vue-axios";
import axios from "#/axios";
createApp(App)
.use(store)
.use(router)
.use(VueAxios, axios)
.mount("#app");
[...]
which works really great globally like this.axios... everywhere. I am also using Passport for authentification and in my protected route I would like to call my Express-endpoint .../api/is-authenticated to check if the user is logged in or not.
To make this work I would like to use the beforeRouteEnter-navigation guard, but unfortunately I can't call this in there.
Right now I am having in in the mounted-hook, which feels wrong. Is there any solution with keeping my code straight and clean?
I'd appreciate a hint. Thanks.
Edit: This worked for me.
beforeRouteEnter(to, from, next) {
next((vm) => {
var self = vm;
self
.axios({ method: "get", url: "authenticate" })
.then() //nothing needed here to continue?
.catch((error) => {
switch (error.response.status) {
case 401: {
return { name: "Authentification" }; //redirect
//break;
}
default: {
return false; //abort navigation
//break;
}
}
});
});
With beforeRouteEnter there is access to the component instance by passing a callback to next. So instead of this.axios, use the following:
beforeRouteEnter (to, from, next) {
next(vm => {
console.log(vm.axios); // `vm` is the instance
})
}
Here's a pseudo-example with an async request. I prefer async/await syntax but this will make it clearer what's happening:
beforeRouteEnter(to, from, next) {
const url = 'https://jsonplaceholder.typicode.com/posts';
// ✅ Routing has not changed at all yet, still looking at last view
axios.get(url).then(res => {
// ✅ async request complete, still haven't changed views
// Now test the result in some way
if (res.data.length > 10) {
// Passed the test. Carry on with routing
next(vm => {
vm.myData = res.data; // Setting a property before the component loads
})
} else {
// Didn't like the result, redirect
next('/')
}
})
}
i used to write pwa via vanilla javascript like this
importScripts('/src/js/idb.js');
importScripts('/src/js/utility.js');
const CACHE_STATIC_NAME = 'static-v4';
const CACHE_DYNAMIC_NAME = 'dynamic-v2';
const STATIC_FILES = [
'/',
'/index.html',
'/offline.html',
'/src/js/app.js',
'/src/js/feed.js',
'/src/js/promise.js',
'/src/js/fetch.js',
'/src/js/idb.js',
'/src/js/material.min.js',
'/src/css/app.css',
'/src/css/feed.css',
'/src/images/main-image.jpg',
'https://fonts.googleapis.com/css?family=Roboto:400,700',
'https://fonts.googleapis.com/icon?family=Material+Icons',
'https://cdnjs.cloudflare.com/ajax/libs/material-design-lite/1.3.0/material.indigo-pink.min.css'
];
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open(CACHE_STATIC_NAME)
.then(function(cache) {
console.log('[Service Worker] Installing Service Worker ...');
cache.addAll(STATIC_FILES);
})
);
});
self.addEventListener('activate', function(e) {
console.log('[Service Worker] Activating Service Worker ...');
// clear old cache
e.waitUntil(
caches.keys()
.then(function(cachedKeys) {
return Promise.all(cachedKeys.map(function(key) {
if(key !== CACHE_STATIC_NAME && key !== CACHE_DYNAMIC_NAME) {
return caches.delete(key);
}
}))
})
);
// Tell the active service worker to take control of the page immediately.
return self.clients.claim(); // to ensure that activating is correctly done
});
//After install, fetch event is triggered for every page request
self.addEventListener('fetch', function(event) {
let url = 'https://pwa-training-4a918.firebaseio.com/posts.json';
if(event.request.url === url) {
event.respondWith(
fetch(event.request).then(res => {
let clonedRes = res.clone();
// in order to clear ol data if new data is different from the original one
clearAllData('posts')
.then(() => {
return clonedRes.json()
})
.then(data => {
for(let key in data) {
writeData('posts', data[key])
}
});
return res;
})
);
// USE Cache only Strategy if the request is in the static Files
} else if(STATIC_FILES.includes(event.request.url)) {
event.respondWith(
caches.match(event.request)
);
} else {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(response => {
return caches.open(CACHE_DYNAMIC_NAME).then(cache => {
cache.put(event.request, response.clone());
return response;
})
})
})
.catch(err => {
return caches.open(CACHE_STATIC_NAME).then(cache => {
// i need to show offline page only if the failure is in the help Page
// because it does not make any sence if i show this page in case of the failure in files like css
if(event.request.headers.get('accept').includes('text/html')) {
return cache.match('/offline.html');
}
})
})
);
}
});
but when I'm trying to write my own in vuejs app I installed pwa via vue add pwa it created for me a file called registerServiceWorker.js that I don't understand because I'm not used to use it
This file contains the following
/* eslint-disable no-console */
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
)
},
registered () {
console.log('Service worker has been registered.')
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}
I don't know how to write my own pwa code here or where I can do that?
Also I don't know if it will work on localhost or not because from what I'm noticing it works in Production
So My Question is, How Can I Write PWA As I used to do with vanilla js in vue app? What are the steps should I do in order to accomplish my full custom PWA?
Can I Do That without using workbox?
if anyone can help me i'll be appreciated.
Thanks in advance.
I/(pretty sure most of us) won't likely throw to redo service worker from scratch in any project, Workbox is also recommended tools in Google Developers' page other than Vue CLI.
As the registerServiceWorker.js, that's boilerplate for your service worker cycle in your App, as the logs pretty straightforward in the flow of your app process
If you wanna to do from scratch still, i would suggest read https://developers.google.com/web/fundamentals/primers/service-workers/ to understand the fundamentals. I would recommend because service-worker pretty much "I hope you know what you doing with your app like what-when-to update/caching/do-when-offline/"
Given the example official Nuxt end-to-end test example using Ava:
import test from 'ava'
import { Nuxt, Builder } from 'nuxt'
import { resolve } from 'path'
// We keep a reference to Nuxt so we can close
// the server at the end of the test
let nuxt = null
// Init Nuxt.js and start listening on localhost:4000
test.before('Init Nuxt.js', async t => {
const rootDir = resolve(__dirname, '..')
let config = {}
try { config = require(resolve(rootDir, 'nuxt.config.js')) } catch (e) {}
config.rootDir = rootDir // project folder
config.dev = false // production build
config.mode = 'universal' // Isomorphic application
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
nuxt.listen(4000, 'localhost')
})
// Example of testing only generated html
test('Route / exits and render HTML', async t => {
let context = {}
const { html } = await nuxt.renderRoute('/', context)
t.true(html.includes('<h1 class="red">Hello world!</h1>'))
})
// Close the Nuxt server
test.after('Closing server', t => {
nuxt.close()
})
How can you use Nuxt or Builder to configure/access the applications Vuex store? The example Vuex store would look like:
import Vuex from "vuex";
const createStore = () => {
return new Vuex.Store({
state: () => ({
todo: null
}),
mutations: {
receiveTodo(state, todo) {
state.todo = todo;
}
},
actions: {
async nuxtServerInit({ commit }, { app }) {
console.log(app);
const todo = await app.$axios.$get(
"https://jsonplaceholder.typicode.com/todos/1"
);
commit("receiveTodo", todo);
}
}
});
};
export default createStore;
Currently trying to run the provided Ava test, leads to an error attempting to access #nuxtjs/axios method $get:
TypeError {
message: 'Cannot read property \'$get\' of undefined',
}
I'd be able to mock $get and even $axios available on app in Vuex store method nuxtServerInit, I just need to understand how to access app in the test configuration.
Thank you for any help you can provide.
Just encountered this and after digging so many tutorial, I pieced together a solution.
You have essentially import your vuex store into Nuxt when using it programmatically. This is done by:
Importing Nuxt's config file
Adding to the config to turn off everything else but enable store
Load the Nuxt instance and continue your tests
Here's a working code (assuming your ava and dependencies are set up)
// For more info on why this works, check this aweomse guide by this post in getting this working
// https://medium.com/#brandonaaskov/how-to-test-nuxt-stores-with-jest-9a5d55d54b28
import test from 'ava'
import jsdom from 'jsdom'
import { Nuxt, Builder } from 'nuxt'
import nuxtConfig from '../nuxt.config' // your nuxt.config
// these boolean switches turn off the build for all but the store
const resetConfig = {
loading: false,
loadingIndicator: false,
fetch: {
client: false,
server: false
},
features: {
store: true,
layouts: false,
meta: false,
middleware: false,
transitions: false,
deprecations: false,
validate: false,
asyncData: false,
fetch: false,
clientOnline: false,
clientPrefetch: false,
clientUseUrl: false,
componentAliases: false,
componentClientOnly: false
},
build: {
indicator: false,
terser: false
}
}
// We keep a reference to Nuxt so we can close
// the server at the end of the test
let nuxt = null
// Init Nuxt.js and start listening on localhost:5000 BEFORE running your tests. We are combining our config file with our resetConfig using Object.assign into an empty object {}
test.before('Init Nuxt.js', async (t) => {
t.timeout(600000)
const config = Object.assign({}, nuxtConfig, resetConfig, {
srcDir: nuxtConfig.srcDir, // don't worry if its not in your nuxt.config file. it has a default
ignore: ['**/components/**/*', '**/layouts/**/*', '**/pages/**/*']
})
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
nuxt.listen(5000, 'localhost')
})
// Then run our tests using the nuxt we defined initially
test.serial('Route / exists and renders correct HTML', async (t) => {
t.timeout(600000) // Sometimes nuxt's response is slow. We increase the timeont to give it time to render
const context = {}
const { html } = await nuxt.renderRoute('/', context)
t.true(html.includes('preload'))
// t.true(true)
})
test.serial('Route / exits and renders title', async (t) => {
t.timeout(600000)
const { html } = await nuxt.renderRoute('/', {})
const { JSDOM } = jsdom // this was the only way i could get JSDOM to work. normal import threw a functione error
const { document } = (new JSDOM(html)).window
t.true(document.title !== null && document.title !== undefined) // simple test to check if site has a title
})
Doing this should work. HOWEVER, You may still get some errors
✖ Timed out while running tests. If you get this you're mostly out of luck. I thought the problem was with Ava given that it didn't give a descriptive error (and removing any Nuxt method seemed to fix it), but so far even with the above snippet sometimes it works and sometimes it doesn't.
My best guess at this time is that there is a delay on Nuxt's side using either renderRouter or renderAndGetWindow that ava doesn't wait for, but on trying any of these methods ava almost immediately "times out" despite the t.timeout being explicitly set for each test. So far my research has lead me to checking the timeout for renderAndGetWindow (if it exists, but the docs doesn't indicate such).
That's all i've got.
I'm using Express Graphql server with react native and Relay. My device does connects to the subscription but it does not subscribe to it. Here's my index.js on the server
const subscriptionServer = SubscriptionServer.create(
{
execute,
subscribe,
schema,
onOperation: (message, params, webSocket) => {
console.log(params)
return params;
},
onConnect: () => {
// My device does connects
console.log("client connected")
}
},
{
server,
path: '/subscriptions'
},
);
app.use('/graphql', graphqlHTTP({
schema,
graphiql: true
}));
app.use('/graphiql', graphiqlExpress({
endpointURL: '/graphql',
subscriptionsEndpoint: `ws://127.0.0.1:8080/subscriptions`
}));
server.listen(PORT, ()=> {
console.log("Groceries running on port " + PORT)
console.log(
`subscriptions is now running on ws://localhost:${PORT}/subscriptions'}`
);
});
The resolver for subscription on the server, it was quite troublesome to figure out since everyone is using executable schema from apolloGraphql.
export default {
type: OrderEdges,
args: {
ShopId: {type: GraphQLID},
},
subscribe: withFilter(() => pubsub.asyncIterator('orderConfirmed'), (payload, variables) => {
console.log(payload)
console.log(variables)
return payload.orderConfirmed.node.ShopId == variables.ShopId;
}),
}
Now the react-native client. My subscription setup with relay environment.
const setupSubscriptions = (config, variables, cacheConfig, observer) => {
const query = config.text; //does console logs the query
const subscriptionClient = new SubscriptionClient(`ws://192.168.0.100:8080/subscriptions`, {reconnect:true});
subscriptionClient.request({query, variables}, (err, result) => {
console.log(err) // doesn't get call inside the request method
observer.onNext(data:result)
})
}
My subscription method,
export default function() {
const variables = {
ShopId: shop.getShop()[0].id
}
requestSubscription(
environment,
{
subscription,
variables,
onCompleted: (res, err) => {
console.log(res)
console.log(err)
},
updater: (store) => {...},
onError: error => console.error(error),
onNext: (response) => {console.log(response)}
});
}
the component where I'm calling to subscribe,
import subscription from '../../GraphQLQueries/subscriptions/orderConfirmed';
class OrdersBox extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
//initializing subscription
orderSubscriptions();
}
When the device starts the app, my device is connected to the web socket as I can see the console.log statement inside the onConnect method in SubscriptionServer. But when the payload is published after a mutation, the subscribe method doesn't get called. I can't seem to figure out what I'm doing wrong. Maybe it's some react-native specific config that I'm missing cuz everything seems to work fine when I test it on graphiql.
I can't find any example of react-native and relay subscriptions used with express graphql.
note: Everything is working when I use subscription with graphiql. But not with react-native and relay.
Thanks in advance guys
....
I wasn't returning the subscriptionClient.request method. Adding a return statement solved the problem. You don't have to return when using subscribe method in subscriptions-transport-ws#0.8.3. But version 0.9.1 replaces the subscribe function with request which does require it to return.
try:
function setupSubscription(config, variables, cacheConfig, observer) {
const query = config.text;
const subscriptionClient = new SubscriptionClient(websocketURL, {
reconnect: true
});
const client = subscriptionClient.request({ query, variables }).subscribe({
next: result => {
observer.onNext({ data: result.data });
},
complete: () => {
observer.onCompleted();
},
error: error => {
observer.onError(error);
}
});
return {
dispose: client.unsubscribe
};
}
subscriptions-transport-ws#0.9.1