@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
@@ -0,0 +1,23 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { fileResponse } from "./response.js";
4
+ /**
5
+ * Searches up the request path until the first index is found.
6
+ */
7
+ export const findIndex = async ({ root }) => async (req) => {
8
+ let filename = path.join(root, req.url ?? "");
9
+ if (path.basename(filename).match(/\.[a-z]{1,3}$/)) {
10
+ return undefined;
11
+ }
12
+ while (filename.startsWith(root)) {
13
+ const index = path.join(filename, "index.html");
14
+ try {
15
+ const stat = await fs.stat(index);
16
+ return fileResponse(index, stat);
17
+ }
18
+ catch (_e) {
19
+ filename = path.dirname(filename);
20
+ }
21
+ }
22
+ return undefined;
23
+ };
@@ -0,0 +1,5 @@
1
+ import type { MiddlewareFactory } from "./index.ts";
2
+ /**
3
+ * Serves .css files statically. Finds .sass files and transpiles them to css.
4
+ */
5
+ export declare const cssFileServer: MiddlewareFactory;
@@ -0,0 +1,43 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { contentResponse } from "./response.js";
4
+ function render(source) {
5
+ // Replace `from "@scope` with `from "/@scope`, for browsers
6
+ // source = source
7
+ // .replaceAll(`from "@`, 'from "/@')
8
+ // .replaceAll(`import("@`, 'import("/@');
9
+ return contentResponse(source, "text/css");
10
+ }
11
+ /**
12
+ * Serves .css files statically. Finds .sass files and transpiles them to css.
13
+ */
14
+ export const cssFileServer = async ({ root, scopes = {} }) => async (req) => {
15
+ const Url = new URL(req.url ?? "/", `http://${req.headers.host}`);
16
+ if (Url.pathname.endsWith(".css")) {
17
+ const scope = Object.entries(scopes).find(([s]) => Url.pathname.startsWith(`/${s}`));
18
+ // Expand url with found scope
19
+ Url.protocol = "file";
20
+ const url = scope
21
+ ? Url.pathname.replace(scope[0], scope[1])
22
+ : Url.pathname;
23
+ let filename = path.join(root, url);
24
+ try {
25
+ const stat = await fs.stat(filename);
26
+ if (stat.isFile()) {
27
+ const css = (await fs.readFile(filename)).toString("utf-8");
28
+ return render(css);
29
+ }
30
+ }
31
+ catch { }
32
+ filename = filename.replace(/\.css$/, ".scss");
33
+ try {
34
+ const stat = await fs.stat(filename);
35
+ if (stat.isFile()) {
36
+ const css = (await fs.readFile(filename)).toString();
37
+ return render(css);
38
+ }
39
+ }
40
+ catch { }
41
+ }
42
+ return undefined;
43
+ };
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { type IncomingMessage, type ServerResponse } from "node:http";
3
+ export interface StaticResponse {
4
+ status: 200 | 404 | 500;
5
+ content: Buffer;
6
+ contentType: string;
7
+ contentLength?: number;
8
+ headers?: Map<string, string>;
9
+ }
10
+ export interface ServerConfig {
11
+ root: string;
12
+ scopes?: Record<`@${string}`, string>;
13
+ }
14
+ export type MiddlewareFactory = (config: ServerConfig) => Promise<StaticMiddleware>;
15
+ export type StaticMiddleware = (req: IncomingMessage) => Promise<undefined | (() => Promise<StaticResponse>)>;
16
+ export declare const makeServer: (config: ServerConfig, middlewares?: MiddlewareFactory[]) => Promise<import("http").Server<typeof IncomingMessage, typeof ServerResponse>>;
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { createServer, } from "node:http";
3
+ import * as path from "node:path";
4
+ import { info } from "../../log.js";
5
+ import { findIndex } from "./apps.js";
6
+ import { cssFileServer } from "./css.js";
7
+ import { fileResponse } from "./response.js";
8
+ import { sitemap } from "./sitemap.js";
9
+ import { staticFileServer } from "./static.js";
10
+ import { tsFileServer } from "./typescript.js";
11
+ const notFound = async ({ root }) => async () => fileResponse(
12
+ // path.join(path.dirname(FLAGS.argv0), "404.html"),
13
+ path.join(root, "404.html"), undefined, 404);
14
+ const BASE_MIDDLEWARES = [
15
+ sitemap,
16
+ tsFileServer,
17
+ cssFileServer,
18
+ staticFileServer,
19
+ findIndex,
20
+ notFound,
21
+ ];
22
+ const error = (res, message) => {
23
+ console.error(message);
24
+ res.statusCode = 500;
25
+ res.write(message);
26
+ res.end();
27
+ return true;
28
+ };
29
+ const sendContent = async (res, { content, contentType, contentLength, headers }) => {
30
+ res.setHeader("Content-Length", `${contentLength}`);
31
+ res.setHeader("Content-Type", contentType);
32
+ if (headers) {
33
+ for (const [header, value] of headers.entries()) {
34
+ res.setHeader(header.toLowerCase(), value);
35
+ }
36
+ }
37
+ await res.write(content);
38
+ res.end();
39
+ return true;
40
+ };
41
+ const log = (req) => {
42
+ const when = new Date().toISOString();
43
+ const who = req.socket.remoteAddress;
44
+ const what = req.url;
45
+ const how = `${req.method} ${what}`;
46
+ info("Request", { when, who, how });
47
+ };
48
+ export const makeServer = async (config, middlewares = []) => {
49
+ const handlers = await Promise.all([...middlewares, ...BASE_MIDDLEWARES].map(async (m) => m(config)));
50
+ const middlewareHandler = async (req, res) => {
51
+ log(req);
52
+ let handler;
53
+ try {
54
+ for (const middleware of handlers) {
55
+ handler = await middleware(req);
56
+ if (handler !== undefined) {
57
+ break;
58
+ }
59
+ }
60
+ if (handler) {
61
+ sendContent(res, await handler());
62
+ }
63
+ else {
64
+ res.end();
65
+ }
66
+ }
67
+ catch (e) {
68
+ error(res, `${e.message}\n${e.stack}`);
69
+ }
70
+ };
71
+ // TODO(https)
72
+ const server = createServer(middlewareHandler);
73
+ server.on("listening", () => {
74
+ const { address, port } = server.address();
75
+ info("Server listening", { address: `http://${address}:${port}` });
76
+ });
77
+ return server;
78
+ };
@@ -0,0 +1,4 @@
1
+ import type { Stats } from "node:fs";
2
+ import type { StaticResponse } from ".";
3
+ export declare const fileResponse: (filename: string, stat?: Stats, status?: 200 | 404 | 500) => () => Promise<StaticResponse>;
4
+ export declare const contentResponse: (content: string, contentType: string, status?: 200 | 404 | 500, headers?: Map<string, string>) => () => Promise<StaticResponse>;
@@ -0,0 +1,43 @@
1
+ import * as fs from "node:fs/promises";
2
+ const MIME_TYPES = {
3
+ js: "text/javascript",
4
+ ts: "text/javascript",
5
+ json: "text/javascript",
6
+ css: "text/css",
7
+ html: "text/html",
8
+ png: "image/png",
9
+ jpg: "image/jpeg",
10
+ jpeg: "image/jpeg",
11
+ svg: "image/svg+xml",
12
+ eot: "application/vnd.ms-fontobject",
13
+ ttf: "application/font-ttf",
14
+ woff: "application/font-woff",
15
+ woff2: "application/font-woff2",
16
+ };
17
+ const mime = (basename) => {
18
+ const extension = basename
19
+ .substring(basename.lastIndexOf(".") + 1)
20
+ .toLowerCase();
21
+ return (MIME_TYPES[extension] ??
22
+ "application/octet-stream");
23
+ };
24
+ export const fileResponse = (filename, stat, status = 200) => async () => {
25
+ if (!stat) {
26
+ stat = await fs.stat(filename);
27
+ }
28
+ const content = await fs.readFile(filename);
29
+ const contentType = mime(filename);
30
+ const contentLength = stat.size;
31
+ return { status, contentType, contentLength, content };
32
+ };
33
+ const CHARSET = "utf-8";
34
+ export const contentResponse = (content, contentType, status = 200, headers = new Map()) => async () => {
35
+ const contentBuffer = Buffer.from(content, CHARSET);
36
+ return {
37
+ content: contentBuffer,
38
+ contentType: `${contentType.split(";")[0]}; charset=${CHARSET}`,
39
+ status,
40
+ contentLength: contentBuffer.length,
41
+ headers,
42
+ };
43
+ };
@@ -0,0 +1,2 @@
1
+ import type { MiddlewareFactory } from "./index.ts";
2
+ export declare const sitemap: MiddlewareFactory;
@@ -0,0 +1,22 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { info } from "../../log.js";
4
+ import { contentResponse } from "./response.js";
5
+ export const sitemap = async ({ root }) => {
6
+ const apps = [];
7
+ for await (const file of fs.glob("**/index.html", {
8
+ cwd: root,
9
+ withFileTypes: false,
10
+ exclude: (name) => name === "node_modules",
11
+ })) {
12
+ const index = `/${file.replaceAll(path.sep, "/")}`;
13
+ info("Adding to sitemap", { index });
14
+ apps.push(index);
15
+ }
16
+ return async (req) => {
17
+ if ((req.url ?? "").endsWith("sitemap.json")) {
18
+ return contentResponse(JSON.stringify(apps), "application/json");
19
+ }
20
+ return undefined;
21
+ };
22
+ };
@@ -0,0 +1,2 @@
1
+ import type { MiddlewareFactory } from "./index.ts";
2
+ export declare const staticFileServer: MiddlewareFactory;
@@ -0,0 +1,22 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { fileResponse } from "./response.js";
4
+ export const staticFileServer = async ({ root, scopes = {} }) => async (req) => {
5
+ const scope = Object.entries(scopes).find(([s]) => req.url?.startsWith(`/${s}`));
6
+ const url = new URL(req.url ?? "", "http://localhost");
7
+ const pathname = scope
8
+ ? url.pathname.replace(scope[0], scope[1])
9
+ : url.pathname;
10
+ const filename = path.join(root, pathname);
11
+ try {
12
+ const stat = await fs.stat(filename);
13
+ return stat.isDirectory() ? undefined : fileResponse(filename, stat);
14
+ }
15
+ catch (e) {
16
+ if (e.code === "ENOENT") {
17
+ return fileResponse(filename, undefined, 404);
18
+ }
19
+ console.error(e);
20
+ return undefined;
21
+ }
22
+ };
@@ -0,0 +1,5 @@
1
+ import type { MiddlewareFactory } from "./index.ts";
2
+ /**
3
+ * Serves .js files statically. Finds .ts files and transpiles them to JS.
4
+ */
5
+ export declare const tsFileServer: MiddlewareFactory;
@@ -0,0 +1,40 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { transpile } from "../../transpile.mjs";
4
+ import { contentResponse } from "./response.js";
5
+ function render(source, headers) {
6
+ // Replace `from "@scope` with `from "/@scope`, for browsers
7
+ source = source
8
+ .replaceAll(`from "@`, 'from "/@')
9
+ .replaceAll(`import("@`, 'import("/@');
10
+ return contentResponse(source, "application/javascript", 200, headers);
11
+ }
12
+ /**
13
+ * Serves .js files statically. Finds .ts files and transpiles them to JS.
14
+ */
15
+ export const tsFileServer = async ({ root, scopes = {} }) => async (req) => {
16
+ if (req.url?.endsWith(".js") || req.url?.endsWith(".ts")) {
17
+ const scope = Object.entries(scopes).find(([s]) => req.url?.startsWith(`/${s}`));
18
+ // Expand url with found scope
19
+ const url = scope ? req.url.replace(scope[0], scope[1]) : req.url;
20
+ let filename = path.join(root, url);
21
+ const stat = await fs.stat(filename);
22
+ if (stat.isFile()) {
23
+ let js;
24
+ const headers = new Map();
25
+ if (filename.endsWith(".ts")) {
26
+ js = await transpile(filename, () => fs.readFile(filename));
27
+ headers.set("SourceMap", `${url}.map`);
28
+ js += `\n//# sourceURL=${url}`;
29
+ }
30
+ else {
31
+ if (url.endsWith(".ts.map")) {
32
+ filename = filename.replace(/\.map$/, "");
33
+ }
34
+ js = (await fs.readFile(filename)).toString("utf-8");
35
+ }
36
+ return render(js, headers);
37
+ }
38
+ }
39
+ return undefined;
40
+ };
@@ -0,0 +1,46 @@
1
+ import { type MiddlewareFactory } from "./http/index.ts";
2
+ export declare function webSocketReloadClient(path: string): string;
3
+ export declare const htmlInjector: (snippet: string) => MiddlewareFactory;
4
+ export declare const ignoreDefault: (file: string) => boolean;
5
+ export declare function watchTree(dirs: string[], onChange: () => void, { debounceMs, ignore, }?: {
6
+ debounceMs?: number;
7
+ ignore?: (file: string) => boolean;
8
+ }): {
9
+ close: () => void;
10
+ };
11
+ export declare function createRebuilder(rebuild: () => Promise<boolean>, onBuilt?: () => void): {
12
+ run: () => Promise<void>;
13
+ trigger: () => void;
14
+ };
15
+ export interface WatchServerOptions {
16
+ root: string;
17
+ watchDirs: string[];
18
+ rebuild: () => Promise<boolean>;
19
+ debounceMs?: number;
20
+ ignore?: (file: string) => boolean;
21
+ port?: number;
22
+ host?: string;
23
+ reloadPath?: string;
24
+ }
25
+ export declare function startWatchServer(opts: WatchServerOptions): Promise<{
26
+ port: number;
27
+ close: () => Promise<void>;
28
+ }>;
29
+ export interface JiffiesArgsParams {
30
+ port?: string;
31
+ host?: string;
32
+ }
33
+ export declare function jiffiesArgs(params?: JiffiesArgsParams): {
34
+ port: {
35
+ type: "string";
36
+ default: string;
37
+ };
38
+ host: {
39
+ type: "string";
40
+ default: string;
41
+ };
42
+ watch: {
43
+ type: "boolean";
44
+ default: false;
45
+ };
46
+ };
@@ -0,0 +1,161 @@
1
+ import { watch } from "node:fs";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { makeServer } from "./http/index.js";
5
+ import { contentResponse } from "./http/response.js";
6
+ import { attachWebSocketServer } from "./ws/index.js";
7
+ // Live-reload feature for a (optionally watched) jiffies static dev server.
8
+ // Shaped as stock jiffies middlewares + a generic watcher composed with the
9
+ // ws/ transport, so a downstream server adds `--port`/`--host`/`--watch` via
10
+ // jiffiesArgs and injects only its own build command and watched paths.
11
+ // See .agents/developer/2026-06-10-A-upstream-livereload-client/design.md.
12
+ // The injected client snippet opens a WebSocket to `path` and reloads the page
13
+ // on any pushed message. On close it reconnects with a short fixed backoff,
14
+ // which recovers from a server restart or a socket dropped during a rebuild.
15
+ // Errors are swallowed; the close handler drives the reconnect. `path` is a
16
+ // parameter so the caller owns the reload route.
17
+ export function webSocketReloadClient(path) {
18
+ return `<script>(function(){function connect(){var ws=new WebSocket((location.protocol==="https:"?"wss://":"ws://")+location.host+${JSON.stringify(path)});ws.onmessage=function(){location.reload();};ws.onerror=function(){};ws.onclose=function(){setTimeout(connect,1000);};}connect();})();</script>`;
19
+ }
20
+ // Injects the reload snippet into HTML navigations. Mirrors jiffies findIndex
21
+ // (walk up for index.html) for extensionless paths; serves .html directly.
22
+ // Asset/CSS/TS requests fall through to the base middlewares.
23
+ export const htmlInjector = (snippet) => async ({ root }) => async (req) => {
24
+ const { pathname } = new URL(req.url ?? "/", "http://localhost");
25
+ const decoded = decodeURIComponent(pathname);
26
+ const ext = path.extname(decoded);
27
+ let target;
28
+ if (ext === ".html") {
29
+ target = path.join(root, decoded);
30
+ }
31
+ else if (ext === "") {
32
+ let dir = path.join(root, decoded);
33
+ while (dir.startsWith(root)) {
34
+ const idx = path.join(dir, "index.html");
35
+ try {
36
+ if ((await fs.stat(idx)).isFile()) {
37
+ target = idx;
38
+ break;
39
+ }
40
+ }
41
+ catch { }
42
+ dir = path.dirname(dir);
43
+ }
44
+ }
45
+ if (!target)
46
+ return undefined;
47
+ try {
48
+ if (!(await fs.stat(target)).isFile())
49
+ return undefined;
50
+ const html = await fs.readFile(target, "utf-8");
51
+ const injected = html.includes("</body>")
52
+ ? html.replace("</body>", `${snippet}</body>`)
53
+ : html + snippet;
54
+ return contentResponse(injected, "text/html");
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ };
60
+ // Ignores dotfiles and node_modules. The argument is the full resolved path, so
61
+ // callers can compose this with project-specific generated-output paths.
62
+ export const ignoreDefault = (file) => /(^|[\\/])\./.test(file) || file.includes("node_modules");
63
+ // Watches dirs recursively, coalescing bursts into a single trailing call. The
64
+ // ignore predicate receives the resolved absolute path of each changed file, so
65
+ // it can exclude build outputs written back into a watched dir (which would
66
+ // otherwise retrigger the build endlessly).
67
+ export function watchTree(dirs, onChange, { debounceMs = 150, ignore = ignoreDefault, } = {}) {
68
+ let timer;
69
+ const watchers = [];
70
+ for (const dir of dirs) {
71
+ try {
72
+ watchers.push(watch(dir, { recursive: true }, (_event, filename) => {
73
+ if (!filename)
74
+ return;
75
+ if (ignore(path.join(dir, filename.toString())))
76
+ return;
77
+ if (timer)
78
+ clearTimeout(timer);
79
+ timer = setTimeout(onChange, debounceMs);
80
+ }));
81
+ }
82
+ catch {
83
+ // Directory may not exist; skip it.
84
+ }
85
+ }
86
+ return {
87
+ close: () => {
88
+ if (timer)
89
+ clearTimeout(timer);
90
+ for (const w of watchers)
91
+ w.close();
92
+ },
93
+ };
94
+ }
95
+ // Serializes rebuilds: a change during a build sets a dirty flag that triggers
96
+ // exactly one more rebuild afterward. `onBuilt` runs once per successful rebuild
97
+ // (each time the inner rebuild resolves true), letting the caller broadcast a
98
+ // reload without the client needing a queryable build id.
99
+ export function createRebuilder(rebuild, onBuilt) {
100
+ let building = false;
101
+ let dirty = false;
102
+ const run = async () => {
103
+ if (building) {
104
+ dirty = true;
105
+ return;
106
+ }
107
+ building = true;
108
+ try {
109
+ do {
110
+ dirty = false;
111
+ if (await rebuild())
112
+ onBuilt?.();
113
+ } while (dirty);
114
+ }
115
+ finally {
116
+ building = false;
117
+ }
118
+ };
119
+ return { run, trigger: () => void run() };
120
+ }
121
+ // Initial build, then serve `root` with the injector middleware, attach a
122
+ // WebSocket hub at reloadPath, and watch `watchDirs`. Each successful rebuild
123
+ // broadcasts "reload" to connected clients. Returns the bound port and a
124
+ // close() for clean shutdown. The hub is late-bound: the initial build runs
125
+ // before it exists, so its onBuilt broadcast is a no-op (no clients yet).
126
+ export async function startWatchServer(opts) {
127
+ const reloadPath = opts.reloadPath ?? "/__livereload";
128
+ let hub;
129
+ const rebuilder = createRebuilder(opts.rebuild, () => hub?.broadcast("reload"));
130
+ await rebuilder.run();
131
+ const snippet = webSocketReloadClient(reloadPath);
132
+ const server = await makeServer({ root: opts.root }, [htmlInjector(snippet)]);
133
+ const watchOpts = {};
134
+ if (opts.debounceMs !== undefined)
135
+ watchOpts.debounceMs = opts.debounceMs;
136
+ if (opts.ignore !== undefined)
137
+ watchOpts.ignore = opts.ignore;
138
+ const watcher = watchTree(opts.watchDirs, rebuilder.trigger, watchOpts);
139
+ await new Promise((resolve) => server.listen(opts.port ?? 8080, opts.host ?? "127.0.0.1", resolve));
140
+ hub = attachWebSocketServer(server, { path: reloadPath });
141
+ const address = server.address();
142
+ return {
143
+ port: address.port,
144
+ close: () => new Promise((resolve) => {
145
+ hub?.close();
146
+ watcher.close();
147
+ server.close(() => resolve());
148
+ }),
149
+ };
150
+ }
151
+ // A parseArgs `options` fragment for launching a (optionally watched) jiffies
152
+ // static server. Spread into a downstream's parseArgs options so it owns the
153
+ // entrypoint and the `values.watch` branch. `params` overrides the port/host
154
+ // defaults; `watch` defaults to false.
155
+ export function jiffiesArgs(params) {
156
+ return {
157
+ port: { type: "string", default: params?.port ?? "8080" },
158
+ host: { type: "string", default: params?.host ?? "0.0.0.0" },
159
+ watch: { type: "boolean", default: false },
160
+ };
161
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -1,30 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import * as process from "node:process";
3
-
4
- import { info } from "../log.ts";
5
-
3
+ import { info } from "../log.js";
6
4
  info("Starting server", { cwd: process.cwd() });
7
-
8
5
  import { parseArgs } from "node:util";
9
-
10
6
  // Declared up front so an unknown flag fails fast: parseArgs defaults to
11
7
  // strict: true, which throws on undeclared options. The throw happens at module
12
8
  // top level (before main() listens), so a malformed invocation exits non-zero
13
9
  // and never reaches the listening state. With no flags the defaults reproduce
14
10
  // the prior bind target (0.0.0.0:8080).
15
11
  const { values } = parseArgs({
16
- options: {
17
- port: { type: "string", default: "8080" },
18
- host: { type: "string", default: "0.0.0.0" },
19
- },
12
+ options: {
13
+ port: { type: "string", default: "8080" },
14
+ host: { type: "string", default: "0.0.0.0" },
15
+ },
20
16
  });
21
-
22
17
  import * as path from "node:path";
23
- import { makeServer } from "./http/index.ts";
24
-
18
+ import { makeServer } from "./http/index.js";
25
19
  async function main() {
26
- const server = await makeServer({ root: path.join(process.cwd(), "src") });
27
- server.listen(Number.parseInt(values.port, 10), values.host);
20
+ const server = await makeServer({ root: path.join(process.cwd(), "src") });
21
+ server.listen(Number.parseInt(values.port, 10), values.host);
28
22
  }
29
-
30
23
  main();
@@ -0,0 +1,2 @@
1
+ export declare function encodeTextFrame(text: string): Buffer;
2
+ export declare function readOpcode(buffer: Buffer): number | undefined;
@@ -0,0 +1,35 @@
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) {
6
+ const payload = Buffer.from(text, "utf-8");
7
+ const length = payload.length;
8
+ const fin = 0x81; // FIN set, opcode 0x1 (text).
9
+ let header;
10
+ if (length < 126) {
11
+ header = Buffer.from([fin, length]);
12
+ }
13
+ else if (length <= 0xffff) {
14
+ header = Buffer.alloc(4);
15
+ header[0] = fin;
16
+ header[1] = 0x7e;
17
+ header.writeUInt16BE(length, 2);
18
+ }
19
+ else {
20
+ header = Buffer.alloc(10);
21
+ header[0] = fin;
22
+ header[1] = 0x7f;
23
+ header.writeBigUInt64BE(BigInt(length), 2);
24
+ }
25
+ return Buffer.concat([header, payload]);
26
+ }
27
+ // Reads the opcode nibble of an inbound (client, masked) frame for control-frame
28
+ // handling: 0x1 text, 0x8 close, 0x9 ping, 0xA pong, etc. Returns undefined for a
29
+ // partial/unreadable buffer. Does NOT decode the masked payload.
30
+ export function readOpcode(buffer) {
31
+ if (buffer.length === 0) {
32
+ return undefined;
33
+ }
34
+ return buffer[0] & 0x0f;
35
+ }
@@ -0,0 +1,4 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { Duplex } from "node:stream";
3
+ export declare function acceptKey(secWebSocketKey: string): string;
4
+ export declare function completeHandshake(req: IncomingMessage, socket: Duplex): boolean;
@@ -0,0 +1,32 @@
1
+ import { createHash } from "node:crypto";
2
+ // The fixed GUID concatenated to the client key before hashing, per RFC 6455.
3
+ const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
4
+ // base64( SHA-1( key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) ), per RFC 6455.
5
+ // Pure function of the client's Sec-WebSocket-Key.
6
+ export function acceptKey(secWebSocketKey) {
7
+ return createHash("sha1")
8
+ .update(secWebSocketKey + WS_GUID)
9
+ .digest("base64");
10
+ }
11
+ // Validates the upgrade request, writes the "101 Switching Protocols" response
12
+ // (Upgrade: websocket, Connection: Upgrade, Sec-WebSocket-Accept: acceptKey(...))
13
+ // to the raw socket, and returns true. Returns false WITHOUT writing on a
14
+ // malformed upgrade (missing/non-"websocket" Upgrade header or missing
15
+ // Sec-WebSocket-Key), leaving the socket for the caller to destroy.
16
+ export function completeHandshake(req, socket) {
17
+ const key = req.headers["sec-websocket-key"];
18
+ const upgrade = req.headers.upgrade;
19
+ if (typeof key !== "string" || key.length === 0) {
20
+ return false;
21
+ }
22
+ if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket") {
23
+ return false;
24
+ }
25
+ const response = "HTTP/1.1 101 Switching Protocols\r\n" +
26
+ "Upgrade: websocket\r\n" +
27
+ "Connection: Upgrade\r\n" +
28
+ `Sec-WebSocket-Accept: ${acceptKey(key)}\r\n` +
29
+ "\r\n";
30
+ socket.write(response);
31
+ return true;
32
+ }
@@ -0,0 +1,14 @@
1
+ import type { Server } from "node:http";
2
+ import type { Duplex } from "node:stream";
3
+ export interface WebSocketHub {
4
+ broadcast(text: string): void;
5
+ readonly size: number;
6
+ close(): void;
7
+ }
8
+ export interface AttachOptions {
9
+ path: string;
10
+ onConnection?: (socket: Duplex) => void;
11
+ }
12
+ export declare function attachWebSocketServer(server: Server, options: AttachOptions): WebSocketHub;
13
+ export { encodeTextFrame, readOpcode } from "./frame.ts";
14
+ export { acceptKey, completeHandshake } from "./handshake.ts";