-
-
Notifications
You must be signed in to change notification settings - Fork 6.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inconsistent URL trailing slash behavior between dev and preview servers #6596
Comments
It seems that it's because Vite only handles trailing slash in dev:
I tried to refactored to |
This should be configurable in order to properly emulate a production CDN which could be configured either way. |
Was trying to work this out locally and wrote a plugin that seems to fix it: https://gist.github.com/emma-k-alexandra/47ef18239e8a1e517160aff591e8132d // forward-to-trailing-slash-plugin.js
/**
* Forwards routes in the given list to a route with a trailing slash in the dev server
* Useful for multi page vite apps where all rollup inputs are known.
*
* Vite fix is upcoming, which will make this plugin unnecessary
* https://github.com/vitejs/vite/issues/6596
*/
export default routes => ({
name: 'forward-to-trailing-slash',
configureServer(server) {
server.middlewares.use((req, _res, next) => {
const requestURLwithoutLeadingSlash = req.url.substring(1)
if (routes.includes(requestURLwithoutLeadingSlash)) {
req.url = `${req.url}/`
}
next()
})
}
}) Example config: // vite.config.js
import { defineConfig } from 'vite'
import forwardToTrailingSlashPlugin from './forward-to-trailing-slash-plugin.js'
const build = {
rollupOptions: {
input: {
main: new URL('./index.html', import.meta.url).href,
photography: new URL('./photography/index.html', import.meta.url).href
}
}
}
export default defineConfig({
build,
plugins: [
forwardToTrailingSlashPlugin(Object.keys(build.rollupOptions.input))
]
}) |
We've hit this inconsistency moving from Create React App/Craco to Vite. We used to have Seems an annoying rule? |
Now that Nuxt is also using vite, I imagine this is going to cause a lot more headaches |
This works fine but doesn't return assets(css styles, javascript or typescript files) in the directory so I upgraded the plugin to this: import { ViteDevServer } from "vite"
export default (routes: string[]) => ({
name: "forward-to-trailing-slash",
configureServer(server: ViteDevServer) {
server.middlewares.use((req, res, next) => {
const assets = ["ts", "css", "js"]
const requestURLwithoutLeadingSlash = req?.url?.substring(1)
const referrerWithoutTrailingSlash = req.headers.referer?.split("/").pop()
const fileExtension = req.url?.split(".").pop()
if (routes.includes(requestURLwithoutLeadingSlash || "")) {
req.url = `${req.url}/`
}
if(routes.includes(referrerWithoutTrailingSlash || "") && assets.includes(fileExtension || "")) {
req.url = `/${referrerWithoutTrailingSlash}${req.url}`
}
next()
})
}
}) |
I refactor this to something similar: import { ViteDevServer } from 'vite';
const assetExtensions = new Set([
'cjs',
'css',
'graphql',
'ico',
'jpeg',
'jpg',
'js',
'json',
'map',
'mjs',
'png',
'sass',
'scss',
'svg',
'ts',
'tsx',
]);
export default () => ({
name: 'forward-to-trailing-slash',
configureServer(server: ViteDevServer) {
server.middlewares.use((req, res, next) => {
const { url, headers } = req;
const startsWithAt = url?.startsWith('/@');
if (startsWithAt) {
return next();
}
const startsWithDot = url?.startsWith('/.');
if (startsWithDot) {
return next();
}
const realUrl = new URL(
url ?? '.',
`${headers[':scheme'] ?? 'http'}://${headers[':authority'] ?? headers.host}`,
);
const endsWithSlash = realUrl.pathname.endsWith('/');
if (!endsWithSlash) {
const ext = realUrl.pathname.split('.').pop();
if (!ext || !assetExtensions.has(ext)) {
realUrl.pathname = `${realUrl.pathname}/`;
req.url = `${realUrl.pathname}${realUrl.search}`;
}
}
return next();
});
},
}); EDIT: This does not work with |
Had same issue, here's another solution using regex {
name: "forward-to-trailing-slash",
configureServer: (server) => {
server.middlewares.use((req, res, next) => {
if (!req.url) {
return next();
}
const requestURL = new URL(req.url, `http://${req.headers.host}`);
if (/^\/(?:[^@]+\/)*[^@./]+$/g.test(requestURL.pathname)) {
requestURL.pathname += "/";
req.url = requestURL.toString();
}
return next();
});
},
} Regex represents
Edit |
As an easy workaround for simple cases you may get away with just adding a simple redirect in your main/root app from the nested app's URL without trailing slash to the same URL with trailing slash. For example in my React project to workaround this issue for one nested app (app using function RedirectToNestedSite() {
// Redirect to nested app without keeping any state from this app
window.location.replace(`/nested/`);
return null;
} |
@Haprog I don't think you are talking about the same issue. This isn't something that be fixed with client side routing as the wrong bundle will be loaded. See the original post. There are two different bundles being served |
So that’s it, no wonder I use |
That's why my suggested workaround modifies |
My version for Vue 3 and Vuetify, with // Plugins
import vue from '@vitejs/plugin-vue'
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import forwardToTrailingSlashPlugin from './forward-to-trailing-slash-plugin.js'
import anotherEntrypointIndexHtml from "./another-entrypoint-index-html";
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
import { resolve } from 'path';
// https://vitejs.dev/config/
const base = "/front2";
export default defineConfig({
base: base,
build: {
rollupOptions: {
input: {
appMain: resolve(__dirname, 'index.html'),
appBlog: resolve(__dirname, 'blog', 'index.html'),
},
},
},
plugins: [
forwardToTrailingSlashPlugin(base),
anotherEntrypointIndexHtml(base, "/blog"),
vue({
template: { transformAssetUrls }
}),
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
vuetify({
autoImport: true,
styles: {
configFile: 'src/styles/settings.scss',
},
}),
],
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
server: {
port: 3000,
strictPort: true,
},
})
// workaround - removes the need of the trailing slash https://github.com/vitejs/vite/issues/6596
export default (base) => ({
name: 'forward-to-trailing-slash',
configureServer(server) {
server.middlewares.use((req, res, next) => {
const { url, headers } = req;
const normalizedBase = base ? base : "";
const startsWithAt = url?.startsWith(`${normalizedBase}/@`);
if (startsWithAt) {
return next();
}
// needed for dynamic routing components in vue
const startsWithSrc = url?.startsWith(`${normalizedBase}/src`);
if (startsWithSrc) {
return next();
}
const startsNodeModules = url?.startsWith(`${normalizedBase}/node_modules`);
if (startsNodeModules) {
return next();
}
const realUrl = new URL(
url ?? '.',
`${headers[':scheme'] ?? 'http'}://${headers[':authority'] ?? headers.host}`,
);
const endsWithSlash = realUrl.pathname.endsWith('/');
if (!endsWithSlash) {
realUrl.pathname = `${realUrl.pathname}/`;
req.url = `${realUrl.pathname}${realUrl.search}`;
}
return next();
});
},
});
// fixes the first load the appropriate index.html for /blog/<whatever>
export default (base, subroute) => ({
name: "another-entrypoint-index-html",
configureServer(server) {
server.middlewares.use(
(req, res, next) => {
const { url, headers } = req;
const realUrl = new URL(
url ?? '.',
`${headers[':scheme'] ?? 'http'}://${headers[':authority'] ?? headers.host}`,
);
if (realUrl.pathname.startsWith(`${base}${subroute}`)) {
realUrl.pathname = `${base}${subroute}/index.html`;
req.url = `${realUrl.pathname}${realUrl.search}`;
}
return next();
}
)
}
})
Urls are
{
"name": "frontend2",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --fix --ignore-path .gitignore"
},
"dependencies": {
"typeface-roboto": "1.1.13",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@mdi/font": "7.2.96",
"@tiptap/extension-color": "2.0.3",
"@tiptap/extension-highlight": "2.0.3",
"@tiptap/extension-image": "2.0.3",
"@tiptap/extension-link": "2.0.3",
"@tiptap/extension-mention": "2.0.3",
"@tiptap/extension-placeholder": "2.0.3",
"@tiptap/extension-text-style": "2.0.3",
"@tiptap/extension-underline": "2.0.3",
"@tiptap/pm": "2.0.3",
"@tiptap/starter-kit": "2.0.3",
"@tiptap/suggestion": "2.0.3",
"@tiptap/vue-3": "2.0.3",
"axios": "^1.4.0",
"core-js": "^3.31.1",
"date-fns": "^2.30.0",
"graphql-ws": "^5.11.2",
"lodash": "^4.17.21",
"mark.js": "^8.11.1",
"mitt": "^3.0.1",
"pinia": "^2.1.4",
"splitpanes": "^3.1.5",
"uuid": "^9.0.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vuetify": "3.3.15"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"eslint": "^8.45.0",
"eslint-plugin-vue": "^9.15.1",
"sass": "^1.63.6",
"stylus": "^0.59.0",
"vite": "^4.4.4",
"vite-plugin-vuetify": "^1.0.2"
}
} |
These workarounds are horrendous. It would be great if something could be merged that fixes this issue. |
@bluwy |
Describe the bug
Multi-page apps created with Vite do not behave consistently between dev and build preview when visiting nested URLs that do not have a trailing slash.
Using the following folder structure:
Expected: Both dev and build servers have consistent behavior when visiting
<root>/nested
Actual: Dev server shows
index.html
from root when visiting<root>/nested
; must use<root>/nested/
instead. Build preview, however, showsnested/index.html
when visiting<root>/nested
.Reproduction
https://github.com/noahmpauls/vite-bug-multipage-url
System Info
Used Package Manager
npm
Logs
No response
Validations
The text was updated successfully, but these errors were encountered: