@spirobel/mininext 0.3.6 → 0.3.7

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/package.json CHANGED
@@ -3,15 +3,15 @@
3
3
  "license" : "MIT" ,
4
4
  "module": "dist/mininext/mininext.js",
5
5
  "type": "module",
6
- "main": "dist/mininext/mininext.js",
7
- "types": "dist/mininext/mininext.d.ts",
6
+ "main": "dist/mininext.js",
7
+ "types": "dist/mininext.d.ts",
8
8
  "scripts": {
9
9
  "publish": "bun run build && npm publish",
10
10
  "build": "tsc",
11
11
  "clean":"rm -rf ./dist"
12
12
  },
13
- "files": ["dist", "mininext"],
14
- "version": "0.3.6",
13
+ "files": ["dist"],
14
+ "version": "0.3.7",
15
15
  "devDependencies": {
16
16
  "@types/bun": "latest"
17
17
  },
@@ -1 +0,0 @@
1
- export {};
@@ -1,38 +0,0 @@
1
- import { expect, test, mock } from "bun:test";
2
- import { html, url } from "../mininext/mininext";
3
- // Example of creating a mock request object
4
- const mockRequestObject = {
5
- method: "GET", // or 'POST', etc.
6
- url: "http://example.com/api/some-endpoint",
7
- body: JSON.stringify({ key: "value" }),
8
- headers: {
9
- "Content-Type": "application/json",
10
- get: () => undefined,
11
- },
12
- };
13
- const makeMockRequest = mock(() => mockRequestObject);
14
- test("no xss when BasedHtml inside of html", async () => {
15
- const req = makeMockRequest();
16
- url.set([
17
- [
18
- "/",
19
- (mini) => {
20
- const basedHtmlString = html `<h2>
21
- this html string is resolved (it can't contain functions like
22
- mini.html HtmlStrings)
23
- </h2>
24
- ${"<script>alert(1)</script>"}`;
25
- return mini.html `<h1> ${"<script>alert(1)</script>"}this HtmlString can contain functions,
26
- that get resolved at request time.</h1>${basedHtmlString}
27
- <h3>${(mini) => mini.html ` ${"<script>alert(1)</script>"}it gives you convenient access to the request object,
28
- anywhere in your code base:${mini.req.url}.
29
- no need to do "props drilling" anymore. just write a function like this:
30
- (mini: Mini)=> return some html and you are golden. ${"<script>alert(1)</script>"} `}`;
31
- },
32
- ],
33
- ]);
34
- const response = await url.match(req, "/");
35
- const responseText = await response?.text();
36
- expect(responseText).not.toInclude("<script>alert(1)</script>");
37
- expect(responseText).toInclude("&lt;script&gt;alert(1)&lt;/script&gt;");
38
- });
package/mininext/html.ts DELETED
@@ -1,354 +0,0 @@
1
- import type {
2
- HandlerReturnType,
3
- HtmlHandler,
4
- LazyHandlerReturnType,
5
- Mini,
6
- } from "./url";
7
- export type HtmlStringValues<T = unknown> =
8
- | HtmlString
9
- | HtmlString[]
10
- | BasedHtml
11
- | BasedHtml[]
12
- | (BasedHtml | HtmlString)[]
13
- | string
14
- | number
15
- | HtmlHandler<T>
16
- | JsonString
17
- | LazyHandlerReturnType
18
- | undefined;
19
- export type JsonStringValues<T = unknown> =
20
- | HtmlStringValues<T>
21
- | { [key: string]: any };
22
- export class HtmlString extends Array {
23
- /**
24
- * a HtmlString is by default resolved.
25
- * if we we pass a function as a value to the html`` template string, it will be unresolved.
26
- * it can also become unresolved if an unresolved HtmlString is passed into it as a value
27
- */
28
- resolved = true;
29
- async resolve<T>(mini: Mini<T>) {
30
- if (this.resolved) return this;
31
-
32
- for (const [index, htmlPiece] of this.entries()) {
33
- if (htmlPiece instanceof HtmlString) {
34
- let resolvedHtmlPiece = await htmlPiece.resolve(mini);
35
- if (this instanceof JsonString || this instanceof DangerJsonInHtml) {
36
- this[index] = JSON.stringify(resolvedHtmlPiece);
37
- } else {
38
- this[index] = resolvedHtmlPiece;
39
- }
40
- } else if (typeof htmlPiece === "function") {
41
- let resolvedHtmlPiece = await htmlPiece(mini); //passing mini
42
- //same cases as outer if statement
43
- if (resolvedHtmlPiece instanceof HtmlString) {
44
- resolvedHtmlPiece = await resolvedHtmlPiece.resolve(mini);
45
- } else if (htmlPiece instanceof BasedHtml) {
46
- this[index] = htmlPiece;
47
- } else {
48
- if (this instanceof JsonString || this instanceof DangerJsonInHtml) {
49
- resolvedHtmlPiece = JSON.stringify(resolvedHtmlPiece);
50
- } else {
51
- const notEmpty = resolvedHtmlPiece || "";
52
- // values will be escaped by default
53
- resolvedHtmlPiece = Bun.escapeHTML(notEmpty + "");
54
- }
55
- }
56
- // Replace the function with the resolved HTML piece in place
57
- this[index] = resolvedHtmlPiece;
58
- } else if (htmlPiece instanceof BasedHtml) {
59
- this[index] = htmlPiece;
60
- }
61
- }
62
- this.resolved = true;
63
- return this;
64
- }
65
- flat(depth: number = 1) {
66
- const flattened = super.flat(depth);
67
- const newHtmlString = new (this.constructor as any)(...flattened);
68
- newHtmlString.resolved = this.resolved;
69
- return newHtmlString as this;
70
- }
71
- }
72
-
73
- export function html<X = unknown>(
74
- strings: TemplateStringsArray,
75
- ...values: HtmlStringValues<X>[]
76
- ) {
77
- const htmlStringArray = new HtmlString();
78
- htmlStringArray.resolved = true;
79
-
80
- // Iterate over strings and values, alternating between them
81
- for (const [index, string] of strings.entries()) {
82
- htmlStringArray.push(string);
83
-
84
- if (index < values.length) {
85
- const value = values[index];
86
-
87
- // we can pass arrays of HtmlString and they will get flattened in the HtmlResponder
88
- if (
89
- Array.isArray(value) &&
90
- value.every(
91
- (val) => val instanceof HtmlString || val instanceof BasedHtml
92
- )
93
- ) {
94
- // If the value is an array of HtmlString objects, add the whole array as a single value
95
- const notResolved = new HtmlString(...(value as any[]));
96
- notResolved.resolved = false;
97
- values[index] = notResolved;
98
- htmlStringArray.resolved = false; // we could bother with .find here
99
- } else if (typeof value === "function") {
100
- htmlStringArray.resolved = false;
101
- values[index] = value;
102
- } else if (value instanceof JsonString) {
103
- values[index] = html`<div style="color:red;">
104
- Please use dangerjson to include json in html. Untrusted input needs
105
- to pass through a html template function to get escaped. You can do
106
- html -> dangerjson -> html if you want!
107
- </div>`;
108
- } else if (!(value instanceof HtmlString || value instanceof BasedHtml)) {
109
- const notEmpty = value || "";
110
- // values will be escaped by default
111
- values[index] = Bun.escapeHTML(notEmpty + "");
112
- } else if (value instanceof HtmlString) {
113
- if (!value.resolved) {
114
- htmlStringArray.resolved = false;
115
- }
116
- }
117
- htmlStringArray.push(values[index]);
118
- }
119
- }
120
- return htmlStringArray;
121
- }
122
- export class JsonString extends HtmlString {}
123
- export class DangerJsonInHtml extends HtmlString {}
124
- function JsonTemplateProcessor(
125
- danger: true
126
- ): <X = unknown>(
127
- strings: TemplateStringsArray,
128
- ...values: JsonStringValues<X>[]
129
- ) => DangerJsonInHtml;
130
- function JsonTemplateProcessor(
131
- danger?: false | undefined
132
- ): <X = unknown>(
133
- strings: TemplateStringsArray,
134
- ...values: JsonStringValues<X>[]
135
- ) => JsonString;
136
-
137
- function JsonTemplateProcessor(danger: boolean = false) {
138
- const constructorr = danger
139
- ? () => new DangerJsonInHtml()
140
- : () => new JsonString();
141
- return function <X = unknown>(
142
- strings: TemplateStringsArray,
143
- ...values: JsonStringValues<X>[]
144
- ) {
145
- const jsonStringArray = constructorr();
146
- jsonStringArray.resolved = true;
147
-
148
- // Iterate over strings and values, alternating between them
149
- for (const [index, string] of strings.entries()) {
150
- jsonStringArray.push(string);
151
-
152
- if (index < values.length) {
153
- const value = values[index];
154
- // we can pass arrays of HtmlString and they will get flattened in the HtmlResponder
155
- if (
156
- Array.isArray(value) &&
157
- value.every(
158
- (val) => val instanceof HtmlString || val instanceof BasedHtml
159
- )
160
- ) {
161
- // If the value is an array of HtmlString objects, add the whole array as a single value
162
- const notResolved = new HtmlString(...(value as any[]));
163
- notResolved.resolved = false;
164
- values[index] = notResolved;
165
- jsonStringArray.resolved = false; // we could bother with .find here
166
- } else if (typeof value === "function") {
167
- jsonStringArray.resolved = false;
168
- values[index] = value;
169
- } else if (value instanceof HtmlString || value instanceof BasedHtml) {
170
- if (value instanceof HtmlString && !value.resolved) {
171
- jsonStringArray.resolved = false;
172
- }
173
- values[index] = value;
174
- } else if (!(value instanceof JsonString)) {
175
- // values will be turned into a JSON string
176
- if (value) {
177
- values[index] = JSON.stringify(value);
178
- }
179
- }
180
- jsonStringArray.push(values[index]);
181
- }
182
- }
183
- return jsonStringArray;
184
- };
185
- }
186
- export const json = JsonTemplateProcessor();
187
- export const dangerjson = JsonTemplateProcessor(true);
188
- export const commonHead = html` <meta
189
- name="viewport"
190
- content="width=device-width, initial-scale=1.0"
191
- />
192
- <script>
193
- /* prevent form resubmission */
194
- if (window.history.replaceState) {
195
- window.history.replaceState(null, null, window.location.href);
196
- }
197
- </script>`;
198
- export const cssReset = html` <style>
199
- /* CSS Reset */
200
- * {
201
- margin: 0;
202
- padding: 0;
203
- box-sizing: border-box;
204
- }
205
-
206
- /* Set the background color to black */
207
- html,
208
- body {
209
- background-color: #000;
210
- color: #fff; /* Set the default text color to white for better contrast */
211
- }
212
- </style>`;
213
- let default_head: HtmlHandler = (mini: Mini) => mini.html`
214
- <title>mini-next</title>
215
- ${commonHead} ${cssReset}
216
- `;
217
- /**
218
- * Set the default head for all pages. Can still be overwritten on a per page basis
219
- * @param defaultHead - HtmlString
220
- *
221
- * @example Here is what a default head might look like:
222
- * ```ts
223
- *head((mini)=>mini.html` <title>hello hello</title> `);
224
- * url.set([
225
- * ["/", (mini) => mini.html`<h1>Hello world</h1>`],
226
- * [
227
- * "/bye",
228
- * (mini) =>
229
- * mini.html`<h1>Goodbye world</h1>${mini.head(
230
- * mini.html` <title>bye bye</title>`
231
- * )}`,
232
- * ],
233
- * ]);
234
- * ```
235
- */
236
- export function head(defaultHead: HtmlHandler) {
237
- default_head = defaultHead;
238
- }
239
-
240
- export async function htmlResponder(
241
- mini: Mini,
242
- maybeUnresolved: HandlerReturnType,
243
- head: HtmlHandler = default_head,
244
- options: ResponseInit = {
245
- headers: {
246
- "Content-Type": "text/html; charset=utf-8",
247
- },
248
- }
249
- ) {
250
- if (!(maybeUnresolved instanceof HtmlString)) {
251
- maybeUnresolved = html`${maybeUnresolved + ""}`;
252
- }
253
- if (maybeUnresolved instanceof DangerJsonInHtml) {
254
- maybeUnresolved = html`<div style="color:red;">
255
- Use json and not dangerjson. The purpose of dangerjson is to be explicit
256
- when you embed unescaped json elements in an html document.
257
- </div>`;
258
- }
259
- if (!(maybeUnresolved instanceof JsonString)) {
260
- const reloader = new HtmlString();
261
- reloader.push(global.Reloader || "");
262
- maybeUnresolved = html`<!DOCTYPE html>
263
- <html>
264
- <head>
265
- ${reloader} ${head}
266
- </head>
267
- <body>
268
- ${maybeUnresolved}
269
- </body>
270
- </html> `;
271
- } else {
272
- const headers = {
273
- ...options.headers,
274
- ...{ "Content-Type": "application/json; charset=utf-8" },
275
- };
276
- options.headers = headers;
277
- }
278
- const definitelyResolved = await maybeUnresolved.resolve(mini);
279
- const flattend = definitelyResolved.flat(Infinity);
280
- async function* stepGen() {
281
- let index = 0;
282
- while (index < flattend.length) {
283
- const step = flattend[index++];
284
- if (step) yield String(step);
285
- }
286
- }
287
- function Stream(a: any) {
288
- return a as ReadableStream;
289
- }
290
- return new Response(Stream(stepGen), options);
291
- }
292
- /**
293
- * Generic html error type guard
294
- * @param submissionResult output of some function
295
- * @returns boolean - true if the given object has a property called "error" and its value is an instance of HtmlString
296
- */
297
- export function isError(
298
- submissionResult:
299
- | any
300
- | {
301
- error: HtmlString;
302
- }
303
- ): submissionResult is { error: HtmlString } {
304
- return (
305
- "error" in submissionResult && submissionResult.error instanceof HtmlString
306
- );
307
- }
308
-
309
- declare global {
310
- var Reloader: BasedHtml | HtmlString | undefined;
311
- }
312
- /**
313
- * The difference between this and HtmlString is that it is fully resolved and only accepts primitive types.
314
- * In plain english this means:
315
- * It does not accept functions (that will be resolved at request time with (mini)=>mini.html) like mini.html does.
316
- */
317
- export class BasedHtml extends String {}
318
- export type BasedHtmlValues =
319
- | number
320
- | string
321
- | undefined
322
- | null
323
- | boolean
324
- | BasedHtml
325
- | BasedHtml[];
326
- //TODO make it so we can embed BasedHtml into mini.html partially resolved html strings
327
- /**
328
- * The difference between this and HtmlString is that it is fully resolved and only accepts primitive types.
329
- * @param strings - html literals
330
- * @param values - values will get escaped to prevent xss
331
- * @returns
332
- */
333
- export const basedHtml = (
334
- strings: TemplateStringsArray,
335
- ...values: BasedHtmlValues[]
336
- ) => {
337
- // Apply escapeHtml to each value before using them in the template string
338
- // In case it didn't already get escaped
339
- for (const [index, value] of values.entries()) {
340
- // we can pass arrays of BasedHtml and they will get flattened automatically
341
- if (
342
- Array.isArray(value) &&
343
- value.every((val) => val instanceof BasedHtml)
344
- ) {
345
- // If the value is an array of BasedHtml objects, flatten it and add to ...values
346
- values[index] = value.join("");
347
- } else if (!(value instanceof BasedHtml)) {
348
- const notEmpty = value || "";
349
- // values will be escaped by default
350
- values[index] = Bun.escapeHTML(notEmpty + "");
351
- }
352
- }
353
- return new BasedHtml(String.raw({ raw: strings }, ...values));
354
- };
@@ -1,228 +0,0 @@
1
- import { url, Mini, type HtmlHandler } from "./url";
2
- import {
3
- isError,
4
- HtmlString,
5
- BasedHtml,
6
- head,
7
- commonHead,
8
- cssReset,
9
- basedHtml as html,
10
- } from "./html";
11
- import type { BunPlugin, Server, WebSocketHandler } from "bun";
12
- import { watch } from "fs/promises";
13
- import * as path from "path";
14
- function projectRoot() {
15
- return global.PROJECT_ROOT || import.meta.dir + "/../../../../";
16
- }
17
- declare global {
18
- var PROJECT_ROOT: string | undefined;
19
- }
20
- async function build(backendPath: string = "backend/backend.ts") {
21
- await buildBackend(backendPath);
22
- if (Bun.argv[2] === "dev") {
23
- await devServer();
24
- }
25
- }
26
-
27
- const streamPlugin: BunPlugin = {
28
- name: "node stream in the frontend",
29
- setup(build) {
30
- build.onResolve({ filter: /^stream$/ }, (args) => {
31
- const path_to_stream_lib = path.resolve(
32
- projectRoot(),
33
- "node_modules/stream-browserify/index.js"
34
- );
35
- if (path_to_stream_lib)
36
- return {
37
- path: path_to_stream_lib,
38
- };
39
- });
40
- },
41
- };
42
- const bufferPlugin: BunPlugin = {
43
- name: "node buffer in the frontend",
44
- setup(build) {
45
- build.onResolve({ filter: /^buffer$/ }, (args) => {
46
- const path_to_buffer_lib = path.resolve(
47
- projectRoot(),
48
- "node_modules/buffer/index.js"
49
- );
50
- if (path_to_buffer_lib)
51
- return {
52
- path: path_to_buffer_lib,
53
- };
54
- });
55
- },
56
- };
57
- const cryptoPlugin: BunPlugin = {
58
- name: "node crypto in the frontend",
59
- setup(build) {
60
- build.onResolve({ filter: /^crypto$/ }, (args) => {
61
- const path_to_crypto_lib = path.resolve(
62
- projectRoot(),
63
- "node_modules/crypto-browserify/index.js"
64
- );
65
- if (path_to_crypto_lib)
66
- return {
67
- path: path_to_crypto_lib,
68
- };
69
- });
70
- },
71
- };
72
- async function buildBackend(backendPath: string = "backend/backend.ts") {
73
- global.FrontendScriptUrls = [];
74
- global.FrontendScripts = [];
75
- global.bundledSVGs = {};
76
- const i = await import(path.resolve(projectRoot(), backendPath));
77
-
78
- for (const frontend of url.getFrontends()) {
79
- const f = await buildFrontend(frontend);
80
- FrontendScriptUrls.push("/" + f.url);
81
- FrontendScripts.push(f.script);
82
- }
83
- for (const svgPath of url.getSvgPaths()) {
84
- const parsedSvgPath = path.parse(svgPath);
85
- const svgContent = Bun.file(
86
- path.join(projectRoot() + "/backend/", svgPath)
87
- );
88
- const svgHash = Bun.hash(await svgContent.arrayBuffer());
89
- const svgUrl = `/${parsedSvgPath.name}-${svgHash}.svg`;
90
- bundledSVGs[svgUrl] = {
91
- svgContent: await svgContent.text(),
92
- svgPath,
93
- };
94
- }
95
- const res = await Bun.build({
96
- entrypoints: [path.resolve(projectRoot(), backendPath)],
97
- outdir: path.resolve(projectRoot(), "dist"),
98
- naming: "backend.js",
99
- minify: Bun.argv[2] === "dev" ? false : true, //production
100
- target: "bun",
101
- define: {
102
- FrontendScripts: JSON.stringify(FrontendScripts),
103
- FrontendScriptUrls: JSON.stringify(FrontendScriptUrls),
104
- bundledSVGs: JSON.stringify(bundledSVGs),
105
- },
106
- });
107
- }
108
-
109
- async function buildFrontend(file: string) {
110
- const result = await Bun.build({
111
- entrypoints: [path.resolve(projectRoot(), `frontend/${file}`)],
112
- outdir: path.resolve(projectRoot(), "dist"),
113
- naming: "[name]-[hash].[ext]",
114
- minify: Bun.argv[2] === "dev" ? false : true, //production
115
- target: "browser",
116
- plugins: [bufferPlugin, streamPlugin, cryptoPlugin],
117
- });
118
- if (!result?.outputs[0]?.path) console.log(result);
119
- const url = path.basename(result.outputs[0].path);
120
- //results.push({ file, p });
121
- return { url, script: await result.outputs[0].text() };
122
- }
123
-
124
- async function devServer() {
125
- //start the reloader and tell browser to refresh once
126
- await buildBackend();
127
- let refreshed_once = false;
128
- const server = Bun.serve({
129
- port: 3001,
130
- fetch(request) {
131
- const success: Boolean = server.upgrade(request);
132
- return success
133
- ? new Response("Reloader works!")
134
- : new Response("Reloader WebSocket upgrade error", { status: 400 });
135
- },
136
- websocket: {
137
- open(ws) {
138
- ws.subscribe("reloader");
139
- if (!refreshed_once) {
140
- ws.send("Reload!");
141
- refreshed_once = true;
142
- }
143
- },
144
- message(ws, message) {}, // a message is received
145
- },
146
- });
147
- async function watchAndBuild(dir: string) {
148
- try {
149
- //start the file watcher that will rebuild frontend on save
150
- const watcher = watch(path.resolve(projectRoot(), dir), {
151
- recursive: true,
152
- });
153
- for await (const event of watcher) {
154
- buildBackend().then(() => {
155
- // tell browser to refresh again because we saw a change
156
- server.publish("reloader", "Reload!");
157
- });
158
- }
159
- } catch (e) {
160
- console.log(
161
- `mini-next dev server has trouble watching "./${dir}", does the directory exist?`
162
- );
163
- }
164
- }
165
- watchAndBuild("frontend");
166
- watchAndBuild("backend");
167
- }
168
- const standardDevReloader = html`
169
- <script>
170
- function reloader() {
171
- let socket = null;
172
-
173
- function connectWebSocket() {
174
- if (socket) {
175
- return;
176
- }
177
- socket = new WebSocket("ws://localhost:3001/reload");
178
-
179
- socket.addEventListener("message", (event) => {
180
- window.location.reload();
181
- });
182
-
183
- socket.addEventListener("close", (event) => {
184
- // Reestablish the connection after 1 second
185
- socket = null;
186
- });
187
-
188
- socket.addEventListener("error", (event) => {
189
- socket = null;
190
- });
191
- }
192
- connectWebSocket(); // connect to reloader, if it does not work:
193
- setInterval(connectWebSocket, 1000); // retry every 1 second
194
- }
195
- reloader();
196
- </script>
197
- `;
198
- async function makeEntrypoint() {
199
- let module;
200
- const backendImportPath = projectRoot() + "/dist/backend.js";
201
- try {
202
- // @ts-ignore
203
- module = await import(backendImportPath);
204
- } catch (error) {
205
- await build();
206
- // @ts-ignore
207
- module = await import(backendImportPath);
208
- }
209
- return module.default() as {
210
- fetch: (req: Request, server: Server) => Promise<Response>;
211
- websocket: WebSocketHandler;
212
- };
213
- }
214
- export {
215
- html,
216
- url,
217
- head,
218
- build,
219
- makeEntrypoint,
220
- isError,
221
- BasedHtml,
222
- HtmlString,
223
- type HtmlHandler,
224
- Mini,
225
- standardDevReloader,
226
- commonHead,
227
- cssReset,
228
- };
package/mininext/url.ts DELETED
@@ -1,554 +0,0 @@
1
- import type { Server, WebSocketHandler } from "bun";
2
- import { htmlResponder, html, json, dangerjson, HtmlString } from "./html";
3
- import type {
4
- BasedHtml,
5
- DangerJsonInHtml,
6
- JsonString,
7
- JsonStringValues,
8
- } from "./html";
9
- export type Form = {
10
- post: boolean;
11
- urlencoded: boolean;
12
- multipart: boolean;
13
- formJson?: any;
14
- formData?: FormData;
15
- formName?: string;
16
- hiddenField?: HtmlString;
17
- actionlink<Y = unknown>(
18
- qs?: string[] | string,
19
- settings?: LinkSettings
20
- ): (mini: Mini<Y>) => string;
21
- onPostSubmit<F>(cb: () => F): F | undefined;
22
- };
23
-
24
- export type DataMaker<X, Z = unknown> =
25
- | ((mini: Mini, rerun?: Z) => DataMakerReturnType<X>)
26
- | (() => DataMakerReturnType<X>);
27
- export type DataMakerReturnType<X> = X | Promise<X>;
28
- export type HandlerReturnType =
29
- | JsonString
30
- | DangerJsonInHtml
31
- | HtmlString
32
- | string
33
- | void;
34
- export type LazyHandlerReturnType =
35
- | HandlerReturnType
36
- | Promise<HandlerReturnType>;
37
-
38
- export type NamedForm<Z> = {
39
- formResponse: LazyHandlerReturnType;
40
- formInfo?: Z;
41
- };
42
- export type NamedFormHandlerReturnType<X> =
43
- | HandlerReturnType
44
- | Promise<HandlerReturnType>
45
- | NamedForm<X>
46
- | Promise<NamedForm<X>>;
47
-
48
- /**
49
- * Mini - the data object can be filled with url.data
50
- * @example
51
- * ``` js
52
- * const {html,json, css, data, req, form, link, svg, deliver, route, params, header, head } = mini //pull everything out of the mini handbag
53
- * ```
54
- */
55
- export class Mini<X = unknown> {
56
- html: typeof html<X>;
57
- css: typeof html<X>;
58
- json: typeof json<X>;
59
- dangerjson: typeof dangerjson<X>;
60
-
61
- data: X;
62
- req!: Request;
63
- head!: (head: HtmlHandler | HtmlString) => undefined;
64
- headers!: (headers: HeadersInit, overwrite?: boolean) => undefined;
65
- options!: (options: ResponseInit) => undefined;
66
- deliver!: typeof url.deliver;
67
- route!: string;
68
- params!: URLSearchParams;
69
- form!: Form;
70
- requrl!: Readonly<URL>;
71
-
72
- constructor(mini: Mini<unknown>, data: X) {
73
- Object.assign(this, mini);
74
- this.html = html<X>;
75
- this.css = html<X>;
76
- this.json = json<X>;
77
- this.dangerjson = dangerjson<X>;
78
- this.data = data;
79
- this.deliver = url.deliver;
80
- this.form.onPostSubmit = (cb) => {
81
- if (this.form.formName) {
82
- if (
83
- this.form.formData &&
84
- this.form.formData.get("formName") === this.form.formName
85
- ) {
86
- return cb();
87
- } else if (
88
- this.form.formJson &&
89
- this.form.formJson.formName === this.form.formName
90
- ) {
91
- return cb();
92
- }
93
- } else if (this.form.post) {
94
- return cb();
95
- }
96
- };
97
- }
98
- }
99
- /**
100
- * HtmlHandler
101
- * @param mini - the mini object
102
- * @returns - return a partially resolved html string with mini.html
103
- * @example
104
- * ``` js
105
- * const {html,json, css, data, req, form, link, svg, deliver, route, params, header, head } = mini //pull everything out of the mini handbag
106
- * ```
107
- */
108
- export type HtmlHandler<Y = unknown> =
109
- | ((mini: Mini<Y>) => LazyHandlerReturnType)
110
- | (() => LazyHandlerReturnType);
111
- export type NamedFormHandler<Y = unknown, Z = undefined> =
112
- | ((mini: Mini<Y>) => NamedFormHandlerReturnType<Z>)
113
- | (() => NamedFormHandlerReturnType<Z>);
114
-
115
- declare global {
116
- var FrontendScripts: Array<string>; // An array of the bundled scriptFiles corresponding to the frontend files, example frontends[0] = "index.tsx" -> FrontendScripts[0] = CONTENT OF frontend/index.js
117
- var FrontendScriptUrls: Array<string>;
118
- var bundledSVGs: Record<string, { svgContent: string; svgPath: string }>;
119
- }
120
- export type ScriptTag = (...params: any[]) => Promise<HtmlString>;
121
- interface LinkSettings {
122
- [key: string]: string | null | undefined;
123
- }
124
- export class url {
125
- static websocket: WebSocketHandler | undefined = undefined;
126
- static server: Server;
127
-
128
- // direct mapping of "url string" -> function leads to Html Response
129
- static direct_handlers_html: ReadonlyMap<string, HtmlHandler>;
130
-
131
- // An array of the uncompiled frontend files, example frontends[0] = "index.tsx" -> frontend/index.tsx (from the project root)
132
- private static frontends: Array<string> = [];
133
- private static svgs: Map<string, ResponseInit> = new Map();
134
-
135
- static svg(
136
- path: string,
137
- options: ResponseInit = {
138
- headers: {
139
- "Content-Type": "image/svg+xml",
140
- "Content-Disposition": "attachment",
141
- },
142
- }
143
- ) {
144
- url.svgs.set(path, options);
145
- var foundEntry = Object.entries(bundledSVGs).find(
146
- ([key, value]) => value.svgPath === path
147
- );
148
-
149
- return foundEntry && foundEntry[0];
150
- }
151
- static frontend(path: string, snippet?: HtmlHandler) {
152
- const frontendIndex = url.frontends.push(path) - 1;
153
- const scriptUrl = FrontendScriptUrls[frontendIndex];
154
-
155
- return html` ${snippet}
156
- <script type="module" src="${scriptUrl}"></script>`; // return an html script tag with the index hash
157
- }
158
- /**
159
- * This is used by the frontend bundler in order to find all frontends and their corresponding script files.
160
- */
161
- static getFrontends() {
162
- return url.frontends;
163
- }
164
- static getSvgPaths() {
165
- return [...url.svgs.keys()];
166
- }
167
- static serveFrontend(req: Request) {
168
- const reqPath = new URL(req.url).pathname;
169
- const index = FrontendScriptUrls.indexOf(reqPath);
170
-
171
- if (index !== -1) {
172
- return new Response(FrontendScripts[index], {
173
- headers: {
174
- "Content-Type": "application/javascript; charset=utf-8",
175
- },
176
- });
177
- }
178
- }
179
- static serveSvg(req: Request) {
180
- const reqPath = new URL(req.url).pathname;
181
- const resolvedSvg = bundledSVGs[reqPath];
182
- if (resolvedSvg) {
183
- return new Response(
184
- resolvedSvg.svgContent,
185
- url.svgs.get(resolvedSvg.svgPath)
186
- );
187
- }
188
- }
189
- /**
190
- * tool to expose data to a frontend as a global variable.
191
- * @param name this will be added as window.name to the window object in the frontend
192
- * @param value this will be parsed as json in the frontend and asigned as follows: window.name = JSON.parsed(value)
193
- * @returns the script tag to be embeded in the html response
194
- *
195
- * @example
196
- * ``` js
197
- * //backend
198
- * url.deliver("user", userData); // window.user = JSON.parse(userData)
199
- * //frontend
200
- * const user = window["user"];
201
- * ```
202
- * if you want to use types, declare them like so in your frontend code:
203
- * ``` ts
204
- * declare global {
205
- * var user: string;
206
- *}
207
- * ```
208
- */
209
- static deliver(name: string, value: JsonStringValues) {
210
- return html` <script type="application/json" id="${name}">
211
- ${dangerjson`${value}`}
212
- </script>
213
-
214
- <script>
215
- window["${name}"] = JSON.parse(
216
- document.getElementById("${name}").innerHTML
217
- );
218
- </script>`;
219
- }
220
- /**
221
- * @param dataHandler the function that prepares the data for the handlers
222
- * @example const {html,json, css, data, req, form, link, svg, deliver, route, params, header, head } = mini //pull everything out of the mini handbag
223
- * @returns
224
- */
225
- static data<T, Z>(dataMaker: DataMaker<T, Z>) {
226
- return {
227
- /**
228
- * @param dataHandler the function that prepares the data for the handlers
229
- * @example const {html,json, css, data, req, form, link, svg, deliver, route, params, header, head } = mini //pull everything out of the mini handbag
230
- * @returns
231
- */
232
- handler: (dataHandler: HtmlHandler<T>) => {
233
- return async (oldmini: Mini) => {
234
- const data = await dataMaker(oldmini);
235
- const mini = new Mini(oldmini, data);
236
-
237
- const unresolvedDataHandler = await dataHandler(mini); // passing mini
238
- if (unresolvedDataHandler instanceof HtmlString) {
239
- return await unresolvedDataHandler.resolve(mini);
240
- }
241
- return unresolvedDataHandler;
242
- };
243
- },
244
- dataMaker,
245
- /**
246
- * use this to **specify the input type for the functions**,
247
- *
248
- * that you want to use in the HtmlHandlers that follow this **data blend!**
249
- * @example type lol = typeof MaybeLoggedIn.$Mini
250
- */
251
- $Mini: {
252
- data: "DONT USE THIS DIRECTLY, ya goofball. This is just to infer the Mini type",
253
- } as Mini<T>,
254
- /**
255
- * use this to **specify the input type for the functions**,
256
- *
257
- * that you want to use in the Htmlhandlers that follow this **data blend!**
258
- * @example type haha = Mini<typeof MaybeLoggedIn.$Data>
259
- */
260
- $Data: {
261
- data: "DONT USE THIS DIRECTLY, ya goofball. This is just to infer the Mini type",
262
- } as T,
263
- };
264
- }
265
- /**
266
- * use this to define your routes.
267
- * @example
268
- * ``` js
269
- * url.set([
270
- * ["/", (mini) => mini.html`<h1>Hello world</h1>`],
271
- * ["/apple", (mini) => mini.html`<h1>Hello apple</h1>`],
272
- * ["/banana", (mini) => mini.html`<h1>Hello banana</h1>`],
273
- * ]);
274
- * ```
275
- */
276
- static set<K extends string>(entries: [K, HtmlHandler][]) {
277
- url.direct_handlers_html = new Map(entries) as ReadonlyMap<K, HtmlHandler>;
278
- }
279
- /**
280
- * wrap your handlers in this if you mutate something to prevent CSRF issues.
281
- * @param handler - normal html handler with mini as the argument
282
- * @returns a wrapped html handler that will only be called when the request is post
283
- */
284
- static post(handler: HtmlHandler) {
285
- return (mini: Mini) => {
286
- if (mini.form.post) {
287
- return handler(mini);
288
- } else {
289
- return no_post_warning;
290
- }
291
- };
292
- }
293
- /**
294
- * wrap your handlers in this if you mutate something to prevent CSRF issues.
295
- * @param handler - normal html handler with mini as the argument
296
- * @returns a wrapped html handler that will only be called when the request is post and contains a json body
297
- */
298
- static postJson(handler: HtmlHandler) {
299
- return (mini: Mini) => {
300
- if (mini.form.formJson) {
301
- return handler(mini);
302
- } else {
303
- return no_post_warning;
304
- }
305
- };
306
- }
307
- /**
308
- * wrap your handlers in this if you mutate something to prevent CSRF issues.
309
- * @param handler - normal html handler with mini as the argument
310
- * @returns a wrapped html handler that will only be called when the request is post and contains a FormData body
311
- */
312
- static postFormData(handler: HtmlHandler) {
313
- return (mini: Mini) => {
314
- if (mini.form.formData) {
315
- return handler(mini);
316
- } else {
317
- return no_post_warning;
318
- }
319
- };
320
- }
321
- /**
322
- * This is useful to decouple forms from routes.
323
- * @param name name of the form - mini.form.onPostSubmit() will only be called if a (possibly hidden) field called formName matches this
324
- * @param handler just like a normal handler (aka you can return the form as a HtmlString), but you can optionally return additional data in formInfo
325
- * @returns - { formResponse: result of the handler, formInfo?: some info about the form. Totally up to you}
326
- */
327
- static namedForm<X = unknown, Z = undefined>(
328
- name: string,
329
- handler: NamedFormHandler<X, Z>
330
- ) {
331
- return async (mini: Mini<X>) => {
332
- mini.form.formName = name;
333
- mini.form.hiddenField = html`<input
334
- type="hidden"
335
- name="formName"
336
- value="${name}"
337
- />`;
338
- const namedFormResponse = await handler(mini);
339
- let handlerResult = {} as NamedForm<Z>;
340
- if (
341
- typeof namedFormResponse !== "string" &&
342
- namedFormResponse &&
343
- "formResponse" in namedFormResponse
344
- ) {
345
- handlerResult.formResponse = await namedFormResponse.formResponse;
346
- handlerResult.formInfo = namedFormResponse.formInfo;
347
- } else {
348
- handlerResult.formResponse = namedFormResponse;
349
- }
350
- delete mini.form.formName;
351
- delete mini.form.hiddenField;
352
- return handlerResult;
353
- };
354
- }
355
-
356
- /**
357
- * pass in all the query string parameter names that you want to preserve in the link
358
- * @param Url - the url that you want to link to (example: "/login")
359
- * @param qs - the query string parameters that you want to preserve in the link
360
- * @param settings - key and string values that you want to set in the link
361
- * @returns - the link that you can use in your html template
362
- */
363
- static link<X>(
364
- Url: string,
365
- qs: string[] | string = "",
366
- settings?: LinkSettings
367
- ) {
368
- return (mini: Mini<X>) => {
369
- return url.currylink(Url, qs, mini.req, settings);
370
- };
371
- }
372
- static currylink(
373
- Url: string,
374
- qs: string[] | string,
375
- req: Request,
376
- settings?: LinkSettings
377
- ) {
378
- if (!Array.isArray(qs)) {
379
- qs = [qs];
380
- }
381
- // Create a new URL object from the current location
382
- // https://github.com/whatwg/url/issues/531#issuecomment-1337050285
383
- const GOOFY_HACK = "http://goofyhack.com";
384
- const updatedUrl = new URL(url.get(Url), GOOFY_HACK);
385
- for (const q of qs) {
386
- // Use URLSearchParams to set the name query parameter
387
- const reqParam = new URL(req.url).searchParams.get(q);
388
- if (reqParam) {
389
- updatedUrl.searchParams.set(q, reqParam);
390
- }
391
- }
392
- for (const key in settings) {
393
- const value = settings[key];
394
- if (value !== undefined && value !== null) {
395
- updatedUrl.searchParams.set(key, value);
396
- }
397
- }
398
- // Return the updated URL as a string
399
- return updatedUrl.toString().slice(GOOFY_HACK.length);
400
- }
401
- /**
402
- * This method retrieves a url from the urls array. If the url does not exist in the urls array, an error will be thrown.
403
- * @param {string} Url - The url to retrieve.
404
- * @return {string} - The retrieved url.
405
- * @throws Will throw an Error if the provided url is not found in the urls array.
406
- */
407
- static get(Url: string): string {
408
- const foundUrl = url.direct_handlers_html.get(Url);
409
- if (!foundUrl) {
410
- throw new Error(`URL "${html`${Url}`}" was not set.`);
411
- }
412
- return Url;
413
- }
414
- static async match(req: Request, reqPath?: string) {
415
- const miniurl: Readonly<URL> = Object.freeze(new URL(req.url));
416
- if (typeof reqPath === "undefined") {
417
- reqPath = miniurl.pathname;
418
- }
419
- const handler = url.direct_handlers_html.get(reqPath);
420
- if (handler) {
421
- //this is the source of mini
422
- let handlerHead: HtmlHandler | HtmlString | undefined = undefined;
423
- let handlerOptions: ResponseInit = {
424
- headers: {
425
- "Content-Type": "text/html; charset=utf-8",
426
- },
427
- };
428
- const post = req.method === "POST";
429
- let formJson: any;
430
- let formData: FormData | undefined;
431
- const urlencoded = (req.headers.get("Content-Type") + "").includes(
432
- "application/x-www-form-urlencoded"
433
- );
434
- const multipart = (req.headers.get("Content-Type") + "").includes(
435
- "multipart/form-data"
436
- );
437
- if (post && !urlencoded && !multipart) {
438
- const length = Number(req.headers.get("content-length"));
439
- const bodyNotEmpty = length > 0;
440
- if (bodyNotEmpty) {
441
- formJson = await req.json();
442
- } else {
443
- formJson = {};
444
- }
445
- }
446
- if (post && (urlencoded || multipart)) {
447
- formData = await req.formData();
448
- }
449
-
450
- const mini = new Mini(
451
- {
452
- requrl: miniurl,
453
- data: undefined,
454
- req,
455
- html,
456
- css: html,
457
- deliver: url.deliver,
458
- route: reqPath,
459
- params: new URL(req.url).searchParams,
460
- json,
461
- form: {
462
- post,
463
- urlencoded,
464
- multipart,
465
- formJson,
466
- formData,
467
- onPostSubmit(cb) {
468
- if (post) {
469
- return cb();
470
- }
471
- },
472
- actionlink: (qs = "", settings) => url.link(reqPath, qs, settings),
473
- },
474
- dangerjson,
475
- head: (head) => {
476
- handlerHead = head;
477
- },
478
- headers: (headers, overwrite = false) => {
479
- if (overwrite) {
480
- handlerOptions.headers = headers;
481
- } else {
482
- handlerOptions.headers = {
483
- ...handlerOptions.headers,
484
- ...headers,
485
- };
486
- }
487
- },
488
- options: (options) => {
489
- handlerOptions = options;
490
- },
491
- },
492
- undefined
493
- );
494
- const unresolved = await handler(mini); //passing mini
495
- return htmlResponder(mini, unresolved, handlerHead, handlerOptions);
496
- }
497
- }
498
- /**
499
- * user this to set the Websocket object. Check out [the bun docs](https://bun.sh/docs/api/websockets) for more details.
500
- * @param wsObject the websocketsocket object {@link WebSocketHandler}
501
- */
502
- static setWebsocket<T = undefined>(wsObject: WebSocketHandler<T>) {
503
- url.websocket = wsObject as WebSocketHandler;
504
- }
505
- /**
506
- * Send a message to all connected {@link ServerWebSocket} subscribed to a topic
507
- * @param topic The topic to publish to
508
- * @param message The data to send
509
- * @returns 0 if the message was dropped, -1 if backpressure was applied, or the number of bytes sent.
510
- */
511
- static publishHtml(topic: string, message: BasedHtml) {
512
- return url.server.publish(topic, message as string);
513
- }
514
- /**
515
- * Fetch handler that is called by the server when a request is made to any of the urls.
516
- * @param {Request} req - The Request object.
517
- * @return {Promise<Response>} - The Response object.
518
- */
519
- static install() {
520
- async function fetchFunction(req: Request, server: Server) {
521
- if (!url.server) url.server = server;
522
- //go through all the Htmlhandlers and see if there is a match
523
- let res = await url.match(req);
524
- if (res) return res;
525
-
526
- //handle frontend js file serving
527
- res = url.serveFrontend(req);
528
- if (res) return res;
529
- //handle svg file serving
530
- res = url.serveSvg(req);
531
- if (res) return res;
532
- // go through all the Htmlhandlers again with added slash at the end.
533
- res = await url.match(req, new URL(req.url).pathname + "/");
534
- if (res) return res;
535
-
536
- return new Response("No matching url found", { status: 404 });
537
- }
538
- return { fetch: fetchFunction, websocket: url.websocket };
539
- }
540
- }
541
-
542
- const no_post_warning = html`<div style="color:red;">
543
- This method is only accessible through the POST method. Remember to make all
544
- mutations (insert / update data in the database) only accessible via POST and
545
- implement your session cookies like this:
546
- <div
547
- style="color:#0FFF50; width:800px; overflow:wrap; margin-left:30px; margin-top:20px; margin-bottom:20px;"
548
- >
549
- "Set-Cookie": sessionId=="some random string made with crypto.randomUUID()"
550
- expires=Thu, 01 Jan 1970 00:00:00 GMT Secure; HttpOnly; SameSite=Strict;
551
- path=/,
552
- </div>
553
- This is necessary to prevent CSRF issues.
554
- </div>`;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes