lobsterboard 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lobsterboard.css +1 -1
- package/dist/lobsterboard.esm.js +1 -1
- package/dist/lobsterboard.esm.min.js +1 -1
- package/dist/lobsterboard.umd.js +1 -1
- package/dist/lobsterboard.umd.min.js +1 -1
- package/package.json +1 -1
- package/server.cjs +103 -51
package/dist/lobsterboard.css
CHANGED
package/dist/lobsterboard.esm.js
CHANGED
package/dist/lobsterboard.umd.js
CHANGED
package/package.json
CHANGED
package/server.cjs
CHANGED
|
@@ -59,68 +59,95 @@ const HOST = process.env.HOST || '127.0.0.1';
|
|
|
59
59
|
// ─────────────────────────────────────────────
|
|
60
60
|
// Pages System — auto-discovery and mounting
|
|
61
61
|
// ─────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
62
|
+
// Load from both package pages AND user's working directory pages (user overrides package)
|
|
63
|
+
const CWD = process.cwd();
|
|
64
|
+
const USER_PAGES_DIR = path.join(CWD, 'pages');
|
|
65
|
+
const PKG_PAGES_DIR = path.join(__dirname, 'pages');
|
|
66
|
+
const PAGES_DIRS = [PKG_PAGES_DIR]; // Package pages first
|
|
67
|
+
if (CWD !== __dirname && fs.existsSync(USER_PAGES_DIR)) {
|
|
68
|
+
PAGES_DIRS.push(USER_PAGES_DIR); // User pages override
|
|
69
|
+
}
|
|
70
|
+
// For backwards compat, PAGES_DIR points to first available (used for _shared)
|
|
71
|
+
const PAGES_DIR = fs.existsSync(USER_PAGES_DIR) ? USER_PAGES_DIR : PKG_PAGES_DIR;
|
|
72
|
+
const USER_PAGES_JSON = path.join(CWD, 'pages.json');
|
|
73
|
+
const PKG_PAGES_JSON = path.join(__dirname, 'pages.json');
|
|
74
|
+
const PAGES_JSON = fs.existsSync(USER_PAGES_JSON) ? USER_PAGES_JSON : PKG_PAGES_JSON;
|
|
75
|
+
// Data always in working directory (user's data)
|
|
76
|
+
const DATA_DIR = path.join(CWD, 'data');
|
|
65
77
|
|
|
66
78
|
let loadedPages = []; // { id, title, icon, description, order, routes: { 'METHOD /path': handler } }
|
|
67
79
|
|
|
68
80
|
function loadPages() {
|
|
69
81
|
const pages = [];
|
|
82
|
+
const seenIds = new Set();
|
|
70
83
|
let overrides = { pages: {} };
|
|
71
84
|
try { overrides = JSON.parse(fs.readFileSync(PAGES_JSON, 'utf8')); } catch (_) {}
|
|
72
85
|
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
// Scan all page directories (user pages loaded last to override package pages)
|
|
87
|
+
for (const pagesDir of PAGES_DIRS) {
|
|
88
|
+
let dirs;
|
|
89
|
+
try { dirs = fs.readdirSync(pagesDir); } catch (_) { continue; }
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
for (const dir of dirs) {
|
|
92
|
+
if (dir.startsWith('_')) continue;
|
|
93
|
+
const metaPath = path.join(pagesDir, dir, 'page.json');
|
|
94
|
+
if (!fs.existsSync(metaPath)) continue;
|
|
80
95
|
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
let meta;
|
|
97
|
+
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch (_) { continue; }
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
const override = overrides.pages[meta.id] || {};
|
|
100
|
+
meta.enabled = override.enabled ?? meta.enabled ?? true;
|
|
101
|
+
meta.order = override.order ?? meta.order ?? 99;
|
|
87
102
|
|
|
88
|
-
|
|
103
|
+
if (!meta.enabled) continue;
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
// If we've seen this ID before (from package), remove it so user page wins
|
|
106
|
+
if (seenIds.has(meta.id)) {
|
|
107
|
+
const idx = pages.findIndex(p => p.id === meta.id);
|
|
108
|
+
if (idx !== -1) pages.splice(idx, 1);
|
|
109
|
+
}
|
|
110
|
+
seenIds.add(meta.id);
|
|
93
111
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
// Store which directory this page came from
|
|
113
|
+
meta._pagesDir = pagesDir;
|
|
114
|
+
|
|
115
|
+
// Ensure data dir
|
|
116
|
+
const dataDir = path.join(DATA_DIR, meta.id);
|
|
117
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
118
|
+
|
|
119
|
+
// Load API routes if api.cjs (or api.js) exists
|
|
120
|
+
let apiPath = path.join(pagesDir, dir, 'api.cjs');
|
|
121
|
+
if (!fs.existsSync(apiPath)) apiPath = path.join(pagesDir, dir, 'api.js');
|
|
122
|
+
let routes = {};
|
|
123
|
+
if (fs.existsSync(apiPath)) {
|
|
124
|
+
try {
|
|
125
|
+
const ctx = {
|
|
126
|
+
dataDir,
|
|
127
|
+
readData: (filename) => JSON.parse(fs.readFileSync(path.join(dataDir, filename), 'utf8')),
|
|
128
|
+
writeData: (filename, obj) => {
|
|
129
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
130
|
+
fs.writeFileSync(path.join(dataDir, filename), JSON.stringify(obj, null, 2));
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const pageModule = require(apiPath)(ctx);
|
|
134
|
+
routes = pageModule.routes || {};
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.error(`Error loading page API for ${meta.id}:`, e.message);
|
|
137
|
+
}
|
|
112
138
|
}
|
|
113
|
-
}
|
|
114
139
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
pages.push({
|
|
141
|
+
id: meta.id,
|
|
142
|
+
title: meta.title,
|
|
143
|
+
icon: meta.icon,
|
|
144
|
+
description: meta.description,
|
|
145
|
+
order: meta.order,
|
|
146
|
+
nav: meta.nav !== false,
|
|
147
|
+
_pagesDir: pagesDir,
|
|
148
|
+
routes
|
|
149
|
+
});
|
|
150
|
+
}
|
|
124
151
|
}
|
|
125
152
|
|
|
126
153
|
return pages.sort((a, b) => a.order - b.order);
|
|
@@ -173,11 +200,16 @@ function matchPageRoute(pages, method, pathname, parsedUrl) {
|
|
|
173
200
|
}
|
|
174
201
|
const page = pages.find(p => p.id === pageId);
|
|
175
202
|
if (page) {
|
|
176
|
-
const subPath = pagesMatch[2] || '
|
|
177
|
-
|
|
178
|
-
|
|
203
|
+
const subPath = pagesMatch[2] || '';
|
|
204
|
+
// Redirect /pages/id to /pages/id/ (trailing slash) for proper relative path resolution
|
|
205
|
+
if (!subPath) {
|
|
206
|
+
return { type: 'redirect', location: `/pages/${pageId}/` };
|
|
207
|
+
}
|
|
208
|
+
const pageDir = page._pagesDir || PAGES_DIR;
|
|
209
|
+
if (subPath === '/') {
|
|
210
|
+
return { type: 'static', filePath: path.join(pageDir, pageId, 'index.html') };
|
|
179
211
|
}
|
|
180
|
-
return { type: 'static', filePath: path.join(
|
|
212
|
+
return { type: 'static', filePath: path.join(pageDir, pageId, subPath.slice(1)) };
|
|
181
213
|
}
|
|
182
214
|
}
|
|
183
215
|
|
|
@@ -2329,9 +2361,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
2329
2361
|
sendJson(res, 200, loadedPages.filter(p => p.nav !== false).map(p => ({ id: p.id, title: p.title, icon: p.icon, description: p.description, order: p.order })));
|
|
2330
2362
|
return;
|
|
2331
2363
|
}
|
|
2364
|
+
if (pageMatch.type === 'redirect') {
|
|
2365
|
+
res.writeHead(302, { Location: pageMatch.location });
|
|
2366
|
+
res.end();
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2332
2369
|
if (pageMatch.type === 'static') {
|
|
2333
2370
|
const resolved = path.resolve(pageMatch.filePath);
|
|
2334
|
-
|
|
2371
|
+
// Security: ensure path is within one of the allowed page directories
|
|
2372
|
+
const isAllowed = PAGES_DIRS.some(dir => resolved.startsWith(path.resolve(dir)));
|
|
2373
|
+
if (!isAllowed) {
|
|
2335
2374
|
sendResponse(res, 403, 'text/plain', 'Forbidden');
|
|
2336
2375
|
return;
|
|
2337
2376
|
}
|
|
@@ -3370,6 +3409,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
3370
3409
|
}
|
|
3371
3410
|
|
|
3372
3411
|
// Serve static files
|
|
3412
|
+
// Check working directory's public/ folder first (for user assets like recap images)
|
|
3413
|
+
const publicPath = path.join(CWD, 'public', pathname);
|
|
3414
|
+
const publicResolved = path.resolve(publicPath);
|
|
3415
|
+
if (publicResolved.startsWith(path.join(CWD, 'public') + path.sep) && fs.existsSync(publicPath)) {
|
|
3416
|
+
const ext = path.extname(publicPath).toLowerCase();
|
|
3417
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
3418
|
+
fs.readFile(publicPath, (err, data) => {
|
|
3419
|
+
if (err) { sendError(res, err.message); return; }
|
|
3420
|
+
sendResponse(res, 200, contentType, data);
|
|
3421
|
+
});
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3373
3425
|
let filePath = path.join(__dirname, pathname);
|
|
3374
3426
|
if (pathname === '/') {
|
|
3375
3427
|
filePath = path.join(__dirname, 'app.html');
|