@tknf/matchbox 0.2.4 → 0.2.6

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,143 +1,11 @@
1
- // src/cgi.ts
2
1
  import { Hono } from "hono";
3
2
  import { basicAuth } from "hono/basic-auth";
4
3
  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) => {
4
+ import { generateCgiError, generateCgiInfo } from "./html.js";
5
+ const isRedirectObject = (obj) => {
138
6
  return obj && obj.__type === "redirect" && typeof obj.url === "string";
139
7
  };
140
- var createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {}, options = {}) => {
8
+ const createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {}, options = {}) => {
141
9
  const app = new Hono();
142
10
  const SESS_KEY = options.sessionCookie?.name || "_SESSION_ID";
143
11
  if (options.middleware && options.middleware.length > 0) {
@@ -301,7 +169,7 @@ var createCgiWithPages = (pages, siteConfig = {}, authMap = {}, rewriteMap = {},
301
169
  }
302
170
  },
303
171
  get_version: () => {
304
- return `MatchboxCGI/v${package_default.version}`;
172
+ return `MatchboxCGI/v${"0.2.6"}`;
305
173
  },
306
174
  /**
307
175
  * Returns information about all loaded CGI modules
package/dist/html.js CHANGED
@@ -1,6 +1,5 @@
1
- // src/html.ts
2
1
  import { html } from "hono/html";
3
- var generateCgiInfo = ({
2
+ const generateCgiInfo = ({
4
3
  $_SERVER,
5
4
  $_SESSION,
6
5
  $_REQUEST,
@@ -43,7 +42,7 @@ var generateCgiInfo = ({
43
42
  `;
44
43
  };
45
44
  };
46
- var generateCgiError = ({
45
+ const generateCgiError = ({
47
46
  error,
48
47
  $_SERVER
49
48
  }) => {
package/dist/index.js CHANGED
@@ -1,441 +1,4 @@
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
- };
1
+ import { createCgi } from "./with-defaults.js";
439
2
  export {
440
3
  createCgi
441
4
  };
package/dist/plugin.js CHANGED
@@ -1,7 +1,6 @@
1
- // src/plugin.ts
2
- import fs from "fs";
3
- import path from "path";
4
- var MatchboxPlugin = (options = {}) => {
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const MatchboxPlugin = (options = {}) => {
5
4
  let viteConfig;
6
5
  const siteConfig = options.config || {};
7
6
  const publicDir = options.publicDir || "public";
@@ -13,7 +12,7 @@ var MatchboxPlugin = (options = {}) => {
13
12
  exclude: ["matchbox"]
14
13
  },
15
14
  ssr: {
16
- noExternal: ["matchbox"]
15
+ noExternal: true
17
16
  },
18
17
  // .htpasswd, .htaccess, .htdigest, .htgroup files in /public should be treated as raw assets
19
18
  assetsInclude: [
@@ -1,368 +1,5 @@
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 = () => {
1
+ import { createCgiWithPages } from "./cgi.js";
2
+ const loadPagesFromPublic = () => {
366
3
  const modules = import.meta.glob("/public/**/*.cgi.{tsx,jsx}", {
367
4
  eager: true
368
5
  });
@@ -431,7 +68,7 @@ var loadPagesFromPublic = () => {
431
68
  }, {});
432
69
  return { pages, authMap, rewriteMap };
433
70
  };
434
- var createCgi = (options) => {
71
+ const createCgi = (options) => {
435
72
  const resolvedConfig = typeof __MATCHBOX_CONFIG__ === "undefined" ? {} : __MATCHBOX_CONFIG__;
436
73
  const { pages, authMap, rewriteMap } = loadPagesFromPublic();
437
74
  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.4",
3
+ "version": "0.2.6",
4
4
  "description": "A Simple Web Server Framework",
5
5
  "keywords": [
6
6
  "cgi",