@tknf/matchbox 0.2.3 → 0.2.4

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/cgi.js CHANGED
@@ -1,12 +1,143 @@
1
+ // src/cgi.ts
1
2
  import { Hono } from "hono";
2
3
  import { basicAuth } from "hono/basic-auth";
3
4
  import { getCookie, setCookie } from "hono/cookie";
4
- import packageJson from "../package.json";
5
- import { generateCgiError, generateCgiInfo } from "./html.js";
6
- const isRedirectObject = (obj) => {
5
+
6
+ // package.json
7
+ var package_default = {
8
+ name: "@tknf/matchbox",
9
+ version: "0.2.4",
10
+ description: "A Simple Web Server Framework",
11
+ keywords: [
12
+ "cgi",
13
+ "framework",
14
+ "hono",
15
+ "server",
16
+ "web"
17
+ ],
18
+ license: "MIT",
19
+ author: "tknf <dev@tknf.net>",
20
+ repository: {
21
+ url: "https://github.com/tknf/matchbox.git"
22
+ },
23
+ files: [
24
+ "dist"
25
+ ],
26
+ type: "module",
27
+ module: "dist/index.js",
28
+ types: "dist/index.d.ts",
29
+ exports: {
30
+ ".": {
31
+ import: {
32
+ types: "./dist/index.d.ts",
33
+ import: "./dist/index.js"
34
+ }
35
+ },
36
+ "./plugin": {
37
+ import: {
38
+ types: "./dist/plugin.d.ts",
39
+ import: "./dist/plugin.js"
40
+ }
41
+ }
42
+ },
43
+ publishConfig: {
44
+ access: "public",
45
+ registry: "https://registry.npmjs.org/"
46
+ },
47
+ scripts: {
48
+ test: "vitest run",
49
+ "test:coverage": "vitest run --coverage",
50
+ "lint:check": "oxlint",
51
+ "lint:fix": "oxlint --fix",
52
+ "format:check": "oxfmt --check",
53
+ "format:write": "oxfmt",
54
+ watch: "tsup --watch",
55
+ build: "tsup"
56
+ },
57
+ dependencies: {
58
+ glob: "^13.0.0"
59
+ },
60
+ devDependencies: {
61
+ "@types/node": "^25.0.3",
62
+ "@vitest/coverage-v8": "^4.0.16",
63
+ hono: "^4.11.1",
64
+ oxfmt: "^0.20.0",
65
+ oxlint: "^1.35.0",
66
+ tsup: "^8.5.1",
67
+ typescript: "^5.9.3",
68
+ vite: "^7.3.0",
69
+ vitest: "^4.0.16"
70
+ },
71
+ peerDependencies: {
72
+ hono: "*"
73
+ }
74
+ };
75
+
76
+ // src/html.ts
77
+ import { html } from "hono/html";
78
+ var generateCgiInfo = ({
79
+ $_SERVER,
80
+ $_SESSION,
81
+ $_REQUEST,
82
+ config
83
+ }) => {
84
+ return () => {
85
+ const infoSection = ({ title, data }) => {
86
+ return html`
87
+ <div style="margin-bottom: 20px; width: 100%; max-width: 900px;">
88
+ <h2 style="background: #ccccff; color: #000; padding: 5px 10px; margin: 0; font-size: 1.2em; border: 1px solid #000; font-family: 'MS PGothic', sans-serif;">${title}</h2>
89
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #000; table-layout: fixed; font-family: 'MS PGothic', sans-serif;">
90
+ ${Object.entries(data).map(
91
+ ([key, val], i) => html`
92
+ <tr style="background: ${i % 2 === 0 ? "#f0f0ff" : "#ffffff"}">
93
+ <td style="padding: 3px 10px; border: 1px solid #000; font-weight: bold; width: 30%; word-break: break-all;">${key}</td>
94
+ <td style="padding: 3px 10px; border: 1px solid #000; width: 70%; word-break: break-all;">
95
+ ${typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)}
96
+ </td>
97
+ </tr>
98
+ `
99
+ )}
100
+ </table>
101
+ </div>
102
+ `;
103
+ };
104
+ return html`
105
+ <div style="background: #ffffff; color: #333333; font-family: 'MS PGothic', sans-serif; padding: 20px; display: flex; flex-direction: column; align-items: center;">
106
+ <h1 style="color: #000000; border-bottom: 3px double #000000; padding-bottom: 10px; margin-bottom: 30px;">Matchbox CGI Version Information</h1>
107
+ <div style="width: 100%; max-width: 900px; padding: 15px; background: #ffffcc; border: 1px dashed #000000; margin-bottom: 20px; text-align: center;">
108
+ <strong>Server Software:</strong> Matchbox Engine on ${typeof process !== "undefined" ? "Node.js/Bun" : "Edge"}
109
+ </div>
110
+ ${infoSection({ title: "$_SERVER (Environment)", data: $_SERVER })}
111
+ ${infoSection({ title: "$_SESSION", data: $_SESSION })}
112
+ ${infoSection({ title: "$_REQUEST", data: $_REQUEST })}
113
+ ${infoSection({ title: "Site Configuration", data: config })}
114
+ <div style="margin-top: 40px; font-size: 0.9em; font-style: italic; border-top: 1px solid #ccc; width: 100%; max-width: 900px; text-align: right; padding-top: 10px;">
115
+ Generated by Matchbox Framework - Ignition for Hono
116
+ </div>
117
+ </div>
118
+ `;
119
+ };
120
+ };
121
+ var generateCgiError = ({
122
+ error,
123
+ $_SERVER
124
+ }) => {
125
+ return html`
126
+ <div style="padding:2rem; background:#fffafa; border:5px double #cc0000; font-family: 'MS PGothic', sans-serif;">
127
+ <h1 style="color:#cc0000; border-bottom: 2px solid #cc0000; padding-bottom: 5px;">Matchbox: Runtime Exception</h1>
128
+ <p><strong>Fatal Error:</strong> ${error.message}</p>
129
+ <pre style="background:#f0f0f0; padding:1rem; border:1px inset #ccc; overflow: auto;">${error.stack}</pre>
130
+ <hr style="border: 0; border-top: 1px solid #cc0000;" />
131
+ <div style="text-align: right; font-size: 0.8em;">Matchbox CGI Engine Server at ${$_SERVER.REMOTE_ADDR}</div>
132
+ </div>
133
+ `;
134
+ };
135
+
136
+ // src/cgi.ts
137
+ var isRedirectObject = (obj) => {
7
138
  return obj && obj.__type === "redirect" && typeof obj.url === "string";
8
139
  };
9
- const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {}, options = {}) => {
140
+ var createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {}, options = {}) => {
10
141
  const app = new Hono();
11
142
  const SESS_KEY = options.sessionCookie?.name || "_SESSION_ID";
12
143
  if (options.middleware && options.middleware.length > 0) {
@@ -170,7 +301,7 @@ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {
170
301
  }
171
302
  },
172
303
  get_version: () => {
173
- return `MatchboxCGI/v${packageJson.version}`;
304
+ return `MatchboxCGI/v${package_default.version}`;
174
305
  },
175
306
  /**
176
307
  * Returns information about all loaded CGI modules
package/dist/html.js CHANGED
@@ -1,5 +1,6 @@
1
+ // src/html.ts
1
2
  import { html } from "hono/html";
2
- const generateCgiInfo = ({
3
+ var generateCgiInfo = ({
3
4
  $_SERVER,
4
5
  $_SESSION,
5
6
  $_REQUEST,
@@ -42,7 +43,7 @@ const generateCgiInfo = ({
42
43
  `;
43
44
  };
44
45
  };
45
- const generateCgiError = ({
46
+ var generateCgiError = ({
46
47
  error,
47
48
  $_SERVER
48
49
  }) => {
package/dist/index.js CHANGED
@@ -1,4 +1,441 @@
1
- import { createCgi } from "./with-defaults.js";
1
+ // src/cgi.ts
2
+ import { Hono } from "hono";
3
+ import { basicAuth } from "hono/basic-auth";
4
+ import { getCookie, setCookie } from "hono/cookie";
5
+
6
+ // package.json
7
+ var package_default = {
8
+ name: "@tknf/matchbox",
9
+ version: "0.2.4",
10
+ description: "A Simple Web Server Framework",
11
+ keywords: [
12
+ "cgi",
13
+ "framework",
14
+ "hono",
15
+ "server",
16
+ "web"
17
+ ],
18
+ license: "MIT",
19
+ author: "tknf <dev@tknf.net>",
20
+ repository: {
21
+ url: "https://github.com/tknf/matchbox.git"
22
+ },
23
+ files: [
24
+ "dist"
25
+ ],
26
+ type: "module",
27
+ module: "dist/index.js",
28
+ types: "dist/index.d.ts",
29
+ exports: {
30
+ ".": {
31
+ import: {
32
+ types: "./dist/index.d.ts",
33
+ import: "./dist/index.js"
34
+ }
35
+ },
36
+ "./plugin": {
37
+ import: {
38
+ types: "./dist/plugin.d.ts",
39
+ import: "./dist/plugin.js"
40
+ }
41
+ }
42
+ },
43
+ publishConfig: {
44
+ access: "public",
45
+ registry: "https://registry.npmjs.org/"
46
+ },
47
+ scripts: {
48
+ test: "vitest run",
49
+ "test:coverage": "vitest run --coverage",
50
+ "lint:check": "oxlint",
51
+ "lint:fix": "oxlint --fix",
52
+ "format:check": "oxfmt --check",
53
+ "format:write": "oxfmt",
54
+ watch: "tsup --watch",
55
+ build: "tsup"
56
+ },
57
+ dependencies: {
58
+ glob: "^13.0.0"
59
+ },
60
+ devDependencies: {
61
+ "@types/node": "^25.0.3",
62
+ "@vitest/coverage-v8": "^4.0.16",
63
+ hono: "^4.11.1",
64
+ oxfmt: "^0.20.0",
65
+ oxlint: "^1.35.0",
66
+ tsup: "^8.5.1",
67
+ typescript: "^5.9.3",
68
+ vite: "^7.3.0",
69
+ vitest: "^4.0.16"
70
+ },
71
+ peerDependencies: {
72
+ hono: "*"
73
+ }
74
+ };
75
+
76
+ // src/html.ts
77
+ import { html } from "hono/html";
78
+ var generateCgiInfo = ({
79
+ $_SERVER,
80
+ $_SESSION,
81
+ $_REQUEST,
82
+ config
83
+ }) => {
84
+ return () => {
85
+ const infoSection = ({ title, data }) => {
86
+ return html`
87
+ <div style="margin-bottom: 20px; width: 100%; max-width: 900px;">
88
+ <h2 style="background: #ccccff; color: #000; padding: 5px 10px; margin: 0; font-size: 1.2em; border: 1px solid #000; font-family: 'MS PGothic', sans-serif;">${title}</h2>
89
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #000; table-layout: fixed; font-family: 'MS PGothic', sans-serif;">
90
+ ${Object.entries(data).map(
91
+ ([key, val], i) => html`
92
+ <tr style="background: ${i % 2 === 0 ? "#f0f0ff" : "#ffffff"}">
93
+ <td style="padding: 3px 10px; border: 1px solid #000; font-weight: bold; width: 30%; word-break: break-all;">${key}</td>
94
+ <td style="padding: 3px 10px; border: 1px solid #000; width: 70%; word-break: break-all;">
95
+ ${typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)}
96
+ </td>
97
+ </tr>
98
+ `
99
+ )}
100
+ </table>
101
+ </div>
102
+ `;
103
+ };
104
+ return html`
105
+ <div style="background: #ffffff; color: #333333; font-family: 'MS PGothic', sans-serif; padding: 20px; display: flex; flex-direction: column; align-items: center;">
106
+ <h1 style="color: #000000; border-bottom: 3px double #000000; padding-bottom: 10px; margin-bottom: 30px;">Matchbox CGI Version Information</h1>
107
+ <div style="width: 100%; max-width: 900px; padding: 15px; background: #ffffcc; border: 1px dashed #000000; margin-bottom: 20px; text-align: center;">
108
+ <strong>Server Software:</strong> Matchbox Engine on ${typeof process !== "undefined" ? "Node.js/Bun" : "Edge"}
109
+ </div>
110
+ ${infoSection({ title: "$_SERVER (Environment)", data: $_SERVER })}
111
+ ${infoSection({ title: "$_SESSION", data: $_SESSION })}
112
+ ${infoSection({ title: "$_REQUEST", data: $_REQUEST })}
113
+ ${infoSection({ title: "Site Configuration", data: config })}
114
+ <div style="margin-top: 40px; font-size: 0.9em; font-style: italic; border-top: 1px solid #ccc; width: 100%; max-width: 900px; text-align: right; padding-top: 10px;">
115
+ Generated by Matchbox Framework - Ignition for Hono
116
+ </div>
117
+ </div>
118
+ `;
119
+ };
120
+ };
121
+ var generateCgiError = ({
122
+ error,
123
+ $_SERVER
124
+ }) => {
125
+ return html`
126
+ <div style="padding:2rem; background:#fffafa; border:5px double #cc0000; font-family: 'MS PGothic', sans-serif;">
127
+ <h1 style="color:#cc0000; border-bottom: 2px solid #cc0000; padding-bottom: 5px;">Matchbox: Runtime Exception</h1>
128
+ <p><strong>Fatal Error:</strong> ${error.message}</p>
129
+ <pre style="background:#f0f0f0; padding:1rem; border:1px inset #ccc; overflow: auto;">${error.stack}</pre>
130
+ <hr style="border: 0; border-top: 1px solid #cc0000;" />
131
+ <div style="text-align: right; font-size: 0.8em;">Matchbox CGI Engine Server at ${$_SERVER.REMOTE_ADDR}</div>
132
+ </div>
133
+ `;
134
+ };
135
+
136
+ // src/cgi.ts
137
+ var isRedirectObject = (obj) => {
138
+ return obj && obj.__type === "redirect" && typeof obj.url === "string";
139
+ };
140
+ var createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {}, options = {}) => {
141
+ const app = new Hono();
142
+ const SESS_KEY = options.sessionCookie?.name || "_SESSION_ID";
143
+ if (options.middleware && options.middleware.length > 0) {
144
+ for (const mw of options.middleware) {
145
+ app.use("*", async (c, next) => {
146
+ const result = await mw(c, next);
147
+ if (result instanceof Response) {
148
+ return result;
149
+ }
150
+ });
151
+ }
152
+ }
153
+ const protectedFiles = [".htaccess", ".htpasswd", ".htdigest", ".htgroup"];
154
+ app.use("*", async (c, next) => {
155
+ const path = c.req.path;
156
+ const lastSegment = path.slice(path.lastIndexOf("/") + 1);
157
+ if (protectedFiles.some((file) => lastSegment === file)) {
158
+ return c.text("Forbidden", 403);
159
+ }
160
+ await next();
161
+ });
162
+ if (options.enforceTrailingSlash) {
163
+ app.use("*", async (c, next) => {
164
+ const path = c.req.path;
165
+ if (!path.endsWith("/") && !path.includes(".")) {
166
+ return c.redirect(`${path}/`, 301);
167
+ }
168
+ await next();
169
+ });
170
+ }
171
+ Object.entries(rewriteMap).forEach(([dir, rules]) => {
172
+ const basePath = dir === "/" ? "" : dir.replace(/\/$/, "");
173
+ app.use(`${basePath}/*`, async (c, next) => {
174
+ const relPath = c.req.path.replace(basePath, "") || "/";
175
+ for (const rule of rules) {
176
+ if (rule.type === "redirect") {
177
+ if (relPath === rule.source) {
178
+ return c.redirect(
179
+ rule.target,
180
+ Number.parseInt(rule.code, 10) || 302
181
+ );
182
+ }
183
+ } else if (rule.type === "rewrite") {
184
+ const regex = new RegExp(rule.pattern);
185
+ if (regex.test(relPath)) {
186
+ const target = rule.target.startsWith("/") ? rule.target : `${basePath}/${rule.target}`;
187
+ if (rule.flags.includes("R")) {
188
+ const code = rule.flags.match(/R=(\d+)/)?.[1] || "302";
189
+ return c.redirect(target, Number.parseInt(code, 10));
190
+ }
191
+ }
192
+ }
193
+ }
194
+ await next();
195
+ });
196
+ });
197
+ Object.entries(authMap).forEach(([dir, content]) => {
198
+ const credentials = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
199
+ const [username, password] = line.split(":");
200
+ return { username, password };
201
+ });
202
+ if (credentials.length > 0) {
203
+ const authPath = dir === "/" ? "*" : `${dir.replace(/\/$/, "")}/*`;
204
+ app.use(authPath, async (c, next) => {
205
+ const handler = basicAuth({
206
+ verifyUser: (u, p) => credentials.some((cred) => cred.username === u && cred.password === p),
207
+ realm: "Restricted Area"
208
+ });
209
+ return handler(c, next);
210
+ });
211
+ }
212
+ });
213
+ pages.forEach(({ urlPath, dirPath, component }) => {
214
+ const routes = [urlPath, `${urlPath}/*`];
215
+ if (dirPath) {
216
+ routes.push(dirPath);
217
+ if (dirPath !== "/") {
218
+ routes.push(`${dirPath}/*`);
219
+ }
220
+ }
221
+ routes.forEach((route) => {
222
+ app.all(route, async (c) => {
223
+ const $_GET = c.req.query();
224
+ const body = await c.req.parseBody({ all: true }).catch(() => ({}));
225
+ const $_POST = {};
226
+ const $_FILES = {};
227
+ for (const [key, value] of Object.entries(body)) {
228
+ if (value instanceof File || Array.isArray(value) && value[0] instanceof File) {
229
+ $_FILES[key] = value;
230
+ } else {
231
+ $_POST[key] = value;
232
+ }
233
+ }
234
+ const $_COOKIE = getCookie(c);
235
+ const $_REQUEST = {
236
+ ...$_COOKIE,
237
+ ...$_GET,
238
+ ...$_POST
239
+ };
240
+ const $_ENV = typeof process !== "undefined" && process.env ? process.env : c.env || {};
241
+ let $_SESSION = {};
242
+ const sRaw = getCookie(c, SESS_KEY);
243
+ if (sRaw) {
244
+ try {
245
+ $_SESSION = JSON.parse(decodeURIComponent(sRaw));
246
+ } catch {
247
+ }
248
+ }
249
+ let responseStatus = 200;
250
+ const responseHeaders = {
251
+ "Content-Type": "text/html; charset=utf-8"
252
+ };
253
+ const $_SERVER = {
254
+ ...$_ENV,
255
+ REQUEST_METHOD: c.req.method,
256
+ REQUEST_URI: c.req.url,
257
+ REMOTE_ADDR: c.req.header("x-forwarded-for") || "127.0.0.1",
258
+ USER_AGENT: c.req.header("user-agent") || "",
259
+ SCRIPT_NAME: urlPath,
260
+ PATH_INFO: c.req.path.replace(urlPath, "") || "/",
261
+ QUERY_STRING: new URL(c.req.url).search.slice(1)
262
+ };
263
+ const cgiinfo = generateCgiInfo({
264
+ $_SERVER,
265
+ $_REQUEST,
266
+ $_SESSION,
267
+ config: siteConfig
268
+ });
269
+ const context = {
270
+ $_GET,
271
+ $_POST,
272
+ $_FILES,
273
+ $_REQUEST,
274
+ $_COOKIE,
275
+ $_ENV,
276
+ $_SERVER,
277
+ $_SESSION,
278
+ config: siteConfig,
279
+ c,
280
+ header: (name, value) => {
281
+ responseHeaders[name.toLocaleLowerCase()] = value;
282
+ },
283
+ status: (code) => {
284
+ responseStatus = code;
285
+ },
286
+ redirect: (url, status = 302) => {
287
+ return { __type: "redirect", url, status };
288
+ },
289
+ cgiinfo,
290
+ request_headers: () => {
291
+ return Object.fromEntries(c.req.raw.headers.entries());
292
+ },
293
+ response_headers: () => {
294
+ return responseHeaders;
295
+ },
296
+ log: (message) => {
297
+ if (options.logger) {
298
+ options.logger(message, "info");
299
+ } else {
300
+ console.log(`[CGI LOG] ${message}`);
301
+ }
302
+ },
303
+ get_version: () => {
304
+ return `MatchboxCGI/v${package_default.version}`;
305
+ },
306
+ /**
307
+ * Returns information about all loaded CGI modules
308
+ * @returns Array of module information containing urlPath and dirPath
309
+ */
310
+ get_modules: () => {
311
+ return pages.map((page) => ({
312
+ urlPath: page.urlPath,
313
+ dirPath: page.dirPath
314
+ }));
315
+ }
316
+ };
317
+ try {
318
+ const result = await component(context);
319
+ const sessionValue = encodeURIComponent(JSON.stringify($_SESSION));
320
+ const sessionOptions = {
321
+ path: options.sessionCookie?.path || "/",
322
+ httpOnly: true,
323
+ sameSite: options.sessionCookie?.sameSite || "Lax"
324
+ };
325
+ if (options.sessionCookie?.secure !== void 0) {
326
+ sessionOptions.secure = options.sessionCookie.secure;
327
+ }
328
+ if (options.sessionCookie?.domain) {
329
+ sessionOptions.domain = options.sessionCookie.domain;
330
+ }
331
+ if (options.sessionCookie?.maxAge) {
332
+ sessionOptions.maxAge = options.sessionCookie.maxAge;
333
+ }
334
+ if (isRedirectObject(result)) {
335
+ setCookie(c, SESS_KEY, sessionValue, sessionOptions);
336
+ return c.redirect(result.url, result.status);
337
+ }
338
+ if (result instanceof Response) {
339
+ setCookie(c, SESS_KEY, sessionValue, sessionOptions);
340
+ return result;
341
+ }
342
+ setCookie(c, SESS_KEY, sessionValue, sessionOptions);
343
+ Object.entries(responseHeaders).forEach(([key, value]) => {
344
+ c.header(key, value);
345
+ });
346
+ const contentType = responseHeaders["content-type"];
347
+ if (contentType?.includes("application/json")) {
348
+ return c.json(
349
+ // biome-ignore lint/suspicious/noExplicitAny: to JSON response
350
+ result ?? { success: true },
351
+ responseStatus
352
+ );
353
+ }
354
+ return c.html(result, responseStatus);
355
+ } catch (error) {
356
+ return c.html(generateCgiError({ error, $_SERVER }), 500);
357
+ }
358
+ });
359
+ });
360
+ });
361
+ return app;
362
+ };
363
+
364
+ // src/with-defaults.ts
365
+ var loadPagesFromPublic = () => {
366
+ const modules = import.meta.glob("/public/**/*.cgi.{tsx,jsx}", {
367
+ eager: true
368
+ });
369
+ const htpasswds = import.meta.glob("/public/**/.htpasswd", {
370
+ eager: true,
371
+ query: "?raw",
372
+ import: "default"
373
+ });
374
+ const htaccessFiles = import.meta.glob("/public/**/.htaccess", {
375
+ eager: true,
376
+ query: "?raw",
377
+ import: "default"
378
+ });
379
+ void import.meta.glob("/public/**/.htdigest", {
380
+ eager: true,
381
+ query: "?raw",
382
+ import: "default"
383
+ });
384
+ void import.meta.glob("/public/**/.htgroup", {
385
+ eager: true,
386
+ query: "?raw",
387
+ import: "default"
388
+ });
389
+ const basePathRegex = /^\/public/;
390
+ const pages = Object.keys(modules).map((key) => {
391
+ const urlPath = key.replace(basePathRegex, "").replace(/.tsx$/, "").replace(/.jsx$/, "");
392
+ const isIndex = urlPath.endsWith("/index.cgi") || urlPath === "/index.cgi";
393
+ const dirPath = isIndex ? urlPath.replace(/\/index\.cgi$/, "/") : null;
394
+ return { urlPath, dirPath, component: modules[key].default };
395
+ });
396
+ const authMap = Object.keys(htpasswds).reduce(
397
+ (acc, key) => {
398
+ const dir = key.replace(basePathRegex, "").replace(/\.htpasswd$/, "") || "/";
399
+ acc[dir] = htpasswds[key];
400
+ return acc;
401
+ },
402
+ {}
403
+ );
404
+ const rewriteMap = Object.keys(htaccessFiles).reduce((acc, key) => {
405
+ const dir = key.replace(basePathRegex, "").replace(/\.htaccess$/, "") || "/";
406
+ const lines = htaccessFiles[key].split("\n");
407
+ const rules = lines.map((line) => {
408
+ const l = line.trim();
409
+ if (!l || l.startsWith("#")) return null;
410
+ const parts = l.split(/\s+/);
411
+ if (parts[0] === "RewriteRule") {
412
+ return {
413
+ type: "rewrite",
414
+ pattern: parts[1],
415
+ target: parts[2],
416
+ flags: parts[3] || ""
417
+ };
418
+ }
419
+ if (parts[0] === "Redirect") {
420
+ return {
421
+ type: "redirect",
422
+ code: parts[1],
423
+ source: parts[2],
424
+ target: parts[3]
425
+ };
426
+ }
427
+ return null;
428
+ }).filter(Boolean);
429
+ acc[dir] = rules;
430
+ return acc;
431
+ }, {});
432
+ return { pages, authMap, rewriteMap };
433
+ };
434
+ var createCgi = (options) => {
435
+ const resolvedConfig = typeof __MATCHBOX_CONFIG__ === "undefined" ? {} : __MATCHBOX_CONFIG__;
436
+ const { pages, authMap, rewriteMap } = loadPagesFromPublic();
437
+ return createCgiWithPages(pages, resolvedConfig, authMap, rewriteMap, options);
438
+ };
2
439
  export {
3
440
  createCgi
4
441
  };
package/dist/plugin.js CHANGED
@@ -1,6 +1,7 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- const MatchboxPlugin = (options = {}) => {
1
+ // src/plugin.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ var MatchboxPlugin = (options = {}) => {
4
5
  let viteConfig;
5
6
  const siteConfig = options.config || {};
6
7
  const publicDir = options.publicDir || "public";
@@ -1,5 +1,368 @@
1
- import { createCgiWithPages } from "./cgi.js";
2
- const loadPagesFromPublic = () => {
1
+ // src/cgi.ts
2
+ import { Hono } from "hono";
3
+ import { basicAuth } from "hono/basic-auth";
4
+ import { getCookie, setCookie } from "hono/cookie";
5
+
6
+ // package.json
7
+ var package_default = {
8
+ name: "@tknf/matchbox",
9
+ version: "0.2.4",
10
+ description: "A Simple Web Server Framework",
11
+ keywords: [
12
+ "cgi",
13
+ "framework",
14
+ "hono",
15
+ "server",
16
+ "web"
17
+ ],
18
+ license: "MIT",
19
+ author: "tknf <dev@tknf.net>",
20
+ repository: {
21
+ url: "https://github.com/tknf/matchbox.git"
22
+ },
23
+ files: [
24
+ "dist"
25
+ ],
26
+ type: "module",
27
+ module: "dist/index.js",
28
+ types: "dist/index.d.ts",
29
+ exports: {
30
+ ".": {
31
+ import: {
32
+ types: "./dist/index.d.ts",
33
+ import: "./dist/index.js"
34
+ }
35
+ },
36
+ "./plugin": {
37
+ import: {
38
+ types: "./dist/plugin.d.ts",
39
+ import: "./dist/plugin.js"
40
+ }
41
+ }
42
+ },
43
+ publishConfig: {
44
+ access: "public",
45
+ registry: "https://registry.npmjs.org/"
46
+ },
47
+ scripts: {
48
+ test: "vitest run",
49
+ "test:coverage": "vitest run --coverage",
50
+ "lint:check": "oxlint",
51
+ "lint:fix": "oxlint --fix",
52
+ "format:check": "oxfmt --check",
53
+ "format:write": "oxfmt",
54
+ watch: "tsup --watch",
55
+ build: "tsup"
56
+ },
57
+ dependencies: {
58
+ glob: "^13.0.0"
59
+ },
60
+ devDependencies: {
61
+ "@types/node": "^25.0.3",
62
+ "@vitest/coverage-v8": "^4.0.16",
63
+ hono: "^4.11.1",
64
+ oxfmt: "^0.20.0",
65
+ oxlint: "^1.35.0",
66
+ tsup: "^8.5.1",
67
+ typescript: "^5.9.3",
68
+ vite: "^7.3.0",
69
+ vitest: "^4.0.16"
70
+ },
71
+ peerDependencies: {
72
+ hono: "*"
73
+ }
74
+ };
75
+
76
+ // src/html.ts
77
+ import { html } from "hono/html";
78
+ var generateCgiInfo = ({
79
+ $_SERVER,
80
+ $_SESSION,
81
+ $_REQUEST,
82
+ config
83
+ }) => {
84
+ return () => {
85
+ const infoSection = ({ title, data }) => {
86
+ return html`
87
+ <div style="margin-bottom: 20px; width: 100%; max-width: 900px;">
88
+ <h2 style="background: #ccccff; color: #000; padding: 5px 10px; margin: 0; font-size: 1.2em; border: 1px solid #000; font-family: 'MS PGothic', sans-serif;">${title}</h2>
89
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #000; table-layout: fixed; font-family: 'MS PGothic', sans-serif;">
90
+ ${Object.entries(data).map(
91
+ ([key, val], i) => html`
92
+ <tr style="background: ${i % 2 === 0 ? "#f0f0ff" : "#ffffff"}">
93
+ <td style="padding: 3px 10px; border: 1px solid #000; font-weight: bold; width: 30%; word-break: break-all;">${key}</td>
94
+ <td style="padding: 3px 10px; border: 1px solid #000; width: 70%; word-break: break-all;">
95
+ ${typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)}
96
+ </td>
97
+ </tr>
98
+ `
99
+ )}
100
+ </table>
101
+ </div>
102
+ `;
103
+ };
104
+ return html`
105
+ <div style="background: #ffffff; color: #333333; font-family: 'MS PGothic', sans-serif; padding: 20px; display: flex; flex-direction: column; align-items: center;">
106
+ <h1 style="color: #000000; border-bottom: 3px double #000000; padding-bottom: 10px; margin-bottom: 30px;">Matchbox CGI Version Information</h1>
107
+ <div style="width: 100%; max-width: 900px; padding: 15px; background: #ffffcc; border: 1px dashed #000000; margin-bottom: 20px; text-align: center;">
108
+ <strong>Server Software:</strong> Matchbox Engine on ${typeof process !== "undefined" ? "Node.js/Bun" : "Edge"}
109
+ </div>
110
+ ${infoSection({ title: "$_SERVER (Environment)", data: $_SERVER })}
111
+ ${infoSection({ title: "$_SESSION", data: $_SESSION })}
112
+ ${infoSection({ title: "$_REQUEST", data: $_REQUEST })}
113
+ ${infoSection({ title: "Site Configuration", data: config })}
114
+ <div style="margin-top: 40px; font-size: 0.9em; font-style: italic; border-top: 1px solid #ccc; width: 100%; max-width: 900px; text-align: right; padding-top: 10px;">
115
+ Generated by Matchbox Framework - Ignition for Hono
116
+ </div>
117
+ </div>
118
+ `;
119
+ };
120
+ };
121
+ var generateCgiError = ({
122
+ error,
123
+ $_SERVER
124
+ }) => {
125
+ return html`
126
+ <div style="padding:2rem; background:#fffafa; border:5px double #cc0000; font-family: 'MS PGothic', sans-serif;">
127
+ <h1 style="color:#cc0000; border-bottom: 2px solid #cc0000; padding-bottom: 5px;">Matchbox: Runtime Exception</h1>
128
+ <p><strong>Fatal Error:</strong> ${error.message}</p>
129
+ <pre style="background:#f0f0f0; padding:1rem; border:1px inset #ccc; overflow: auto;">${error.stack}</pre>
130
+ <hr style="border: 0; border-top: 1px solid #cc0000;" />
131
+ <div style="text-align: right; font-size: 0.8em;">Matchbox CGI Engine Server at ${$_SERVER.REMOTE_ADDR}</div>
132
+ </div>
133
+ `;
134
+ };
135
+
136
+ // src/cgi.ts
137
+ var isRedirectObject = (obj) => {
138
+ return obj && obj.__type === "redirect" && typeof obj.url === "string";
139
+ };
140
+ var createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {}, options = {}) => {
141
+ const app = new Hono();
142
+ const SESS_KEY = options.sessionCookie?.name || "_SESSION_ID";
143
+ if (options.middleware && options.middleware.length > 0) {
144
+ for (const mw of options.middleware) {
145
+ app.use("*", async (c, next) => {
146
+ const result = await mw(c, next);
147
+ if (result instanceof Response) {
148
+ return result;
149
+ }
150
+ });
151
+ }
152
+ }
153
+ const protectedFiles = [".htaccess", ".htpasswd", ".htdigest", ".htgroup"];
154
+ app.use("*", async (c, next) => {
155
+ const path = c.req.path;
156
+ const lastSegment = path.slice(path.lastIndexOf("/") + 1);
157
+ if (protectedFiles.some((file) => lastSegment === file)) {
158
+ return c.text("Forbidden", 403);
159
+ }
160
+ await next();
161
+ });
162
+ if (options.enforceTrailingSlash) {
163
+ app.use("*", async (c, next) => {
164
+ const path = c.req.path;
165
+ if (!path.endsWith("/") && !path.includes(".")) {
166
+ return c.redirect(`${path}/`, 301);
167
+ }
168
+ await next();
169
+ });
170
+ }
171
+ Object.entries(rewriteMap).forEach(([dir, rules]) => {
172
+ const basePath = dir === "/" ? "" : dir.replace(/\/$/, "");
173
+ app.use(`${basePath}/*`, async (c, next) => {
174
+ const relPath = c.req.path.replace(basePath, "") || "/";
175
+ for (const rule of rules) {
176
+ if (rule.type === "redirect") {
177
+ if (relPath === rule.source) {
178
+ return c.redirect(
179
+ rule.target,
180
+ Number.parseInt(rule.code, 10) || 302
181
+ );
182
+ }
183
+ } else if (rule.type === "rewrite") {
184
+ const regex = new RegExp(rule.pattern);
185
+ if (regex.test(relPath)) {
186
+ const target = rule.target.startsWith("/") ? rule.target : `${basePath}/${rule.target}`;
187
+ if (rule.flags.includes("R")) {
188
+ const code = rule.flags.match(/R=(\d+)/)?.[1] || "302";
189
+ return c.redirect(target, Number.parseInt(code, 10));
190
+ }
191
+ }
192
+ }
193
+ }
194
+ await next();
195
+ });
196
+ });
197
+ Object.entries(authMap).forEach(([dir, content]) => {
198
+ const credentials = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
199
+ const [username, password] = line.split(":");
200
+ return { username, password };
201
+ });
202
+ if (credentials.length > 0) {
203
+ const authPath = dir === "/" ? "*" : `${dir.replace(/\/$/, "")}/*`;
204
+ app.use(authPath, async (c, next) => {
205
+ const handler = basicAuth({
206
+ verifyUser: (u, p) => credentials.some((cred) => cred.username === u && cred.password === p),
207
+ realm: "Restricted Area"
208
+ });
209
+ return handler(c, next);
210
+ });
211
+ }
212
+ });
213
+ pages.forEach(({ urlPath, dirPath, component }) => {
214
+ const routes = [urlPath, `${urlPath}/*`];
215
+ if (dirPath) {
216
+ routes.push(dirPath);
217
+ if (dirPath !== "/") {
218
+ routes.push(`${dirPath}/*`);
219
+ }
220
+ }
221
+ routes.forEach((route) => {
222
+ app.all(route, async (c) => {
223
+ const $_GET = c.req.query();
224
+ const body = await c.req.parseBody({ all: true }).catch(() => ({}));
225
+ const $_POST = {};
226
+ const $_FILES = {};
227
+ for (const [key, value] of Object.entries(body)) {
228
+ if (value instanceof File || Array.isArray(value) && value[0] instanceof File) {
229
+ $_FILES[key] = value;
230
+ } else {
231
+ $_POST[key] = value;
232
+ }
233
+ }
234
+ const $_COOKIE = getCookie(c);
235
+ const $_REQUEST = {
236
+ ...$_COOKIE,
237
+ ...$_GET,
238
+ ...$_POST
239
+ };
240
+ const $_ENV = typeof process !== "undefined" && process.env ? process.env : c.env || {};
241
+ let $_SESSION = {};
242
+ const sRaw = getCookie(c, SESS_KEY);
243
+ if (sRaw) {
244
+ try {
245
+ $_SESSION = JSON.parse(decodeURIComponent(sRaw));
246
+ } catch {
247
+ }
248
+ }
249
+ let responseStatus = 200;
250
+ const responseHeaders = {
251
+ "Content-Type": "text/html; charset=utf-8"
252
+ };
253
+ const $_SERVER = {
254
+ ...$_ENV,
255
+ REQUEST_METHOD: c.req.method,
256
+ REQUEST_URI: c.req.url,
257
+ REMOTE_ADDR: c.req.header("x-forwarded-for") || "127.0.0.1",
258
+ USER_AGENT: c.req.header("user-agent") || "",
259
+ SCRIPT_NAME: urlPath,
260
+ PATH_INFO: c.req.path.replace(urlPath, "") || "/",
261
+ QUERY_STRING: new URL(c.req.url).search.slice(1)
262
+ };
263
+ const cgiinfo = generateCgiInfo({
264
+ $_SERVER,
265
+ $_REQUEST,
266
+ $_SESSION,
267
+ config: siteConfig
268
+ });
269
+ const context = {
270
+ $_GET,
271
+ $_POST,
272
+ $_FILES,
273
+ $_REQUEST,
274
+ $_COOKIE,
275
+ $_ENV,
276
+ $_SERVER,
277
+ $_SESSION,
278
+ config: siteConfig,
279
+ c,
280
+ header: (name, value) => {
281
+ responseHeaders[name.toLocaleLowerCase()] = value;
282
+ },
283
+ status: (code) => {
284
+ responseStatus = code;
285
+ },
286
+ redirect: (url, status = 302) => {
287
+ return { __type: "redirect", url, status };
288
+ },
289
+ cgiinfo,
290
+ request_headers: () => {
291
+ return Object.fromEntries(c.req.raw.headers.entries());
292
+ },
293
+ response_headers: () => {
294
+ return responseHeaders;
295
+ },
296
+ log: (message) => {
297
+ if (options.logger) {
298
+ options.logger(message, "info");
299
+ } else {
300
+ console.log(`[CGI LOG] ${message}`);
301
+ }
302
+ },
303
+ get_version: () => {
304
+ return `MatchboxCGI/v${package_default.version}`;
305
+ },
306
+ /**
307
+ * Returns information about all loaded CGI modules
308
+ * @returns Array of module information containing urlPath and dirPath
309
+ */
310
+ get_modules: () => {
311
+ return pages.map((page) => ({
312
+ urlPath: page.urlPath,
313
+ dirPath: page.dirPath
314
+ }));
315
+ }
316
+ };
317
+ try {
318
+ const result = await component(context);
319
+ const sessionValue = encodeURIComponent(JSON.stringify($_SESSION));
320
+ const sessionOptions = {
321
+ path: options.sessionCookie?.path || "/",
322
+ httpOnly: true,
323
+ sameSite: options.sessionCookie?.sameSite || "Lax"
324
+ };
325
+ if (options.sessionCookie?.secure !== void 0) {
326
+ sessionOptions.secure = options.sessionCookie.secure;
327
+ }
328
+ if (options.sessionCookie?.domain) {
329
+ sessionOptions.domain = options.sessionCookie.domain;
330
+ }
331
+ if (options.sessionCookie?.maxAge) {
332
+ sessionOptions.maxAge = options.sessionCookie.maxAge;
333
+ }
334
+ if (isRedirectObject(result)) {
335
+ setCookie(c, SESS_KEY, sessionValue, sessionOptions);
336
+ return c.redirect(result.url, result.status);
337
+ }
338
+ if (result instanceof Response) {
339
+ setCookie(c, SESS_KEY, sessionValue, sessionOptions);
340
+ return result;
341
+ }
342
+ setCookie(c, SESS_KEY, sessionValue, sessionOptions);
343
+ Object.entries(responseHeaders).forEach(([key, value]) => {
344
+ c.header(key, value);
345
+ });
346
+ const contentType = responseHeaders["content-type"];
347
+ if (contentType?.includes("application/json")) {
348
+ return c.json(
349
+ // biome-ignore lint/suspicious/noExplicitAny: to JSON response
350
+ result ?? { success: true },
351
+ responseStatus
352
+ );
353
+ }
354
+ return c.html(result, responseStatus);
355
+ } catch (error) {
356
+ return c.html(generateCgiError({ error, $_SERVER }), 500);
357
+ }
358
+ });
359
+ });
360
+ });
361
+ return app;
362
+ };
363
+
364
+ // src/with-defaults.ts
365
+ var loadPagesFromPublic = () => {
3
366
  const modules = import.meta.glob("/public/**/*.cgi.{tsx,jsx}", {
4
367
  eager: true
5
368
  });
@@ -68,7 +431,7 @@ const loadPagesFromPublic = () => {
68
431
  }, {});
69
432
  return { pages, authMap, rewriteMap };
70
433
  };
71
- const createCgi = (options) => {
434
+ var createCgi = (options) => {
72
435
  const resolvedConfig = typeof __MATCHBOX_CONFIG__ === "undefined" ? {} : __MATCHBOX_CONFIG__;
73
436
  const { pages, authMap, rewriteMap } = loadPagesFromPublic();
74
437
  return createCgiWithPages(pages, resolvedConfig, authMap, rewriteMap, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tknf/matchbox",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "A Simple Web Server Framework",
5
5
  "keywords": [
6
6
  "cgi",