@hyperspan/framework 0.3.2 → 0.3.3
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/build.ts +1 -1
- package/dist/assets.js +2 -0
- package/dist/chunk-atw8cdg1.js +19 -0
- package/dist/middleware.js +178 -0
- package/dist/server.js +2 -1
- package/package.json +12 -6
- package/src/actions.test.ts +5 -5
- package/src/actions.ts +3 -3
- package/src/clientjs/hyperspan-client.ts +1 -1
- package/src/clientjs/{idiomorph.esm.js → idiomorph.ts} +34 -34
- package/src/middleware.ts +19 -0
- package/src/server.ts +12 -1
package/build.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { build } from 'bun';
|
|
2
2
|
|
|
3
|
-
const entrypoints = ['./src/server.ts', './src/assets.ts'];
|
|
3
|
+
const entrypoints = ['./src/server.ts', './src/assets.ts', './src/middleware.ts'];
|
|
4
4
|
const external = ['@hyperspan/html'];
|
|
5
5
|
const outdir = './dist';
|
|
6
6
|
const target = 'node';
|
package/dist/assets.js
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
7
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
8
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
9
|
+
for (let key of __getOwnPropNames(mod))
|
|
10
|
+
if (!__hasOwnProp.call(to, key))
|
|
11
|
+
__defProp(to, key, {
|
|
12
|
+
get: () => mod[key],
|
|
13
|
+
enumerable: true
|
|
14
|
+
});
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
18
|
+
|
|
19
|
+
export { __toESM, __commonJS };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__commonJS,
|
|
3
|
+
__toESM
|
|
4
|
+
} from "./chunk-atw8cdg1.js";
|
|
5
|
+
|
|
6
|
+
// ../../node_modules/timestring/index.js
|
|
7
|
+
var require_timestring = __commonJS((exports, module) => {
|
|
8
|
+
module.exports = parseTimestring;
|
|
9
|
+
var DEFAULT_OPTS = {
|
|
10
|
+
hoursPerDay: 24,
|
|
11
|
+
daysPerWeek: 7,
|
|
12
|
+
weeksPerMonth: 4,
|
|
13
|
+
monthsPerYear: 12,
|
|
14
|
+
daysPerYear: 365.25
|
|
15
|
+
};
|
|
16
|
+
var UNIT_MAP = {
|
|
17
|
+
ms: ["ms", "milli", "millisecond", "milliseconds"],
|
|
18
|
+
s: ["s", "sec", "secs", "second", "seconds"],
|
|
19
|
+
m: ["m", "min", "mins", "minute", "minutes"],
|
|
20
|
+
h: ["h", "hr", "hrs", "hour", "hours"],
|
|
21
|
+
d: ["d", "day", "days"],
|
|
22
|
+
w: ["w", "week", "weeks"],
|
|
23
|
+
mth: ["mon", "mth", "mths", "month", "months"],
|
|
24
|
+
y: ["y", "yr", "yrs", "year", "years"]
|
|
25
|
+
};
|
|
26
|
+
function parseTimestring(value, returnUnit, opts) {
|
|
27
|
+
opts = Object.assign({}, DEFAULT_OPTS, opts || {});
|
|
28
|
+
if (typeof value === "number" || value.match(/^[-+]?[0-9.]+$/g)) {
|
|
29
|
+
value = parseInt(value) + "ms";
|
|
30
|
+
}
|
|
31
|
+
let totalSeconds = 0;
|
|
32
|
+
const unitValues = getUnitValues(opts);
|
|
33
|
+
const groups = value.toLowerCase().replace(/[^.\w+-]+/g, "").match(/[-+]?[0-9.]+[a-z]+/g);
|
|
34
|
+
if (groups === null) {
|
|
35
|
+
throw new Error(`The value [${value}] could not be parsed by timestring`);
|
|
36
|
+
}
|
|
37
|
+
groups.forEach((group) => {
|
|
38
|
+
const value2 = group.match(/[0-9.]+/g)[0];
|
|
39
|
+
const unit = group.match(/[a-z]+/g)[0];
|
|
40
|
+
totalSeconds += getSeconds(value2, unit, unitValues);
|
|
41
|
+
});
|
|
42
|
+
if (returnUnit) {
|
|
43
|
+
return convert(totalSeconds, returnUnit, unitValues);
|
|
44
|
+
}
|
|
45
|
+
return totalSeconds;
|
|
46
|
+
}
|
|
47
|
+
function getUnitValues(opts) {
|
|
48
|
+
const unitValues = {
|
|
49
|
+
ms: 0.001,
|
|
50
|
+
s: 1,
|
|
51
|
+
m: 60,
|
|
52
|
+
h: 3600
|
|
53
|
+
};
|
|
54
|
+
unitValues.d = opts.hoursPerDay * unitValues.h;
|
|
55
|
+
unitValues.w = opts.daysPerWeek * unitValues.d;
|
|
56
|
+
unitValues.mth = opts.daysPerYear / opts.monthsPerYear * unitValues.d;
|
|
57
|
+
unitValues.y = opts.daysPerYear * unitValues.d;
|
|
58
|
+
return unitValues;
|
|
59
|
+
}
|
|
60
|
+
function getUnitKey(unit) {
|
|
61
|
+
for (const key of Object.keys(UNIT_MAP)) {
|
|
62
|
+
if (UNIT_MAP[key].indexOf(unit) > -1) {
|
|
63
|
+
return key;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`The unit [${unit}] is not supported by timestring`);
|
|
67
|
+
}
|
|
68
|
+
function getSeconds(value, unit, unitValues) {
|
|
69
|
+
return value * unitValues[getUnitKey(unit)];
|
|
70
|
+
}
|
|
71
|
+
function convert(value, unit, unitValues) {
|
|
72
|
+
return value / unitValues[getUnitKey(unit)];
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ../../node_modules/hono/dist/middleware/etag/digest.js
|
|
77
|
+
var mergeBuffers = (buffer1, buffer2) => {
|
|
78
|
+
if (!buffer1) {
|
|
79
|
+
return buffer2;
|
|
80
|
+
}
|
|
81
|
+
const merged = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
|
|
82
|
+
merged.set(new Uint8Array(buffer1), 0);
|
|
83
|
+
merged.set(buffer2, buffer1.byteLength);
|
|
84
|
+
return merged;
|
|
85
|
+
};
|
|
86
|
+
var generateDigest = async (stream, generator) => {
|
|
87
|
+
if (!stream) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
let result = undefined;
|
|
91
|
+
const reader = stream.getReader();
|
|
92
|
+
for (;; ) {
|
|
93
|
+
const { value, done } = await reader.read();
|
|
94
|
+
if (done) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
result = await generator(mergeBuffers(result, value));
|
|
98
|
+
}
|
|
99
|
+
if (!result) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return Array.prototype.map.call(new Uint8Array(result), (x) => x.toString(16).padStart(2, "0")).join("");
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ../../node_modules/hono/dist/middleware/etag/index.js
|
|
106
|
+
var RETAINED_304_HEADERS = [
|
|
107
|
+
"cache-control",
|
|
108
|
+
"content-location",
|
|
109
|
+
"date",
|
|
110
|
+
"etag",
|
|
111
|
+
"expires",
|
|
112
|
+
"vary"
|
|
113
|
+
];
|
|
114
|
+
function etagMatches(etag2, ifNoneMatch) {
|
|
115
|
+
return ifNoneMatch != null && ifNoneMatch.split(/,\s*/).indexOf(etag2) > -1;
|
|
116
|
+
}
|
|
117
|
+
function initializeGenerator(generator) {
|
|
118
|
+
if (!generator) {
|
|
119
|
+
if (crypto && crypto.subtle) {
|
|
120
|
+
generator = (body) => crypto.subtle.digest({
|
|
121
|
+
name: "SHA-1"
|
|
122
|
+
}, body);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return generator;
|
|
126
|
+
}
|
|
127
|
+
var etag = (options) => {
|
|
128
|
+
const retainedHeaders = options?.retainedHeaders ?? RETAINED_304_HEADERS;
|
|
129
|
+
const weak = options?.weak ?? false;
|
|
130
|
+
const generator = initializeGenerator(options?.generateDigest);
|
|
131
|
+
return async function etag2(c, next) {
|
|
132
|
+
const ifNoneMatch = c.req.header("If-None-Match") ?? null;
|
|
133
|
+
await next();
|
|
134
|
+
const res = c.res;
|
|
135
|
+
let etag3 = res.headers.get("ETag");
|
|
136
|
+
if (!etag3) {
|
|
137
|
+
if (!generator) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const hash = await generateDigest(res.clone().body, generator);
|
|
141
|
+
if (hash === null) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
etag3 = weak ? `W/"${hash}"` : `"${hash}"`;
|
|
145
|
+
}
|
|
146
|
+
if (etagMatches(etag3, ifNoneMatch)) {
|
|
147
|
+
c.res = new Response(null, {
|
|
148
|
+
status: 304,
|
|
149
|
+
statusText: "Not Modified",
|
|
150
|
+
headers: {
|
|
151
|
+
ETag: etag3
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
c.res.headers.forEach((_, key) => {
|
|
155
|
+
if (retainedHeaders.indexOf(key.toLowerCase()) === -1) {
|
|
156
|
+
c.res.headers.delete(key);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
c.res.headers.set("ETag", etag3);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/middleware.ts
|
|
166
|
+
var import_timestring = __toESM(require_timestring(), 1);
|
|
167
|
+
function cacheTime(timeStrOrSeconds) {
|
|
168
|
+
return (c, next) => etag()(c, () => {
|
|
169
|
+
if (c.req.method.toUpperCase() === "GET") {
|
|
170
|
+
const timeInSeconds = typeof timeStrOrSeconds === "number" ? timeStrOrSeconds : import_timestring.default(timeStrOrSeconds);
|
|
171
|
+
c.header("Cache-Control", `public, max-age=${timeInSeconds}`);
|
|
172
|
+
}
|
|
173
|
+
return next();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
export {
|
|
177
|
+
cacheTime
|
|
178
|
+
};
|
package/dist/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
buildClientCSS,
|
|
3
3
|
buildClientJS
|
|
4
4
|
} from "./assets.js";
|
|
5
|
+
import"./chunk-atw8cdg1.js";
|
|
5
6
|
|
|
6
7
|
// src/server.ts
|
|
7
8
|
import { readdir } from "node:fs/promises";
|
|
@@ -1869,7 +1870,7 @@ function createRoute(handler) {
|
|
|
1869
1870
|
const streamOpt = context.req.query("__nostream");
|
|
1870
1871
|
const streamingEnabled = !userIsBot && (streamOpt !== undefined ? streamOpt : true);
|
|
1871
1872
|
if (isHSHtml(routeContent)) {
|
|
1872
|
-
if (streamingEnabled) {
|
|
1873
|
+
if (streamingEnabled && routeContent.asyncContent?.length > 0) {
|
|
1873
1874
|
return new StreamResponse(renderStream(routeContent));
|
|
1874
1875
|
} else {
|
|
1875
1876
|
const output = await renderAsync(routeContent);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspan/framework",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
5
|
"main": "dist/server.ts",
|
|
6
6
|
"types": "src/server.ts",
|
|
@@ -17,13 +17,17 @@
|
|
|
17
17
|
"types": "./src/server.ts",
|
|
18
18
|
"default": "./dist/server.js"
|
|
19
19
|
},
|
|
20
|
-
"./actions": {
|
|
21
|
-
"types": "./src/actions.ts",
|
|
22
|
-
"default": "./src/actions.ts"
|
|
23
|
-
},
|
|
24
20
|
"./assets": {
|
|
25
21
|
"types": "./src/assets.ts",
|
|
26
22
|
"default": "./dist/assets.js"
|
|
23
|
+
},
|
|
24
|
+
"./middleware": {
|
|
25
|
+
"types": "./src/middleware.ts",
|
|
26
|
+
"default": "./dist/middleware.js"
|
|
27
|
+
},
|
|
28
|
+
"./unstable/actions": {
|
|
29
|
+
"types": "./src/actions.ts",
|
|
30
|
+
"default": "./src/actions.ts"
|
|
27
31
|
}
|
|
28
32
|
},
|
|
29
33
|
"author": "Vance Lucas <vance@vancelucas.com>",
|
|
@@ -56,6 +60,7 @@
|
|
|
56
60
|
"@types/bun": "^1.2.14",
|
|
57
61
|
"@types/node": "^22.15.20",
|
|
58
62
|
"@types/react": "^19.1.5",
|
|
63
|
+
"@types/timestring": "^7.0.0",
|
|
59
64
|
"prettier": "^3.5.3",
|
|
60
65
|
"typescript": "^5.8.3"
|
|
61
66
|
},
|
|
@@ -63,6 +68,7 @@
|
|
|
63
68
|
"@hyperspan/html": "^0.1.7",
|
|
64
69
|
"hono": "^4.7.10",
|
|
65
70
|
"isbot": "^5.1.28",
|
|
66
|
-
"
|
|
71
|
+
"timestring": "^7.0.0",
|
|
72
|
+
"zod": "^3.25.67"
|
|
67
73
|
}
|
|
68
74
|
}
|
package/src/actions.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import z from 'zod
|
|
2
|
-
import {
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { unstable__createAction } from './actions';
|
|
3
3
|
import { describe, it, expect } from 'bun:test';
|
|
4
4
|
import { html, render, type HSHtml } from '@hyperspan/html';
|
|
5
5
|
import type { Context } from 'hono';
|
|
@@ -22,7 +22,7 @@ describe('createAction', () => {
|
|
|
22
22
|
const schema = z.object({
|
|
23
23
|
name: z.string(),
|
|
24
24
|
});
|
|
25
|
-
const action =
|
|
25
|
+
const action = unstable__createAction(schema, formWithNameOnly);
|
|
26
26
|
|
|
27
27
|
const formResponse = render(action.render({ data: { name: 'John' } }) as HSHtml);
|
|
28
28
|
expect(formResponse).toContain('value="John"');
|
|
@@ -34,7 +34,7 @@ describe('createAction', () => {
|
|
|
34
34
|
const schema = z.object({
|
|
35
35
|
name: z.string().nonempty(),
|
|
36
36
|
});
|
|
37
|
-
const action =
|
|
37
|
+
const action = unstable__createAction(schema, formWithNameOnly)
|
|
38
38
|
.post((c, { data }) => {
|
|
39
39
|
return html`<div>Thanks for submitting the form, ${data?.name}!</div>`;
|
|
40
40
|
})
|
|
@@ -65,7 +65,7 @@ describe('createAction', () => {
|
|
|
65
65
|
const schema = z.object({
|
|
66
66
|
name: z.string().nonempty(),
|
|
67
67
|
});
|
|
68
|
-
const action =
|
|
68
|
+
const action = unstable__createAction(schema)
|
|
69
69
|
.form(formWithNameOnly)
|
|
70
70
|
.post((c, { data }) => {
|
|
71
71
|
return html`<div>Thanks for submitting the form, ${data?.name}!</div>`;
|
package/src/actions.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface HSAction<T extends z.ZodTypeAny> {
|
|
|
32
32
|
run(method: 'GET' | 'POST', c: Context): Promise<THSResponseTypes>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function
|
|
35
|
+
export function unstable__createAction<T extends z.ZodTypeAny>(
|
|
36
36
|
schema: T | null = null,
|
|
37
37
|
form: Parameters<HSAction<T>['form']>[0] | null = null
|
|
38
38
|
) {
|
|
@@ -87,7 +87,7 @@ export function createAction<T extends z.ZodTypeAny>(
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
const formData = await c.req.formData();
|
|
90
|
-
const jsonData =
|
|
90
|
+
const jsonData = unstable__formDataToJSON(formData);
|
|
91
91
|
const schemaData = schema ? schema.safeParse(jsonData) : null;
|
|
92
92
|
const data = schemaData?.success ? (schemaData.data as z.infer<T>) : undefined;
|
|
93
93
|
let error: z.ZodError | Error | null = null;
|
|
@@ -128,7 +128,7 @@ export type THSHandlerResponse = (context: Context) => THSResponseTypes | Promis
|
|
|
128
128
|
*
|
|
129
129
|
* @link https://stackoverflow.com/a/75406413
|
|
130
130
|
*/
|
|
131
|
-
export function
|
|
131
|
+
export function unstable__formDataToJSON(formData: FormData): Record<string, string | string[]> {
|
|
132
132
|
let object = {};
|
|
133
133
|
|
|
134
134
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
/**
|
|
2
3
|
* @typedef {object} ConfigHead
|
|
3
4
|
*
|
|
@@ -206,9 +207,8 @@ var Idiomorph = (function () {
|
|
|
206
207
|
*/
|
|
207
208
|
function saveAndRestoreFocus(ctx, fn) {
|
|
208
209
|
if (!ctx.config.restoreFocus) return fn();
|
|
209
|
-
let activeElement =
|
|
210
|
-
document.activeElement
|
|
211
|
-
);
|
|
210
|
+
let activeElement =
|
|
211
|
+
/** @type {HTMLInputElement|HTMLTextAreaElement|null} */ document.activeElement;
|
|
212
212
|
|
|
213
213
|
// don't bother if the active element is not an input or textarea
|
|
214
214
|
if (
|
|
@@ -324,7 +324,7 @@ var Idiomorph = (function () {
|
|
|
324
324
|
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;
|
|
325
325
|
if (ctx.idMap.has(newChild)) {
|
|
326
326
|
// node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm
|
|
327
|
-
const newEmptyChild = document.createElement(/** @type {Element} */
|
|
327
|
+
const newEmptyChild = document.createElement(/** @type {Element} */ newChild.tagName);
|
|
328
328
|
oldParent.insertBefore(newEmptyChild, insertionPoint);
|
|
329
329
|
morphNode(newEmptyChild, newChild, ctx);
|
|
330
330
|
ctx.callbacks.afterNodeAdded(newEmptyChild);
|
|
@@ -431,8 +431,8 @@ var Idiomorph = (function () {
|
|
|
431
431
|
*/
|
|
432
432
|
function isSoftMatch(oldNode, newNode) {
|
|
433
433
|
// ok to cast: if one is not element, `id` and `tagName` will be undefined and we'll just compare that.
|
|
434
|
-
const oldElt = /** @type {Element} */
|
|
435
|
-
const newElt = /** @type {Element} */
|
|
434
|
+
const oldElt = /** @type {Element} */ oldNode;
|
|
435
|
+
const newElt = /** @type {Element} */ newNode;
|
|
436
436
|
|
|
437
437
|
return (
|
|
438
438
|
oldElt.nodeType === newElt.nodeType &&
|
|
@@ -483,7 +483,7 @@ var Idiomorph = (function () {
|
|
|
483
483
|
let cursor = startInclusive;
|
|
484
484
|
// remove nodes until the endExclusive node
|
|
485
485
|
while (cursor && cursor !== endExclusive) {
|
|
486
|
-
let tempNode = /** @type {Node} */
|
|
486
|
+
let tempNode = /** @type {Node} */ cursor;
|
|
487
487
|
cursor = cursor.nextSibling;
|
|
488
488
|
removeNode(ctx, tempNode);
|
|
489
489
|
}
|
|
@@ -503,11 +503,9 @@ var Idiomorph = (function () {
|
|
|
503
503
|
function moveBeforeById(parentNode, id, after, ctx) {
|
|
504
504
|
const target =
|
|
505
505
|
/** @type {Element} - will always be found */
|
|
506
|
-
(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
ctx.pantry.querySelector(`[id="${id}"]`)
|
|
510
|
-
);
|
|
506
|
+
(ctx.target.id === id && ctx.target) ||
|
|
507
|
+
ctx.target.querySelector(`[id="${id}"]`) ||
|
|
508
|
+
ctx.pantry.querySelector(`[id="${id}"]`);
|
|
511
509
|
removeElementFromAncestorsIdMaps(target, ctx);
|
|
512
510
|
moveBefore(parentNode, target, after);
|
|
513
511
|
return target;
|
|
@@ -587,7 +585,7 @@ var Idiomorph = (function () {
|
|
|
587
585
|
// ignore the head element
|
|
588
586
|
} else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== 'morph') {
|
|
589
587
|
// ok to cast: if newContent wasn't also a <head>, it would've got caught in the `!isSoftMatch` branch above
|
|
590
|
-
handleHeadElement(oldNode, /** @type {HTMLHeadElement} */
|
|
588
|
+
handleHeadElement(oldNode, /** @type {HTMLHeadElement} */ newContent, ctx);
|
|
591
589
|
} else {
|
|
592
590
|
morphAttributes(oldNode, newContent, ctx);
|
|
593
591
|
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
|
@@ -613,8 +611,8 @@ var Idiomorph = (function () {
|
|
|
613
611
|
// if is an element type, sync the attributes from the
|
|
614
612
|
// new node into the new node
|
|
615
613
|
if (type === 1 /* element type */) {
|
|
616
|
-
const oldElt = /** @type {Element} */
|
|
617
|
-
const newElt = /** @type {Element} */
|
|
614
|
+
const oldElt = /** @type {Element} */ oldNode;
|
|
615
|
+
const newElt = /** @type {Element} */ newNode;
|
|
618
616
|
|
|
619
617
|
const oldAttributes = oldElt.attributes;
|
|
620
618
|
const newAttributes = newElt.attributes;
|
|
@@ -868,9 +866,9 @@ var Idiomorph = (function () {
|
|
|
868
866
|
let promises = [];
|
|
869
867
|
for (const newNode of nodesToAppend) {
|
|
870
868
|
// TODO: This could theoretically be null, based on type
|
|
871
|
-
let newElt = /** @type {ChildNode} */
|
|
872
|
-
|
|
873
|
-
|
|
869
|
+
let newElt = /** @type {ChildNode} */ document
|
|
870
|
+
.createRange()
|
|
871
|
+
.createContextualFragment(newNode.outerHTML).firstChild;
|
|
874
872
|
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
|
875
873
|
if (('href' in newElt && newElt.href) || ('src' in newElt && newElt.src)) {
|
|
876
874
|
/** @type {(result?: any) => void} */ let resolve;
|
|
@@ -1116,16 +1114,16 @@ var Idiomorph = (function () {
|
|
|
1116
1114
|
return document.createElement('div'); // dummy parent element
|
|
1117
1115
|
} else if (typeof newContent === 'string') {
|
|
1118
1116
|
return normalizeParent(parseContent(newContent));
|
|
1119
|
-
} else if (generatedByIdiomorph.has(/** @type {Element} */
|
|
1117
|
+
} else if (generatedByIdiomorph.has(/** @type {Element} */ newContent)) {
|
|
1120
1118
|
// the template tag created by idiomorph parsing can serve as a dummy parent
|
|
1121
|
-
return /** @type {Element} */
|
|
1119
|
+
return /** @type {Element} */ newContent;
|
|
1122
1120
|
} else if (newContent instanceof Node) {
|
|
1123
1121
|
if (newContent.parentNode) {
|
|
1124
1122
|
// we can't use the parent directly because newContent may have siblings
|
|
1125
1123
|
// that we don't want in the morph, and reparenting might be expensive (TODO is it?),
|
|
1126
1124
|
// so instead we create a fake parent node that only sees a slice of its children.
|
|
1127
1125
|
/** @type {Element} */
|
|
1128
|
-
return /** @type {any} */
|
|
1126
|
+
return /** @type {any} */ new SlicedParentNode(newContent);
|
|
1129
1127
|
} else {
|
|
1130
1128
|
// a single node is added as a child to a dummy parent
|
|
1131
1129
|
const dummyParent = document.createElement('div');
|
|
@@ -1154,7 +1152,7 @@ var Idiomorph = (function () {
|
|
|
1154
1152
|
/** @param {Node} node */
|
|
1155
1153
|
constructor(node) {
|
|
1156
1154
|
this.originalNode = node;
|
|
1157
|
-
this.realParentNode = /** @type {Element} */
|
|
1155
|
+
this.realParentNode = /** @type {Element} */ node.parentNode;
|
|
1158
1156
|
this.previousSibling = node.previousSibling;
|
|
1159
1157
|
this.nextSibling = node.nextSibling;
|
|
1160
1158
|
}
|
|
@@ -1178,16 +1176,19 @@ var Idiomorph = (function () {
|
|
|
1178
1176
|
* @returns {Element[]}
|
|
1179
1177
|
*/
|
|
1180
1178
|
querySelectorAll(selector) {
|
|
1181
|
-
return this.childNodes.reduce(
|
|
1182
|
-
|
|
1183
|
-
if (node
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1179
|
+
return this.childNodes.reduce(
|
|
1180
|
+
(results, node) => {
|
|
1181
|
+
if (node instanceof Element) {
|
|
1182
|
+
if (node.matches(selector)) results.push(node);
|
|
1183
|
+
const nodeList = node.querySelectorAll(selector);
|
|
1184
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
1185
|
+
results.push(nodeList[i]);
|
|
1186
|
+
}
|
|
1187
1187
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1188
|
+
return results;
|
|
1189
|
+
},
|
|
1190
|
+
/** @type {Element[]} */ []
|
|
1191
|
+
);
|
|
1191
1192
|
}
|
|
1192
1193
|
|
|
1193
1194
|
/**
|
|
@@ -1255,9 +1256,8 @@ var Idiomorph = (function () {
|
|
|
1255
1256
|
'<body><template>' + newContent + '</template></body>',
|
|
1256
1257
|
'text/html'
|
|
1257
1258
|
);
|
|
1258
|
-
let content =
|
|
1259
|
-
responseDoc.body.querySelector('template')
|
|
1260
|
-
).content;
|
|
1259
|
+
let content =
|
|
1260
|
+
/** @type {HTMLTemplateElement} */ responseDoc.body.querySelector('template').content;
|
|
1261
1261
|
generatedByIdiomorph.add(content);
|
|
1262
1262
|
return content;
|
|
1263
1263
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Context, Next } from 'hono';
|
|
2
|
+
import { etag } from 'hono/etag';
|
|
3
|
+
import timestring from 'timestring';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cache the response for a given time length ('30s', '1d', '1w', '1m', etc) or given number of seconds
|
|
7
|
+
*/
|
|
8
|
+
export function cacheTime(timeStrOrSeconds: string | number) {
|
|
9
|
+
return (c: Context, next: Next) =>
|
|
10
|
+
etag()(c, () => {
|
|
11
|
+
// Only cache GET requests
|
|
12
|
+
if (c.req.method.toUpperCase() === 'GET') {
|
|
13
|
+
const timeInSeconds =
|
|
14
|
+
typeof timeStrOrSeconds === 'number' ? timeStrOrSeconds : timestring(timeStrOrSeconds);
|
|
15
|
+
c.header('Cache-Control', `public, max-age=${timeInSeconds}`);
|
|
16
|
+
}
|
|
17
|
+
return next();
|
|
18
|
+
});
|
|
19
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { buildClientJS, buildClientCSS } from './assets';
|
|
|
6
6
|
import { Hono, type Context } from 'hono';
|
|
7
7
|
import { serveStatic } from 'hono/bun';
|
|
8
8
|
import { HTTPException } from 'hono/http-exception';
|
|
9
|
+
|
|
9
10
|
import type { HandlerResponse, MiddlewareHandler } from 'hono/types';
|
|
10
11
|
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
11
12
|
|
|
@@ -55,14 +56,23 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
|
|
|
55
56
|
|
|
56
57
|
const api: THSRoute = {
|
|
57
58
|
_kind: 'hsRoute',
|
|
59
|
+
/**
|
|
60
|
+
* Add a GET route handler (primary page display)
|
|
61
|
+
*/
|
|
58
62
|
get(handler: THSRouteHandler) {
|
|
59
63
|
_handlers['GET'] = handler;
|
|
60
64
|
return api;
|
|
61
65
|
},
|
|
66
|
+
/**
|
|
67
|
+
* Add a POST route handler (typically to process form data)
|
|
68
|
+
*/
|
|
62
69
|
post(handler: THSRouteHandler) {
|
|
63
70
|
_handlers['POST'] = handler;
|
|
64
71
|
return api;
|
|
65
72
|
},
|
|
73
|
+
/**
|
|
74
|
+
* Add middleware specific to this route
|
|
75
|
+
*/
|
|
66
76
|
middleware(middleware: Array<MiddlewareHandler>) {
|
|
67
77
|
_middleware = middleware;
|
|
68
78
|
return api;
|
|
@@ -93,7 +103,8 @@ export function createRoute(handler?: THSRouteHandler): THSRoute {
|
|
|
93
103
|
|
|
94
104
|
// Render HSHtml if returned from route handler
|
|
95
105
|
if (isHSHtml(routeContent)) {
|
|
96
|
-
if
|
|
106
|
+
// Stream only if enabled and there is async content to stream
|
|
107
|
+
if (streamingEnabled && (routeContent as HSHtml).asyncContent?.length > 0) {
|
|
97
108
|
return new StreamResponse(renderStream(routeContent as HSHtml)) as Response;
|
|
98
109
|
} else {
|
|
99
110
|
const output = await renderAsync(routeContent as HSHtml);
|