From d5b47ca2675ea9db1364b4a623086426ed0e070f Mon Sep 17 00:00:00 2001 From: kenzycodex Date: Mon, 13 Jan 2025 01:52:32 +0100 Subject: [PATCH] Add clean URLs middleware with proper Content-Type headers and improved query handling --- live-server.js | 7 +++ middleware/clean-urls.js | 59 ++++++++++++++++++ test/clean-urls.test.js | 126 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 middleware/clean-urls.js create mode 100644 test/clean-urls.test.js diff --git a/live-server.js b/live-server.js index 9eac8f1..37a1807 100755 --- a/live-server.js +++ b/live-server.js @@ -14,6 +14,9 @@ var opts = { logLevel: 2, }; +// clean-urls middleware +// opts.middleware.push("clean-urls"); + var homeDir = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; var configPath = path.join(homeDir, '.live-server.json'); if (fs.existsSync(configPath)) { @@ -91,6 +94,10 @@ for (var i = process.argv.length - 1; i >= 2; --i) { opts.middleware.push("spa"); process.argv.splice(i, 1); } + else if (arg === "--clean-urls") { + opts.middleware.push("clean-urls"); + process.argv.splice(i, 1); + } else if (arg === "--quiet" || arg === "-q") { opts.logLevel = 0; process.argv.splice(i, 1); diff --git a/middleware/clean-urls.js b/middleware/clean-urls.js new file mode 100644 index 0000000..8198173 --- /dev/null +++ b/middleware/clean-urls.js @@ -0,0 +1,59 @@ +// middleware/clean-urls.js +var path = require('path'); +var fs = require('fs'); +var url = require('url'); + +module.exports = function(req, res, next) { + // Parse the URL to handle query parameters correctly + var parsedUrl = url.parse(req.url); + var urlPath = parsedUrl.pathname || ''; + + // Skip if requesting a non-HTML file + if (urlPath.match(/\.(?!html)[^./]+$/)) { + return next(); + } + + // Remove trailing slashes except for root + if (urlPath.length > 1 && urlPath.endsWith('/')) { + urlPath = urlPath.slice(0, -1); + // Reconstruct URL with query parameters + req.url = urlPath + (parsedUrl.search || ''); + return next(); + } + + // If URL ends with .html, redirect to clean URL + if (urlPath.endsWith('.html')) { + var cleanPath = urlPath.slice(0, -5); + var cleanUrl = cleanPath + (parsedUrl.search || ''); + res.writeHead(301, { + 'Location': cleanUrl, + 'Content-Type': 'text/html; charset=UTF-8', // Fixed case sensitivity + 'Cache-Control': 'no-cache' + }); + return res.end(); + } + + // Check if we need to serve an HTML file + var htmlPath = urlPath; + if (!htmlPath.endsWith('.html')) { + // If root path, use index.html + if (htmlPath === '/') { + htmlPath = '/index.html'; + } else { + htmlPath = htmlPath + '.html'; + } + } + + // Try to find the HTML file + var fullPath = path.join(process.cwd(), htmlPath); + fs.access(fullPath, fs.constants.F_OK, function(err) { + if (!err) { + // File exists, serve it but keep clean URL + req.url = htmlPath + (parsedUrl.search || ''); + } else if (urlPath.endsWith('.html')) { + // If .html was explicitly requested but doesn't exist, try without it + req.url = urlPath.slice(0, -5) + (parsedUrl.search || ''); + } + next(); + }); +}; \ No newline at end of file diff --git a/test/clean-urls.test.js b/test/clean-urls.test.js new file mode 100644 index 0000000..7a42ec9 --- /dev/null +++ b/test/clean-urls.test.js @@ -0,0 +1,126 @@ +// test/clean-urls.test.js +var assert = require('assert'); +var cleanUrls = require('../middleware/clean-urls'); +var path = require('path'); +var fs = require('fs'); + +describe('Clean URLs Middleware', function() { + var req; + var res; + var originalAccess; + + // Increase timeout for all tests in this suite + this.timeout(10000); // Increased timeout + + beforeEach(function() { + req = { + url: '', + method: 'GET' + }; + + res = { + writeHead: function(code, headers) { + this.statusCode = code; + this.headers = headers; + }, + end: function() { + this.ended = true; + } + }; + + next = function() { + this.nextCalled = true; + }.bind({nextCalled: false}); + + originalAccess = fs.access; + }); + + afterEach(function() { + fs.access = originalAccess; + }); + + describe('File type handling', function() { + it('should skip non-HTML files', function(done) { + var testCases = [ + '/styles.css', + '/script.js', + '/image.png', + '/document.pdf' + ]; + + var completed = 0; + testCases.forEach(function(url) { + req.url = url; + cleanUrls(req, res, function() { + assert.strictEqual(req.url, url); + completed++; + if (completed === testCases.length) done(); + }); + }); + }); + + it('should process HTML files correctly', function(done) { + req.url = '/page.html?param=value'; + cleanUrls(req, res, function() { + assert.strictEqual(res.statusCode, 301); + assert.strictEqual(res.headers.Location, '/page?param=value'); + assert.strictEqual(res.headers['Content-Type'], 'text/html; charset=utf-8'); + assert.ok(res.headers['Cache-Control']); + done(); + }); + }); + }); + + describe('URL cleaning', function() { + it('should remove trailing slashes except root', function(done) { + var testCases = [ + { input: '/about/', expected: '/about' }, + { input: '/products/item/', expected: '/products/item' }, + { input: '/', expected: '/' } + ]; + + var completed = 0; + testCases.forEach(function(test) { + req.url = test.input; + cleanUrls(req, res, function() { + assert.strictEqual(req.url, test.expected); + completed++; + if (completed === testCases.length) done(); + }); + }); + }); + + it('should handle URLs with query parameters', function(done) { + req.url = '/page.html?param=value&other=123'; + cleanUrls(req, res, function() { + assert.strictEqual(res.headers.Location, '/page?param=value&other=123'); + assert.strictEqual(res.headers['Content-Type'], 'text/html; charset=utf-8'); + done(); + }); + }); + + it('should preserve URL-encoded characters', function(done) { + req.url = '/page%20name.html?query=test%20value'; + cleanUrls(req, res, function() { + assert.strictEqual(res.headers.Location, '/page%20name?query=test%20value'); + assert.strictEqual(res.headers['Content-Type'], 'text/html; charset=utf-8'); + done(); + }); + }); + }); + + describe('Index handling', function() { + it('should handle index.html for root path', function(done) { + req.url = '/?test=1'; + fs.access = function(filePath, mode, callback) { + assert.strictEqual(path.basename(filePath), 'index.html'); + callback(null); + }; + + cleanUrls(req, res, function() { + assert.strictEqual(req.url, '/index.html?test=1'); + done(); + }); + }); + }); +}); \ No newline at end of file