From bfd1e039dd4a577fd13f15c79118895995adaf83 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:53:26 +0100 Subject: [PATCH] Add /apps route for serving SPAs from the data directory Serves static files from /apps/ under /apps, with .html extension fallback and directory index.html resolution (e.g. /apps/myapp serves myapp/index.html). Includes tests. Co-Authored-By: Claude --- server.js | 12 +++++++ tests/server/apps-hosting.test.js | 58 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/server/apps-hosting.test.js diff --git a/server.js b/server.js index 997bc9c..b53936f 100644 --- a/server.js +++ b/server.js @@ -448,6 +448,18 @@ app.get('/', async (req, res) => { // Serve static files app.use(express.static(path.join(__dirname, 'static'))); +// Serve single-page applications from data directory +const appsDir = path.join(folders.dataDir(), 'apps'); +app.use('/apps', express.static(appsDir, { extensions: ['html'] })); +app.use('/apps/:app', (req, res, next) => { + const indexPath = path.join(appsDir, req.params.app, 'index.html'); + if (fs.existsSync(indexPath)) { + res.sendFile(indexPath); + } else { + next(); + } +}); + // Health check endpoint app.get('/health', async (req, res) => { const healthStatus = { diff --git a/tests/server/apps-hosting.test.js b/tests/server/apps-hosting.test.js new file mode 100644 index 0000000..6ed45f2 --- /dev/null +++ b/tests/server/apps-hosting.test.js @@ -0,0 +1,58 @@ +const express = require('express'); +const request = require('supertest'); +const path = require('path'); +const fs = require('fs'); +const tmp = require('tmp'); + +describe('/apps SPA hosting', () => { + let app; + let tmpDir; + + beforeAll(() => { + tmpDir = tmp.dirSync({ unsafeCleanup: true }); + const appsDir = path.join(tmpDir.name, 'apps'); + + // Create a mock SPA: apps/testapp/index.html + fs.mkdirSync(path.join(appsDir, 'testapp'), { recursive: true }); + fs.writeFileSync(path.join(appsDir, 'testapp', 'index.html'), 'Test App'); + fs.writeFileSync(path.join(appsDir, 'testapp', 'about.html'), 'About'); + + app = express(); + app.use('/apps', express.static(appsDir, { extensions: ['html'] })); + app.use('/apps/:app', (req, res, next) => { + const indexPath = path.join(appsDir, req.params.app, 'index.html'); + if (fs.existsSync(indexPath)) { + res.sendFile(indexPath); + } else { + next(); + } + }); + }); + + afterAll(() => { + tmpDir.removeCallback(); + }); + + test('/apps/testapp/index.html serves the app', async () => { + const res = await request(app).get('/apps/testapp/index.html'); + expect(res.status).toBe(200); + expect(res.text).toContain('Test App'); + }); + + test('/apps/testapp serves index.html (directory shortcut)', async () => { + const res = await request(app).get('/apps/testapp').redirects(1); + expect(res.status).toBe(200); + expect(res.text).toContain('Test App'); + }); + + test('/apps/testapp/about serves about.html (extension fallback)', async () => { + const res = await request(app).get('/apps/testapp/about'); + expect(res.status).toBe(200); + expect(res.text).toContain('About'); + }); + + test('/apps/nonexistent returns 404', async () => { + const res = await request(app).get('/apps/nonexistent'); + expect(res.status).toBe(404); + }); +});