@davidsouther/jiffies 2026.24.0 → 2026.24.2

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.
Files changed (226) hide show
  1. package/lib/esm/assert.d.ts +26 -0
  2. package/lib/esm/assert.js +38 -0
  3. package/lib/esm/awaitable.js +1 -0
  4. package/lib/esm/case.d.ts +1 -0
  5. package/lib/esm/case.js +5 -0
  6. package/lib/esm/components/accordion.d.ts +5 -0
  7. package/lib/esm/components/accordion.js +9 -0
  8. package/lib/esm/components/alert.d.ts +7 -0
  9. package/lib/esm/components/alert.js +31 -0
  10. package/lib/esm/components/button_bar.d.ts +8 -0
  11. package/lib/esm/components/button_bar.js +25 -0
  12. package/lib/esm/components/card.d.ts +8 -0
  13. package/lib/esm/components/card.js +31 -0
  14. package/lib/esm/components/children.d.ts +2 -0
  15. package/{src/components/children.ts → lib/esm/components/children.js} +2 -6
  16. package/lib/esm/components/form.d.ts +5 -0
  17. package/lib/esm/components/form.js +13 -0
  18. package/{src/components/index.ts → lib/esm/components/index.d.ts} +2 -15
  19. package/lib/esm/components/index.js +10 -0
  20. package/lib/esm/components/inline_edit.d.ts +12 -0
  21. package/lib/esm/components/inline_edit.js +48 -0
  22. package/lib/esm/components/link.d.ts +5 -0
  23. package/lib/esm/components/link.js +11 -0
  24. package/lib/esm/components/logger.d.ts +6 -0
  25. package/lib/esm/components/logger.js +22 -0
  26. package/lib/esm/components/modal.d.ts +2 -0
  27. package/{src/components/modal.ts → lib/esm/components/modal.js} +3 -8
  28. package/lib/esm/components/nav.d.ts +11 -0
  29. package/lib/esm/components/nav.js +27 -0
  30. package/lib/esm/components/property.d.ts +9 -0
  31. package/lib/esm/components/property.js +16 -0
  32. package/lib/esm/components/select.d.ts +10 -0
  33. package/lib/esm/components/select.js +3 -0
  34. package/lib/esm/components/tabs.d.ts +20 -0
  35. package/lib/esm/components/tabs.js +45 -0
  36. package/lib/esm/components/virtual_scroll.d.ts +42 -0
  37. package/lib/esm/components/virtual_scroll.js +94 -0
  38. package/lib/esm/debounce.d.ts +1 -0
  39. package/lib/esm/debounce.js +11 -0
  40. package/lib/esm/diff.d.ts +15 -0
  41. package/lib/esm/diff.js +50 -0
  42. package/lib/esm/display.d.ts +5 -0
  43. package/lib/esm/display.js +11 -0
  44. package/lib/esm/dom/css/border.d.ts +11 -0
  45. package/lib/esm/dom/css/border.js +27 -0
  46. package/lib/esm/dom/css/constants.d.ts +31 -0
  47. package/lib/esm/dom/css/constants.js +28 -0
  48. package/lib/esm/dom/css/core.d.ts +5 -0
  49. package/lib/esm/dom/css/core.js +24 -0
  50. package/lib/esm/dom/css/fstyle.d.ts +5 -0
  51. package/lib/esm/dom/css/fstyle.js +32 -0
  52. package/lib/esm/dom/css/sizing.d.ts +5 -0
  53. package/lib/esm/dom/css/sizing.js +10 -0
  54. package/lib/esm/dom/dom.d.ts +36 -0
  55. package/lib/esm/dom/dom.js +217 -0
  56. package/lib/esm/dom/fc.d.ts +10 -0
  57. package/lib/esm/dom/fc.js +32 -0
  58. package/lib/esm/dom/form/form.app.d.ts +1 -0
  59. package/lib/esm/dom/form/form.app.js +19 -0
  60. package/lib/esm/dom/form/form.d.ts +27 -0
  61. package/lib/esm/dom/form/form.js +65 -0
  62. package/lib/esm/dom/html.d.ts +112 -0
  63. package/{src/dom/html.ts → lib/esm/dom/html.js} +2 -14
  64. package/lib/esm/dom/hydrate.d.ts +39 -0
  65. package/lib/esm/dom/hydrate.js +187 -0
  66. package/lib/esm/dom/index.js +2 -0
  67. package/lib/esm/dom/navigation/index.d.ts +76 -0
  68. package/lib/esm/dom/navigation/index.js +292 -0
  69. package/lib/esm/dom/observable.d.ts +2 -0
  70. package/lib/esm/dom/observable.js +6 -0
  71. package/lib/esm/dom/provide.d.ts +3 -0
  72. package/lib/esm/dom/provide.js +7 -0
  73. package/lib/esm/dom/render.d.ts +8 -0
  74. package/lib/esm/dom/render.js +28 -0
  75. package/lib/esm/dom/router/link.d.ts +6 -0
  76. package/lib/esm/dom/router/link.js +3 -0
  77. package/lib/esm/dom/router/router.d.ts +13 -0
  78. package/lib/esm/dom/router/router.js +52 -0
  79. package/lib/esm/dom/svg.d.ts +64 -0
  80. package/{src/dom/svg.ts → lib/esm/dom/svg.js} +2 -19
  81. package/lib/esm/dom/types/css.d.ts +6590 -0
  82. package/lib/esm/dom/types/css.js +1 -0
  83. package/lib/esm/dom/types/dom.js +1 -0
  84. package/lib/esm/dom/types/html.d.ts +614 -0
  85. package/lib/esm/dom/types/html.js +1 -0
  86. package/lib/esm/dom/xml.d.ts +1 -0
  87. package/lib/esm/dom/xml.js +4 -0
  88. package/lib/esm/equal.d.ts +11 -0
  89. package/lib/esm/equal.js +43 -0
  90. package/lib/esm/fs.d.ts +72 -0
  91. package/lib/esm/fs.js +227 -0
  92. package/lib/esm/fs_node.d.ts +15 -0
  93. package/lib/esm/fs_node.js +45 -0
  94. package/lib/esm/generator.d.ts +1 -0
  95. package/lib/esm/generator.js +10 -0
  96. package/lib/esm/lock.d.ts +1 -0
  97. package/lib/esm/lock.js +23 -0
  98. package/lib/esm/log.d.ts +69 -0
  99. package/lib/esm/log.js +211 -0
  100. package/lib/esm/observable/event.d.ts +35 -0
  101. package/lib/esm/observable/event.js +46 -0
  102. package/lib/esm/observable/observable.d.ts +134 -0
  103. package/lib/esm/observable/observable.js +349 -0
  104. package/lib/esm/range.d.ts +1 -0
  105. package/lib/esm/range.js +7 -0
  106. package/lib/esm/result.d.ts +31 -0
  107. package/lib/esm/result.js +66 -0
  108. package/lib/esm/safe.d.ts +1 -0
  109. package/lib/esm/safe.js +10 -0
  110. package/lib/esm/server/http/apps.d.ts +5 -0
  111. package/lib/esm/server/http/apps.js +23 -0
  112. package/lib/esm/server/http/css.d.ts +5 -0
  113. package/lib/esm/server/http/css.js +43 -0
  114. package/lib/esm/server/http/index.d.ts +16 -0
  115. package/lib/esm/server/http/index.js +78 -0
  116. package/lib/esm/server/http/response.d.ts +4 -0
  117. package/lib/esm/server/http/response.js +43 -0
  118. package/lib/esm/server/http/sitemap.d.ts +2 -0
  119. package/lib/esm/server/http/sitemap.js +22 -0
  120. package/lib/esm/server/http/static.d.ts +2 -0
  121. package/lib/esm/server/http/static.js +22 -0
  122. package/lib/esm/server/http/typescript.d.ts +5 -0
  123. package/lib/esm/server/http/typescript.js +40 -0
  124. package/lib/esm/server/live-reload.d.ts +46 -0
  125. package/lib/esm/server/live-reload.js +161 -0
  126. package/lib/esm/server/main.d.ts +2 -0
  127. package/{src/server/main.ts → lib/esm/server/main.js} +8 -15
  128. package/lib/esm/server/ws/frame.d.ts +2 -0
  129. package/lib/esm/server/ws/frame.js +35 -0
  130. package/lib/esm/server/ws/handshake.d.ts +4 -0
  131. package/lib/esm/server/ws/handshake.js +32 -0
  132. package/lib/esm/server/ws/index.d.ts +14 -0
  133. package/lib/esm/server/ws/index.js +68 -0
  134. package/lib/esm/ssg/bundle.d.ts +14 -0
  135. package/lib/esm/ssg/bundle.js +73 -0
  136. package/lib/esm/ssg/copy-public.d.ts +6 -0
  137. package/lib/esm/ssg/copy-public.js +34 -0
  138. package/lib/esm/ssg/discover.d.ts +15 -0
  139. package/lib/esm/ssg/discover.js +117 -0
  140. package/lib/esm/ssg/main.d.ts +2 -0
  141. package/lib/esm/ssg/main.js +122 -0
  142. package/lib/esm/ssg/rewrite.d.ts +9 -0
  143. package/{src/ssg/rewrite.ts → lib/esm/ssg/rewrite.js} +6 -9
  144. package/lib/esm/ssg/ssg.d.ts +26 -0
  145. package/lib/esm/ssg/ssg.js +84 -0
  146. package/lib/esm/transpile.d.mts +3 -0
  147. package/lib/esm/transpile.mjs +12 -0
  148. package/package.json +11 -7
  149. package/src/404.html +0 -14
  150. package/src/assert.ts +0 -56
  151. package/src/case.ts +0 -5
  152. package/src/components/_notes +0 -33
  153. package/src/components/accordion.ts +0 -25
  154. package/src/components/alert.ts +0 -47
  155. package/src/components/button_bar.ts +0 -42
  156. package/src/components/card.ts +0 -54
  157. package/src/components/form.ts +0 -25
  158. package/src/components/inline_edit.ts +0 -78
  159. package/src/components/link.ts +0 -22
  160. package/src/components/logger.ts +0 -35
  161. package/src/components/nav.ts +0 -42
  162. package/src/components/property.ts +0 -32
  163. package/src/components/select.ts +0 -22
  164. package/src/components/tabs.ts +0 -82
  165. package/src/components/virtual_scroll.ts +0 -199
  166. package/src/debounce.ts +0 -14
  167. package/src/diff.ts +0 -82
  168. package/src/display.ts +0 -18
  169. package/src/dom/README.md +0 -107
  170. package/src/dom/SKILL.md +0 -201
  171. package/src/dom/css/border.ts +0 -47
  172. package/src/dom/css/constants.ts +0 -34
  173. package/src/dom/css/core.ts +0 -28
  174. package/src/dom/css/fstyle.ts +0 -42
  175. package/src/dom/css/sizing.ts +0 -11
  176. package/src/dom/dom.ts +0 -327
  177. package/src/dom/fc.ts +0 -81
  178. package/src/dom/form/form.app.ts +0 -44
  179. package/src/dom/form/form.ts +0 -151
  180. package/src/dom/form/index.html +0 -15
  181. package/src/dom/hydrate.ts +0 -206
  182. package/src/dom/navigation/index.ts +0 -349
  183. package/src/dom/observable.ts +0 -11
  184. package/src/dom/provide.ts +0 -11
  185. package/src/dom/render.ts +0 -41
  186. package/src/dom/router/link.ts +0 -14
  187. package/src/dom/router/router.ts +0 -72
  188. package/src/dom/types/css.ts +0 -10088
  189. package/src/dom/types/html.ts +0 -629
  190. package/src/dom/xml.ts +0 -11
  191. package/src/equal.ts +0 -66
  192. package/src/favicon.ico +0 -0
  193. package/src/fs.ts +0 -300
  194. package/src/fs_node.ts +0 -57
  195. package/src/generator.ts +0 -12
  196. package/src/hooks/_notes +0 -6
  197. package/src/lock.ts +0 -23
  198. package/src/log.ts +0 -307
  199. package/src/observable/_notes +0 -26
  200. package/src/observable/event.ts +0 -93
  201. package/src/observable/observable.ts +0 -484
  202. package/src/range.ts +0 -7
  203. package/src/result.ts +0 -107
  204. package/src/safe.ts +0 -12
  205. package/src/server/http/apps.ts +0 -26
  206. package/src/server/http/css.ts +0 -49
  207. package/src/server/http/index.ts +0 -127
  208. package/src/server/http/response.ts +0 -60
  209. package/src/server/http/sitemap.ts +0 -24
  210. package/src/server/http/static.ts +0 -28
  211. package/src/server/http/typescript.ts +0 -46
  212. package/src/server/live-reload.ts +0 -208
  213. package/src/server/ws/frame.ts +0 -36
  214. package/src/server/ws/handshake.ts +0 -42
  215. package/src/server/ws/index.ts +0 -100
  216. package/src/ssg/bundle.ts +0 -85
  217. package/src/ssg/copy-public.ts +0 -44
  218. package/src/ssg/discover.ts +0 -143
  219. package/src/ssg/main.ts +0 -168
  220. package/src/ssg/ssg.ts +0 -134
  221. package/src/transpile.mjs +0 -16
  222. package/src/zip/spec.txt +0 -3260
  223. package/tsconfig.json +0 -34
  224. /package/{src/awaitable.ts → lib/esm/awaitable.d.ts} +0 -0
  225. /package/{src/dom/index.ts → lib/esm/dom/index.d.ts} +0 -0
  226. /package/{src/dom/types/dom.ts → lib/esm/dom/types/dom.d.ts} +0 -0
package/src/safe.ts DELETED
@@ -1,12 +0,0 @@
1
- import { error } from "./log.ts";
2
-
3
- export const safe =
4
- <A extends unknown[], R>(fn: (...args: A) => R, r: R): ((...args: A) => R) =>
5
- (...args: A) => {
6
- try {
7
- return fn(...args);
8
- } catch (e: unknown) {
9
- error(`${e}`);
10
- return r;
11
- }
12
- };
@@ -1,26 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import type { MiddlewareFactory } from "./index.ts";
4
- import { fileResponse } from "./response.ts";
5
-
6
- /**
7
- * Searches up the request path until the first index is found.
8
- */
9
- export const findIndex: MiddlewareFactory =
10
- async ({ root }) =>
11
- async (req) => {
12
- let filename = path.join(root, req.url ?? "");
13
- if (path.basename(filename).match(/\.[a-z]{1,3}$/)) {
14
- return undefined;
15
- }
16
- while (filename.startsWith(root)) {
17
- const index = path.join(filename, "index.html");
18
- try {
19
- const stat = await fs.stat(index);
20
- return fileResponse(index, stat);
21
- } catch (_e) {
22
- filename = path.dirname(filename);
23
- }
24
- }
25
- return undefined;
26
- };
@@ -1,49 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import type { MiddlewareFactory } from "./index.ts";
4
- import { contentResponse } from "./response.ts";
5
-
6
- function render(source: string) {
7
- // Replace `from "@scope` with `from "/@scope`, for browsers
8
- // source = source
9
- // .replaceAll(`from "@`, 'from "/@')
10
- // .replaceAll(`import("@`, 'import("/@');
11
- return contentResponse(source, "text/css");
12
- }
13
-
14
- /**
15
- * Serves .css files statically. Finds .sass files and transpiles them to css.
16
- */
17
- export const cssFileServer: MiddlewareFactory =
18
- async ({ root, scopes = {} }) =>
19
- async (req) => {
20
- const Url = new URL(req.url ?? "/", `http://${req.headers.host}`);
21
- if (Url.pathname.endsWith(".css")) {
22
- const scope = Object.entries(scopes).find(([s]) =>
23
- Url.pathname.startsWith(`/${s}`),
24
- );
25
- // Expand url with found scope
26
- Url.protocol = "file";
27
- const url = scope
28
- ? Url.pathname.replace(scope[0], scope[1])
29
- : Url.pathname;
30
- let filename = path.join(root, url);
31
- try {
32
- const stat = await fs.stat(filename);
33
- if (stat.isFile()) {
34
- const css = (await fs.readFile(filename)).toString("utf-8");
35
- return render(css);
36
- }
37
- } catch {}
38
-
39
- filename = filename.replace(/\.css$/, ".scss");
40
- try {
41
- const stat = await fs.stat(filename);
42
- if (stat.isFile()) {
43
- const css = (await fs.readFile(filename)).toString();
44
- return render(css);
45
- }
46
- } catch {}
47
- }
48
- return undefined;
49
- };
@@ -1,127 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import {
4
- createServer,
5
- type IncomingMessage,
6
- type RequestListener,
7
- type ServerResponse,
8
- } from "node:http";
9
- import type { AddressInfo } from "node:net";
10
- import * as path from "node:path";
11
- import { info } from "../../log.ts";
12
- import { findIndex } from "./apps.ts";
13
- import { cssFileServer } from "./css.ts";
14
- import { fileResponse } from "./response.ts";
15
- import { sitemap } from "./sitemap.ts";
16
- import { staticFileServer } from "./static.ts";
17
- import { tsFileServer } from "./typescript.ts";
18
-
19
- export interface StaticResponse {
20
- status: 200 | 404 | 500;
21
- content: Buffer;
22
- contentType: string;
23
- contentLength?: number;
24
- headers?: Map<string, string>;
25
- }
26
-
27
- export interface ServerConfig {
28
- root: string;
29
- scopes?: Record<`@${string}`, string>;
30
- }
31
-
32
- export type MiddlewareFactory = (
33
- config: ServerConfig,
34
- ) => Promise<StaticMiddleware>;
35
-
36
- export type StaticMiddleware = (
37
- req: IncomingMessage,
38
- ) => Promise<undefined | (() => Promise<StaticResponse>)>;
39
-
40
- const notFound: MiddlewareFactory =
41
- async ({ root }) =>
42
- async () =>
43
- fileResponse(
44
- // path.join(path.dirname(FLAGS.argv0), "404.html"),
45
- path.join(root, "404.html"),
46
- undefined,
47
- 404,
48
- );
49
-
50
- const BASE_MIDDLEWARES: MiddlewareFactory[] = [
51
- sitemap,
52
- tsFileServer,
53
- cssFileServer,
54
- staticFileServer,
55
- findIndex,
56
- notFound,
57
- ];
58
-
59
- const error = (res: ServerResponse, message: string) => {
60
- console.error(message);
61
- res.statusCode = 500;
62
- res.write(message);
63
- res.end();
64
- return true;
65
- };
66
-
67
- const sendContent = async (
68
- res: ServerResponse,
69
- { content, contentType, contentLength, headers }: StaticResponse,
70
- ) => {
71
- res.setHeader("Content-Length", `${contentLength}`);
72
- res.setHeader("Content-Type", contentType);
73
- if (headers) {
74
- for (const [header, value] of headers.entries()) {
75
- res.setHeader(header.toLowerCase(), value);
76
- }
77
- }
78
- await res.write(content);
79
- res.end();
80
- return true;
81
- };
82
-
83
- const log = (req: IncomingMessage) => {
84
- const when = new Date().toISOString();
85
- const who = req.socket.remoteAddress;
86
- const what = req.url;
87
- const how = `${req.method} ${what}`;
88
- info("Request", { when, who, how });
89
- };
90
-
91
- export const makeServer = async (
92
- config: ServerConfig,
93
- middlewares: MiddlewareFactory[] = [],
94
- ) => {
95
- const handlers = await Promise.all(
96
- [...middlewares, ...BASE_MIDDLEWARES].map(async (m) => m(config)),
97
- );
98
- const middlewareHandler: RequestListener = async (req, res) => {
99
- log(req);
100
- let handler: undefined | (() => Promise<StaticResponse>);
101
- try {
102
- for (const middleware of handlers) {
103
- handler = await middleware(req);
104
- if (handler !== undefined) {
105
- break;
106
- }
107
- }
108
- if (handler) {
109
- sendContent(res, await handler());
110
- } else {
111
- res.end();
112
- }
113
- } catch (e) {
114
- error(res, `${(e as Error).message}\n${(e as Error).stack}`);
115
- }
116
- };
117
-
118
- // TODO(https)
119
- const server = createServer(middlewareHandler);
120
-
121
- server.on("listening", () => {
122
- const { address, port } = server.address() as AddressInfo;
123
- info("Server listening", { address: `http://${address}:${port}` });
124
- });
125
-
126
- return server;
127
- };
@@ -1,60 +0,0 @@
1
- import type { Stats } from "node:fs";
2
- import * as fs from "node:fs/promises";
3
- import type { StaticResponse } from ".";
4
-
5
- const MIME_TYPES = {
6
- js: "text/javascript",
7
- ts: "text/javascript",
8
- json: "text/javascript",
9
- css: "text/css",
10
- html: "text/html",
11
- png: "image/png",
12
- jpg: "image/jpeg",
13
- jpeg: "image/jpeg",
14
- svg: "image/svg+xml",
15
- eot: "application/vnd.ms-fontobject",
16
- ttf: "application/font-ttf",
17
- woff: "application/font-woff",
18
- woff2: "application/font-woff2",
19
- } as const;
20
-
21
- const mime = (basename: string) => {
22
- const extension = basename
23
- .substring(basename.lastIndexOf(".") + 1)
24
- .toLowerCase();
25
- return (
26
- MIME_TYPES[extension as keyof typeof MIME_TYPES] ??
27
- "application/octet-stream"
28
- );
29
- };
30
-
31
- export const fileResponse =
32
- (filename: string, stat?: Stats, status: 200 | 404 | 500 = 200) =>
33
- async (): Promise<StaticResponse> => {
34
- if (!stat) {
35
- stat = await fs.stat(filename);
36
- }
37
- const content = await fs.readFile(filename);
38
- const contentType = mime(filename);
39
- const contentLength = stat.size;
40
- return { status, contentType, contentLength, content };
41
- };
42
-
43
- const CHARSET = "utf-8";
44
- export const contentResponse =
45
- (
46
- content: string,
47
- contentType: string,
48
- status: 200 | 404 | 500 = 200,
49
- headers: Map<string, string> = new Map(),
50
- ) =>
51
- async (): Promise<StaticResponse> => {
52
- const contentBuffer = Buffer.from(content, CHARSET);
53
- return {
54
- content: contentBuffer,
55
- contentType: `${contentType.split(";")[0]}; charset=${CHARSET}`,
56
- status,
57
- contentLength: contentBuffer.length,
58
- headers,
59
- };
60
- };
@@ -1,24 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { info } from "../../log.ts";
4
- import type { MiddlewareFactory } from "./index.ts";
5
- import { contentResponse } from "./response.ts";
6
-
7
- export const sitemap: MiddlewareFactory = async ({ root }) => {
8
- const apps: string[] = [];
9
- for await (const file of fs.glob("**/index.html", {
10
- cwd: root,
11
- withFileTypes: false,
12
- exclude: (name) => name === "node_modules",
13
- })) {
14
- const index = `/${file.replaceAll(path.sep, "/")}`;
15
- info("Adding to sitemap", { index });
16
- apps.push(index);
17
- }
18
- return async (req) => {
19
- if ((req.url ?? "").endsWith("sitemap.json")) {
20
- return contentResponse(JSON.stringify(apps), "application/json");
21
- }
22
- return undefined;
23
- };
24
- };
@@ -1,28 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import type { MiddlewareFactory } from "./index.ts";
4
- import { fileResponse } from "./response.ts";
5
-
6
- export const staticFileServer: MiddlewareFactory =
7
- async ({ root, scopes = {} }) =>
8
- async (req) => {
9
- const scope = Object.entries(scopes).find(([s]) =>
10
- req.url?.startsWith(`/${s}`),
11
- );
12
- const url = new URL(req.url ?? "", "http://localhost");
13
- const pathname = scope
14
- ? url.pathname.replace(scope[0], scope[1])
15
- : url.pathname;
16
- const filename = path.join(root, pathname);
17
-
18
- try {
19
- const stat = await fs.stat(filename);
20
- return stat.isDirectory() ? undefined : fileResponse(filename, stat);
21
- } catch (e) {
22
- if ((e as { code?: string }).code === "ENOENT") {
23
- return fileResponse(filename, undefined, 404);
24
- }
25
- console.error(e);
26
- return undefined;
27
- }
28
- };
@@ -1,46 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { transpile } from "../../transpile.mjs";
4
- import type { MiddlewareFactory } from "./index.ts";
5
- import { contentResponse } from "./response.ts";
6
-
7
- function render(source: string, headers: Map<string, string>) {
8
- // Replace `from "@scope` with `from "/@scope`, for browsers
9
- source = source
10
- .replaceAll(`from "@`, 'from "/@')
11
- .replaceAll(`import("@`, 'import("/@');
12
- return contentResponse(source, "application/javascript", 200, headers);
13
- }
14
-
15
- /**
16
- * Serves .js files statically. Finds .ts files and transpiles them to JS.
17
- */
18
- export const tsFileServer: MiddlewareFactory =
19
- async ({ root, scopes = {} }) =>
20
- async (req) => {
21
- if (req.url?.endsWith(".js") || req.url?.endsWith(".ts")) {
22
- const scope = Object.entries(scopes).find(([s]) =>
23
- req.url?.startsWith(`/${s}`),
24
- );
25
- // Expand url with found scope
26
- const url = scope ? req.url.replace(scope[0], scope[1]) : req.url;
27
- let filename = path.join(root, url);
28
- const stat = await fs.stat(filename);
29
- if (stat.isFile()) {
30
- let js: string;
31
- const headers: Map<string, string> = new Map();
32
- if (filename.endsWith(".ts")) {
33
- js = await transpile(filename, () => fs.readFile(filename));
34
- headers.set("SourceMap", `${url}.map`);
35
- js += `\n//# sourceURL=${url}`;
36
- } else {
37
- if (url.endsWith(".ts.map")) {
38
- filename = filename.replace(/\.map$/, "");
39
- }
40
- js = (await fs.readFile(filename)).toString("utf-8");
41
- }
42
- return render(js, headers);
43
- }
44
- }
45
- return undefined;
46
- };
@@ -1,208 +0,0 @@
1
- import { type FSWatcher, watch } from "node:fs";
2
- import * as fs from "node:fs/promises";
3
- import type { AddressInfo } from "node:net";
4
- import * as path from "node:path";
5
- import { type MiddlewareFactory, makeServer } from "./http/index.ts";
6
- import { contentResponse } from "./http/response.ts";
7
- import { attachWebSocketServer, type WebSocketHub } from "./ws/index.ts";
8
-
9
- // Live-reload feature for a (optionally watched) jiffies static dev server.
10
- // Shaped as stock jiffies middlewares + a generic watcher composed with the
11
- // ws/ transport, so a downstream server adds `--port`/`--host`/`--watch` via
12
- // jiffiesArgs and injects only its own build command and watched paths.
13
- // See .agents/developer/2026-06-10-A-upstream-livereload-client/design.md.
14
-
15
- // The injected client snippet opens a WebSocket to `path` and reloads the page
16
- // on any pushed message. On close it reconnects with a short fixed backoff,
17
- // which recovers from a server restart or a socket dropped during a rebuild.
18
- // Errors are swallowed; the close handler drives the reconnect. `path` is a
19
- // parameter so the caller owns the reload route.
20
- export function webSocketReloadClient(path: string): string {
21
- return `<script>(function(){function connect(){var ws=new WebSocket((location.protocol==="https:"?"wss://":"ws://")+location.host+${JSON.stringify(
22
- path,
23
- )});ws.onmessage=function(){location.reload();};ws.onerror=function(){};ws.onclose=function(){setTimeout(connect,1000);};}connect();})();</script>`;
24
- }
25
-
26
- // Injects the reload snippet into HTML navigations. Mirrors jiffies findIndex
27
- // (walk up for index.html) for extensionless paths; serves .html directly.
28
- // Asset/CSS/TS requests fall through to the base middlewares.
29
- export const htmlInjector =
30
- (snippet: string): MiddlewareFactory =>
31
- async ({ root }) =>
32
- async (req) => {
33
- const { pathname } = new URL(req.url ?? "/", "http://localhost");
34
- const decoded = decodeURIComponent(pathname);
35
- const ext = path.extname(decoded);
36
- let target: string | undefined;
37
- if (ext === ".html") {
38
- target = path.join(root, decoded);
39
- } else if (ext === "") {
40
- let dir = path.join(root, decoded);
41
- while (dir.startsWith(root)) {
42
- const idx = path.join(dir, "index.html");
43
- try {
44
- if ((await fs.stat(idx)).isFile()) {
45
- target = idx;
46
- break;
47
- }
48
- } catch {}
49
- dir = path.dirname(dir);
50
- }
51
- }
52
- if (!target) return undefined;
53
- try {
54
- if (!(await fs.stat(target)).isFile()) return undefined;
55
- const html = await fs.readFile(target, "utf-8");
56
- const injected = html.includes("</body>")
57
- ? html.replace("</body>", `${snippet}</body>`)
58
- : html + snippet;
59
- return contentResponse(injected, "text/html");
60
- } catch {
61
- return undefined;
62
- }
63
- };
64
-
65
- // Ignores dotfiles and node_modules. The argument is the full resolved path, so
66
- // callers can compose this with project-specific generated-output paths.
67
- export const ignoreDefault = (file: string): boolean =>
68
- /(^|[\\/])\./.test(file) || file.includes("node_modules");
69
-
70
- // Watches dirs recursively, coalescing bursts into a single trailing call. The
71
- // ignore predicate receives the resolved absolute path of each changed file, so
72
- // it can exclude build outputs written back into a watched dir (which would
73
- // otherwise retrigger the build endlessly).
74
- export function watchTree(
75
- dirs: string[],
76
- onChange: () => void,
77
- {
78
- debounceMs = 150,
79
- ignore = ignoreDefault,
80
- }: { debounceMs?: number; ignore?: (file: string) => boolean } = {},
81
- ): { close: () => void } {
82
- let timer: ReturnType<typeof setTimeout> | undefined;
83
- const watchers: FSWatcher[] = [];
84
- for (const dir of dirs) {
85
- try {
86
- watchers.push(
87
- watch(dir, { recursive: true }, (_event, filename) => {
88
- if (!filename) return;
89
- if (ignore(path.join(dir, filename.toString()))) return;
90
- if (timer) clearTimeout(timer);
91
- timer = setTimeout(onChange, debounceMs);
92
- }),
93
- );
94
- } catch {
95
- // Directory may not exist; skip it.
96
- }
97
- }
98
- return {
99
- close: () => {
100
- if (timer) clearTimeout(timer);
101
- for (const w of watchers) w.close();
102
- },
103
- };
104
- }
105
-
106
- // Serializes rebuilds: a change during a build sets a dirty flag that triggers
107
- // exactly one more rebuild afterward. `onBuilt` runs once per successful rebuild
108
- // (each time the inner rebuild resolves true), letting the caller broadcast a
109
- // reload without the client needing a queryable build id.
110
- export function createRebuilder(
111
- rebuild: () => Promise<boolean>,
112
- onBuilt?: () => void,
113
- ): {
114
- run: () => Promise<void>;
115
- trigger: () => void;
116
- } {
117
- let building = false;
118
- let dirty = false;
119
- const run = async (): Promise<void> => {
120
- if (building) {
121
- dirty = true;
122
- return;
123
- }
124
- building = true;
125
- try {
126
- do {
127
- dirty = false;
128
- if (await rebuild()) onBuilt?.();
129
- } while (dirty);
130
- } finally {
131
- building = false;
132
- }
133
- };
134
- return { run, trigger: () => void run() };
135
- }
136
-
137
- export interface WatchServerOptions {
138
- root: string;
139
- watchDirs: string[];
140
- rebuild: () => Promise<boolean>;
141
- debounceMs?: number;
142
- // Resolved paths to exclude from watching (e.g. build outputs written back
143
- // into a watched dir). Defaults to ignoreDefault when omitted.
144
- ignore?: (file: string) => boolean;
145
- port?: number;
146
- host?: string;
147
- // The reload route. Defaults to "/__livereload". The injected snippet and the
148
- // ws hub are both bound to this path.
149
- reloadPath?: string;
150
- }
151
-
152
- // Initial build, then serve `root` with the injector middleware, attach a
153
- // WebSocket hub at reloadPath, and watch `watchDirs`. Each successful rebuild
154
- // broadcasts "reload" to connected clients. Returns the bound port and a
155
- // close() for clean shutdown. The hub is late-bound: the initial build runs
156
- // before it exists, so its onBuilt broadcast is a no-op (no clients yet).
157
- export async function startWatchServer(
158
- opts: WatchServerOptions,
159
- ): Promise<{ port: number; close: () => Promise<void> }> {
160
- const reloadPath = opts.reloadPath ?? "/__livereload";
161
- let hub: WebSocketHub | undefined;
162
- const rebuilder = createRebuilder(opts.rebuild, () =>
163
- hub?.broadcast("reload"),
164
- );
165
- await rebuilder.run();
166
- const snippet = webSocketReloadClient(reloadPath);
167
- const server = await makeServer({ root: opts.root }, [htmlInjector(snippet)]);
168
- const watchOpts: { debounceMs?: number; ignore?: (file: string) => boolean } =
169
- {};
170
- if (opts.debounceMs !== undefined) watchOpts.debounceMs = opts.debounceMs;
171
- if (opts.ignore !== undefined) watchOpts.ignore = opts.ignore;
172
- const watcher = watchTree(opts.watchDirs, rebuilder.trigger, watchOpts);
173
- await new Promise<void>((resolve) =>
174
- server.listen(opts.port ?? 8080, opts.host ?? "127.0.0.1", resolve),
175
- );
176
- hub = attachWebSocketServer(server, { path: reloadPath });
177
- const address = server.address() as AddressInfo;
178
- return {
179
- port: address.port,
180
- close: () =>
181
- new Promise<void>((resolve) => {
182
- hub?.close();
183
- watcher.close();
184
- server.close(() => resolve());
185
- }),
186
- };
187
- }
188
-
189
- export interface JiffiesArgsParams {
190
- port?: string; // default for --port (default "8080")
191
- host?: string; // default for --host (default "0.0.0.0")
192
- }
193
-
194
- // A parseArgs `options` fragment for launching a (optionally watched) jiffies
195
- // static server. Spread into a downstream's parseArgs options so it owns the
196
- // entrypoint and the `values.watch` branch. `params` overrides the port/host
197
- // defaults; `watch` defaults to false.
198
- export function jiffiesArgs(params?: JiffiesArgsParams): {
199
- port: { type: "string"; default: string };
200
- host: { type: "string"; default: string };
201
- watch: { type: "boolean"; default: false };
202
- } {
203
- return {
204
- port: { type: "string", default: params?.port ?? "8080" },
205
- host: { type: "string", default: params?.host ?? "0.0.0.0" },
206
- watch: { type: "boolean", default: false },
207
- };
208
- }
@@ -1,36 +0,0 @@
1
- // Encodes `text` as a single unmasked server->client WebSocket text frame:
2
- // FIN set + opcode 0x1 => first byte 0x81; mask bit clear; 7-bit / 16-bit /
3
- // 64-bit extended payload-length selection by utf8 byte count. Server-to-client
4
- // frames are never masked (RFC 6455).
5
- export function encodeTextFrame(text: string): Buffer {
6
- const payload = Buffer.from(text, "utf-8");
7
- const length = payload.length;
8
- const fin = 0x81; // FIN set, opcode 0x1 (text).
9
-
10
- let header: Buffer;
11
- if (length < 126) {
12
- header = Buffer.from([fin, length]);
13
- } else if (length <= 0xffff) {
14
- header = Buffer.alloc(4);
15
- header[0] = fin;
16
- header[1] = 0x7e;
17
- header.writeUInt16BE(length, 2);
18
- } else {
19
- header = Buffer.alloc(10);
20
- header[0] = fin;
21
- header[1] = 0x7f;
22
- header.writeBigUInt64BE(BigInt(length), 2);
23
- }
24
-
25
- return Buffer.concat([header, payload]);
26
- }
27
-
28
- // Reads the opcode nibble of an inbound (client, masked) frame for control-frame
29
- // handling: 0x1 text, 0x8 close, 0x9 ping, 0xA pong, etc. Returns undefined for a
30
- // partial/unreadable buffer. Does NOT decode the masked payload.
31
- export function readOpcode(buffer: Buffer): number | undefined {
32
- if (buffer.length === 0) {
33
- return undefined;
34
- }
35
- return buffer[0] & 0x0f;
36
- }
@@ -1,42 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import type { IncomingMessage } from "node:http";
3
- import type { Duplex } from "node:stream";
4
-
5
- // The fixed GUID concatenated to the client key before hashing, per RFC 6455.
6
- const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
7
-
8
- // base64( SHA-1( key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) ), per RFC 6455.
9
- // Pure function of the client's Sec-WebSocket-Key.
10
- export function acceptKey(secWebSocketKey: string): string {
11
- return createHash("sha1")
12
- .update(secWebSocketKey + WS_GUID)
13
- .digest("base64");
14
- }
15
-
16
- // Validates the upgrade request, writes the "101 Switching Protocols" response
17
- // (Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Accept: acceptKey(...))
18
- // to the raw socket, and returns true. Returns false WITHOUT writing on a
19
- // malformed upgrade (missing/non-"websocket" Upgrade header or missing
20
- // Sec-WebSocket-Key), leaving the socket for the caller to destroy.
21
- export function completeHandshake(
22
- req: IncomingMessage,
23
- socket: Duplex,
24
- ): boolean {
25
- const key = req.headers["sec-websocket-key"];
26
- const upgrade = req.headers.upgrade;
27
- if (typeof key !== "string" || key.length === 0) {
28
- return false;
29
- }
30
- if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket") {
31
- return false;
32
- }
33
-
34
- const response =
35
- "HTTP/1.1 101 Switching Protocols\r\n" +
36
- "Upgrade: websocket\r\n" +
37
- "Connection: Upgrade\r\n" +
38
- `Sec-WebSocket-Accept: ${acceptKey(key)}\r\n` +
39
- "\r\n";
40
- socket.write(response);
41
- return true;
42
- }